From 2e9aa569859ac19d37e60882e52b34588da65303 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 20 Nov 2018 12:14:32 +0100 Subject: [PATCH 001/231] Play recording on double-click --- client/src/main/java/ctbrec/ui/RecordingsTab.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index aba6a66e..b1f3d47b 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -54,6 +54,7 @@ import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.input.ContextMenuEvent; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.FlowPane; @@ -152,6 +153,14 @@ public class RecordingsTab extends Tab implements TabSelectionListener { popup.hide(); } }); + table.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + if(event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) { + Recording recording = table.getSelectionModel().getSelectedItem(); + if(recording != null) { + play(recording); + } + } + }); table.addEventFilter(KeyEvent.KEY_PRESSED, event -> { JavaFxRecording recording = table.getSelectionModel().getSelectedItem(); if (recording != null) { From 97d3be0b98914a8e71e79b90deace59140236fac Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 20 Nov 2018 14:35:06 +0100 Subject: [PATCH 002/231] Add setting to diable thumbnail updates This is a feature for people who have bandwidth / contingent restrictions. It can also help keeping the CPU usage down. --- .../src/main/java/ctbrec/ui/SettingsTab.java | 9 +++++++ client/src/main/java/ctbrec/ui/ThumbCell.java | 27 ++++++++++--------- common/src/main/java/ctbrec/Settings.java | 1 + 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java index 87ecf4b9..ea3b47ea 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/SettingsTab.java @@ -67,6 +67,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private CheckBox secureCommunication = new CheckBox(); private CheckBox chooseStreamQuality = new CheckBox(); private CheckBox multiplePlayers = new CheckBox(); + private CheckBox updateThumbnails = new CheckBox(); private RadioButton recordLocal; private RadioButton recordRemote; private ToggleGroup recordLocation; @@ -334,6 +335,14 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(chooseStreamQuality, 1, row++); + l = new Label("Update thumbnails"); + layout.add(l, 0, row); + updateThumbnails.setSelected(Config.getInstance().getSettings().updateThumbnails); + updateThumbnails.setOnAction((e) -> Config.getInstance().getSettings().updateThumbnails = updateThumbnails.isSelected()); + GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); + GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + layout.add(updateThumbnails, 1, row++); + l = new Label("Maximum resolution (0 = unlimited)"); layout.add(l, 0, row); List resolutionOptions = new ArrayList<>(); diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index 146cd680..8b563344 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -262,20 +262,23 @@ public class ThumbCell extends StackPane { private void setImage(String url) { if(!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { - Image img = new Image(url, true); + boolean updateThumbs = Config.getInstance().getSettings().updateThumbnails; + if(updateThumbs || iv.getImage() == null) { + Image img = new Image(url, true); - // wait for the image to load, otherwise the ImageView replaces the current image with an "empty" image, - // which causes to show the grey background until the image is loaded - img.progressProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, Number oldValue, Number newValue) { - if(newValue.doubleValue() == 1.0) { - //imgAspectRatio = img.getHeight() / img.getWidth(); - iv.setImage(img); - setThumbWidth(Config.getInstance().getSettings().thumbWidth); + // wait for the image to load, otherwise the ImageView replaces the current image with an "empty" image, + // which causes to show the grey background until the image is loaded + img.progressProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, Number oldValue, Number newValue) { + if(newValue.doubleValue() == 1.0) { + //imgAspectRatio = img.getHeight() / img.getWidth(); + iv.setImage(img); + setThumbWidth(Config.getInstance().getSettings().thumbWidth); + } } - } - }); + }); + } } } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 7e4a7136..d3895b3c 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -63,6 +63,7 @@ public class Settings { public String proxyUser; public String proxyPassword; public int thumbWidth = 180; + public boolean updateThumbnails = true; public int windowWidth = 1340; public int windowHeight = 800; public boolean windowMaximized = false; From e11acea52e02782b9cb1149a3c514ca8e02fc18e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 20 Nov 2018 22:30:27 +0100 Subject: [PATCH 003/231] FIX: Avoid NPE in onFailure --- .../main/java/ctbrec/sites/mfc/MyFreeCamsClient.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index 7a5ef0c6..cdc4f734 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -157,11 +157,15 @@ public class MyFreeCamsClient { @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { super.onFailure(webSocket, t, response); - int code = response.code(); - String message = response.message(); - response.close(); + if(response != null) { + int code = response.code(); + String message = response.message(); + LOG.error("MFC websocket failure: {} {}", code, message, t); + response.close(); + } else { + LOG.error("MFC websocket failure", t); + } MyFreeCamsClient.this.ws = null; - LOG.error("MFC websocket failure: {} {}", code, message, t); } private StringBuilder msgBuffer = new StringBuilder(); From ea57d4faea619ac5f6782cdedc75bfc4eec5a375 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 20 Nov 2018 22:30:59 +0100 Subject: [PATCH 004/231] Let the followed tab blink when a model is followed --- .../main/java/ctbrec/ui/ThumbOverviewTab.java | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index ba77291a..0aad00b2 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -34,6 +34,7 @@ import javafx.animation.FadeTransition; import javafx.animation.Interpolator; import javafx.animation.ParallelTransition; import javafx.animation.ScaleTransition; +import javafx.animation.Transition; import javafx.animation.TranslateTransition; import javafx.application.Platform; import javafx.collections.FXCollections; @@ -66,6 +67,7 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; import javafx.scene.transform.Transform; import javafx.util.Duration; @@ -468,7 +470,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { translate.setFromX(0); translate.setFromY(0); translate.setByX(-tx.getTx() - 200); - translate.setByY(-offsetInViewPort + getFollowedTabYPosition()); + TabProvider tabProvider = SiteUiFactory.getUi(site).getTabProvider(); + Tab followedTab = tabProvider.getFollowedTab(); + translate.setByY(-offsetInViewPort + getFollowedTabYPosition(followedTab)); StackPane.setMargin(iv, new Insets(offsetInViewPort, 0, 0, tx.getTx())); translate.setInterpolator(Interpolator.EASE_BOTH); FadeTransition fade = new FadeTransition(Duration.millis(duration), iv); @@ -482,12 +486,40 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { pt.setOnFinished((evt) -> { root.getChildren().remove(iv); }); + + String normalStyle = followedTab.getStyle(); + Color normal = Color.web("#f4f4f4"); + Color highlight = Color.web("#2b8513"); + Transition blink = new Transition() { + { + setCycleDuration(Duration.millis(500)); + } + @Override + protected void interpolate(double frac) { + double rh = highlight.getRed(); + double rn = normal.getRed(); + double diff = rh - rn; + double r = (rn + diff * frac) * 255; + double gh = highlight.getGreen(); + double gn = normal.getGreen(); + diff = gh - gn; + double g = (gn + diff * frac) * 255; + double bh = highlight.getBlue(); + double bn = normal.getBlue(); + diff = bh - bn; + double b = (bn + diff * frac) * 255; + String style = "-fx-background-color: rgb(" + r + "," + g + "," + b + ")"; + followedTab.setStyle(style); + } + }; + blink.setCycleCount(6); + blink.setAutoReverse(true); + blink.setOnFinished((evt) -> followedTab.setStyle(normalStyle)); + blink.play(); }); } - private double getFollowedTabYPosition() { - TabProvider tabProvider = SiteUiFactory.getUi(site).getTabProvider(); - Tab followedTab = tabProvider.getFollowedTab(); + private double getFollowedTabYPosition(Tab followedTab) { TabPane tabPane = getTabPane(); int idx = tabPane.getTabs().indexOf(followedTab); for (Node node : tabPane.getChildrenUnmodifiable()) { From 0dbf319575071385605407712f78989b9ad3a04b Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 21 Nov 2018 14:20:37 +0100 Subject: [PATCH 005/231] Invalidate cache when a new websocket is opened When a new connection is established (for example by the watchdog), invalidate the caches. Also don't trigger a connect, if we are already trying to connect. --- .../src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index cdc4f734..78b9e864 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -59,6 +59,7 @@ public class MyFreeCamsClient { private String chatToken; private int sessionId; private long heartBeat; + private volatile boolean connecting = false; private EvictingQueue receivedTextHistory = EvictingQueue.create(100); @@ -86,7 +87,7 @@ public class MyFreeCamsClient { Thread watchDog = new Thread(() -> { while(running) { - if (ws == null) { + if (ws == null && !connecting) { LOG.info("Websocket is null. Starting a new connection"); Request req = new Request.Builder() .url(wsUrl) @@ -126,11 +127,15 @@ public class MyFreeCamsClient { } private WebSocket createWebSocket(Request req) { + connecting = true; WebSocket ws = mfc.getHttpClient().newWebSocket(req, new WebSocketListener() { @Override public void onOpen(WebSocket webSocket, Response response) { super.onOpen(webSocket, response); try { + connecting = false; + sessionStates.invalidateAll(); + models.invalidateAll(); LOG.trace("open: [{}]", response.body().string()); webSocket.send("hello fcserver\n"); webSocket.send("fcsws_20180422\n"); @@ -147,6 +152,7 @@ public class MyFreeCamsClient { @Override public void onClosed(WebSocket webSocket, int code, String reason) { super.onClosed(webSocket, code, reason); + connecting = false; LOG.info("MFC websocket closed: {} {}", code, reason); MyFreeCamsClient.this.ws = null; if(!running) { @@ -157,6 +163,7 @@ public class MyFreeCamsClient { @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { super.onFailure(webSocket, t, response); + connecting = false; if(response != null) { int code = response.code(); String message = response.message(); From d1e6a790ba6a775281be039b4fb2bc9ef7ca3bf4 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 21 Nov 2018 15:56:17 +0100 Subject: [PATCH 006/231] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c9fc55b..534bd5f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +1.10.1 +======================== +* Double-click starts playback of recordings +* Refresh of thumbnails can be disabled + 1.10.0 ======================== * Fix: HMAC authentication didn't work for playing and downloading of a From 2202dc969f7c82d22d43c57c4b01622cdf6107a1 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 22 Nov 2018 16:30:07 +0100 Subject: [PATCH 007/231] Add setting to define the start tab When ctbrec is started, this is the first tab shown to the user. --- .../java/ctbrec/ui/CamrecApplication.java | 21 +++++++++++++++--- .../src/main/java/ctbrec/ui/SettingsTab.java | 22 ++++++++++++++++++- common/src/main/java/ctbrec/Settings.java | 1 + 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 9a4e3eb1..537eed29 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -21,6 +21,7 @@ import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import ctbrec.Config; +import ctbrec.StringUtil; import ctbrec.Version; import ctbrec.io.HttpClient; import ctbrec.recorder.LocalRecorder; @@ -110,9 +111,6 @@ public class CamrecApplication extends Application { rootPane.getTabs().add(siteTab); } } - try { - ((SiteTab)rootPane.getTabs().get(0)).selected(); - } catch(ClassCastException | IndexOutOfBoundsException e) {} RecordedModelsTab modelsTab = new RecordedModelsTab("Recording", recorder, sites); rootPane.getTabs().add(modelsTab); @@ -122,6 +120,8 @@ public class CamrecApplication extends Application { rootPane.getTabs().add(settingsTab); rootPane.getTabs().add(new DonateTabFx()); + switchToStartTab(); + primaryStage.getScene().getStylesheets().add("/ctbrec/ui/ThumbCell.css"); primaryStage.getScene().widthProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowWidth = newVal.intValue()); primaryStage.getScene().heightProperty() @@ -184,6 +184,21 @@ public class CamrecApplication extends Application { }); } + private void switchToStartTab() { + String startTab = Config.getInstance().getSettings().startTab; + if(StringUtil.isNotBlank(startTab)) { + for (Tab tab : rootPane.getTabs()) { + if(Objects.equals(startTab, tab.getText())) { + rootPane.getSelectionModel().select(tab); + break; + } + } + } + if(rootPane.getSelectionModel().getSelectedItem() instanceof TabSelectionListener) { + ((TabSelectionListener)rootPane.getSelectionModel().getSelectedItem()).selected(); + } + } + private void createRecorder() { if (config.getSettings().localRecording) { recorder = new LocalRecorder(config); diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java index ea3b47ea..11287e82 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/SettingsTab.java @@ -15,6 +15,7 @@ import ctbrec.Config; import ctbrec.Hmac; import ctbrec.Settings; import ctbrec.Settings.DirectoryStructure; +import ctbrec.StringUtil; import ctbrec.sites.ConfigUI; import ctbrec.sites.Site; import javafx.beans.value.ChangeListener; @@ -75,6 +76,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private ComboBox maxResolution; private ComboBox splitAfter; private ComboBox directoryStructure; + private ComboBox startTab; private List sites; private Label restartLabel; private Accordion credentialsAccordion = new Accordion(); @@ -373,7 +375,17 @@ public class SettingsTab extends Tab implements TabSelectionListener { splitAfter.setOnAction((e) -> Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue()); GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - maxResolution.prefWidthProperty().bind(splitAfter.widthProperty()); + + l = new Label("Start Tab"); + layout.add(l, 0, row); + startTab = new ComboBox<>(); + layout.add(startTab, 1, row++); + startTab.setOnAction((e) -> Config.getInstance().getSettings().startTab = startTab.getSelectionModel().getSelectedItem()); + GridPane.setMargin(l, new Insets(0, 0, 0, 0)); + GridPane.setMargin(startTab, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + + splitAfter.prefWidthProperty().bind(startTab.widthProperty()); + maxResolution.prefWidthProperty().bind(startTab.widthProperty()); TitledPane general = new TitledPane("General", layout); general.setCollapsible(false); @@ -589,6 +601,14 @@ public class SettingsTab extends Tab implements TabSelectionListener { @Override public void selected() { + startTab.getItems().clear(); + for(Tab tab : getTabPane().getTabs()) { + startTab.getItems().add(tab.getText()); + } + String startTabName = Config.getInstance().getSettings().startTab; + if(StringUtil.isNotBlank(startTabName)) { + startTab.getSelectionModel().select(startTabName); + } } @Override diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index d3895b3c..e7ff8ceb 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -62,6 +62,7 @@ public class Settings { public String proxyPort; public String proxyUser; public String proxyPassword; + public String startTab = "Settings"; public int thumbWidth = 180; public boolean updateThumbnails = true; public int windowWidth = 1340; From b9f24a209ebd8bd88d42162ceb9365132d1e174e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 23 Nov 2018 20:27:49 +0100 Subject: [PATCH 008/231] Implement search feature If a site supports searching, add a search field on the right side next to the filter input field. This search uses the sites search function to look for models and returns a list of matches in a popup window --- .../java/ctbrec/ui/CamrecApplication.java | 2 + .../java/ctbrec/ui/RecordedModelsTab.java | 2 +- client/src/main/java/ctbrec/ui/ThumbCell.java | 4 +- .../main/java/ctbrec/ui/ThumbOverviewTab.java | 144 +++++- .../AutoFillTextField.java | 2 +- .../main/java/ctbrec/ui/controls/Popover.css | 74 +++ .../main/java/ctbrec/ui/controls/Popover.java | 480 ++++++++++++++++++ .../ctbrec/ui/controls/PopoverTreeList.java | 112 ++++ .../java/ctbrec/ui/controls/SearchBox.css | 34 ++ .../java/ctbrec/ui/controls/SearchBox.java | 96 ++++ .../ctbrec/ui/controls/SearchPopover.java | 9 + .../ui/controls/SearchPopoverTreeList.java | 317 ++++++++++++ client/src/main/resources/anonymous.png | Bin 0 -> 3344 bytes client/src/main/resources/popover-arrow.png | Bin 0 -> 1104 bytes .../src/main/resources/popover-arrow@2x.png | Bin 0 -> 1112 bytes client/src/main/resources/popover-empty.png | Bin 0 -> 4624 bytes .../src/main/resources/popover-empty@2x.png | Bin 0 -> 9431 bytes .../main/java/ctbrec/sites/AbstractSite.java | 20 + common/src/main/java/ctbrec/sites/Site.java | 4 + .../java/ctbrec/sites/bonga/BongaCams.java | 57 +++ .../src/main/java/ctbrec/sites/cam4/Cam4.java | 62 ++- .../java/ctbrec/sites/camsoda/Camsoda.java | 46 ++ .../ctbrec/sites/chaturbate/Chaturbate.java | 43 ++ .../java/ctbrec/sites/mfc/MyFreeCams.java | 11 + .../ctbrec/sites/mfc/MyFreeCamsClient.java | 49 +- 25 files changed, 1545 insertions(+), 23 deletions(-) rename client/src/main/java/ctbrec/ui/{autofilltextbox => controls}/AutoFillTextField.java (98%) create mode 100644 client/src/main/java/ctbrec/ui/controls/Popover.css create mode 100644 client/src/main/java/ctbrec/ui/controls/Popover.java create mode 100644 client/src/main/java/ctbrec/ui/controls/PopoverTreeList.java create mode 100644 client/src/main/java/ctbrec/ui/controls/SearchBox.css create mode 100644 client/src/main/java/ctbrec/ui/controls/SearchBox.java create mode 100644 client/src/main/java/ctbrec/ui/controls/SearchPopover.java create mode 100644 client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java create mode 100644 client/src/main/resources/anonymous.png create mode 100644 client/src/main/resources/popover-arrow.png create mode 100644 client/src/main/resources/popover-arrow@2x.png create mode 100644 client/src/main/resources/popover-empty.png create mode 100644 client/src/main/resources/popover-empty@2x.png diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 537eed29..d9814e87 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -123,6 +123,8 @@ public class CamrecApplication extends Application { switchToStartTab(); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/ThumbCell.css"); + primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/SearchBox.css"); + primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/Popover.css"); primaryStage.getScene().widthProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowWidth = newVal.intValue()); primaryStage.getScene().heightProperty() .addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowHeight = newVal.intValue()); diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index 5874f8c2..63366614 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -24,7 +24,7 @@ import ctbrec.Model; import ctbrec.Recording; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; -import ctbrec.ui.autofilltextbox.AutoFillTextField; +import ctbrec.ui.controls.AutoFillTextField; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index 8b563344..52a39f89 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -181,6 +181,8 @@ public class ThumbCell extends StackPane { if(Config.getInstance().getSettings().determineResolution) { determineResolution(); } + + update(); } public void setSelected(boolean selected) { @@ -478,7 +480,7 @@ public class ThumbCell extends StackPane { setRecording(recorder.isRecording(model)); setImage(model.getPreview()); String txt = recording ? " " : ""; - txt += model.getDescription(); + txt += model.getDescription() != null ? model.getDescription() : ""; topic.setText(txt); if(Config.getInstance().getSettings().determineResolution) { diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index 0aad00b2..5fb861e4 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -30,6 +30,9 @@ import ctbrec.recorder.Recorder; import ctbrec.sites.Site; import ctbrec.sites.mfc.MyFreeCamsClient; import ctbrec.sites.mfc.MyFreeCamsModel; +import ctbrec.ui.controls.SearchBox; +import ctbrec.ui.controls.SearchPopover; +import ctbrec.ui.controls.SearchPopoverTreeList; import javafx.animation.FadeTransition; import javafx.animation.Interpolator; import javafx.animation.ParallelTransition; @@ -37,8 +40,10 @@ import javafx.animation.ScaleTransition; import javafx.animation.Transition; import javafx.animation.TranslateTransition; import javafx.application.Platform; +import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.concurrent.Task; import javafx.concurrent.Worker.State; import javafx.concurrent.WorkerStateEvent; import javafx.event.EventHandler; @@ -61,11 +66,14 @@ import javafx.scene.image.ImageView; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.scene.input.ContextMenuEvent; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.transform.Transform; @@ -96,6 +104,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { ContextMenu popup; Site site; StackPane root = new StackPane(); + Task> searchTask; + SearchPopover popover; + SearchPopoverTreeList popoverTreelist = new SearchPopoverTreeList(); private ComboBox thumbWidth; @@ -113,10 +124,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { grid.setHgap(5); grid.setVgap(5); - TextField search = new TextField(); - search.setPromptText("Filter models on this page"); - search.textProperty().addListener( (observableValue, oldValue, newValue) -> { - filter = search.getText(); + SearchBox filterInput = new SearchBox(false); + filterInput.setPromptText("Filter models on this page"); + filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> { + filter = filterInput.getText(); gridLock.lock(); try { filter(); @@ -125,12 +136,41 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { gridLock.unlock(); } }); - Tooltip searchTooltip = new Tooltip("Filter the models by their name, stream description or #hashtags.\n\n" + Tooltip filterTooltip = new Tooltip("Filter the models by their name, stream description or #hashtags.\n\n" + "If the display of stream resolution is enabled, you can even filter for public rooms or by resolution.\n\n" + "Try \"1080\" or \">720\" or \"public\""); - search.setTooltip(searchTooltip); + filterInput.setTooltip(filterTooltip); + filterInput.getStyleClass().remove("search-box-icon"); + BorderPane.setMargin(filterInput, new Insets(5)); - BorderPane.setMargin(search, new Insets(5)); + SearchBox searchInput = new SearchBox(); + searchInput.setPromptText("Search Model"); + searchInput.prefWidth(200); + searchInput.textProperty().addListener(search()); + searchInput.addEventHandler(KeyEvent.KEY_PRESSED, evt -> { + if(evt.getCode() == KeyCode.ESCAPE) { + popover.hide(); + } + }); + BorderPane.setMargin(searchInput, new Insets(5)); + + popover = new SearchPopover(); + popover.maxWidthProperty().bind(popover.minWidthProperty()); + popover.prefWidthProperty().bind(popover.minWidthProperty()); + popover.setMinWidth(400); + popover.maxHeightProperty().bind(popover.minHeightProperty()); + popover.prefHeightProperty().bind(popover.minHeightProperty()); + popover.setMinHeight(400); + popover.pushPage(popoverTreelist); + StackPane.setAlignment(popover, Pos.TOP_RIGHT); + StackPane.setMargin(popover, new Insets(50, 50, 0, 0)); + + HBox topBar = new HBox(5); + HBox.setHgrow(filterInput, Priority.ALWAYS); + topBar.getChildren().add(filterInput); + if(site.supportsSearch()) { + topBar.getChildren().add(searchInput); + } scrollPane.setContent(grid); scrollPane.setFitToHeight(true); @@ -186,14 +226,69 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { BorderPane borderPane = new BorderPane(); borderPane.setPadding(new Insets(5)); - borderPane.setTop(search); + borderPane.setTop(topBar); borderPane.setCenter(scrollPane); borderPane.setBottom(bottomPane); root.getChildren().add(borderPane); + root.getChildren().add(popover); setContent(root); } + private ChangeListener search() { + return (observableValue, oldValue, newValue) -> { + if(searchTask != null) { + searchTask.cancel(true); + } + + if(newValue.length() < 2) { + return; + } + + + searchTask = new Task>() { + @Override + protected List call() throws Exception { + if(site.searchRequiresLogin()) { + boolean loggedin = false; + try { + loggedin = SiteUiFactory.getUi(site).login(); + } catch (IOException e) { + loggedin = false; + } + if(!loggedin) { + showError("Login failed", "Search won't work correctly without login", null); + } + } + return site.search(newValue); + } + + @Override + protected void failed() { + LOG.error("Search failed", getException()); + } + + @Override + protected void succeeded() { + Platform.runLater(() -> { + List models = getValue(); + LOG.debug("Search result {} {}", isCancelled(), models); + if(models.isEmpty()) { + popover.hide(); + } else { + popoverTreelist.getItems().clear(); + for (Model model : getValue()) { + popoverTreelist.getItems().add(model); + } + popover.show(); + } + }); + } + }; + new Thread(searchTask).start(); + }; + } + private void updateThumbSize() { int width = Config.getInstance().getSettings().thumbWidth; thumbWidth.getSelectionModel().select(Integer.valueOf(width)); @@ -375,18 +470,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { event.put("amount", tokens); CamrecApplication.bus.post(event); } catch (Exception e1) { - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Error"); - alert.setHeaderText("Couldn't send tip"); - alert.setContentText("An error occured while sending tip: " + e1.getLocalizedMessage()); - alert.showAndWait(); + showError("Couldn't send tip", "An error occured while sending tip:", e1); } } else { - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Error"); - alert.setHeaderText("Couldn't send tip"); - alert.setContentText("You entered an invalid amount of tokens"); - alert.showAndWait(); + showError("Couldn't send tip", "You entered an invalid amount of tokens", null); } } }); @@ -700,6 +787,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { public void setRecorder(Recorder recorder) { this.recorder = recorder; + popoverTreelist.setRecorder(recorder); } @Override @@ -731,4 +819,24 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { selectedThumbCells.get(0).setSelected(false); } } + + private void showError(String header, String text, Exception e) { + Runnable r = () -> { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText(header); + String content = text; + if(e != null) { + content += " " + e.getLocalizedMessage(); + } + alert.setContentText(content); + alert.showAndWait(); + }; + + if(Platform.isFxApplicationThread()) { + r.run(); + } else { + Platform.runLater(r); + } + } } diff --git a/client/src/main/java/ctbrec/ui/autofilltextbox/AutoFillTextField.java b/client/src/main/java/ctbrec/ui/controls/AutoFillTextField.java similarity index 98% rename from client/src/main/java/ctbrec/ui/autofilltextbox/AutoFillTextField.java rename to client/src/main/java/ctbrec/ui/controls/AutoFillTextField.java index bf986360..ca772778 100644 --- a/client/src/main/java/ctbrec/ui/autofilltextbox/AutoFillTextField.java +++ b/client/src/main/java/ctbrec/ui/controls/AutoFillTextField.java @@ -1,4 +1,4 @@ -package ctbrec.ui.autofilltextbox; +package ctbrec.ui.controls; import javafx.collections.ObservableList; diff --git a/client/src/main/java/ctbrec/ui/controls/Popover.css b/client/src/main/java/ctbrec/ui/controls/Popover.css new file mode 100644 index 00000000..7fc3e632 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/Popover.css @@ -0,0 +1,74 @@ +.popover { + -fx-padding: 43 7 7 7; +} +.popover-frame { + -fx-border-image-source: url("/popover-empty.png"); + -fx-border-image-slice: 78 50 60 120 fill; + -fx-border-image-width: 78 50 60 120; + -fx-border-image-insets: -32 -37 -47 -37; +} +.popover.right-tooth .popover-frame { + -fx-border-image-slice: 78 120 60 50 fill; + -fx-border-image-width: 78 120 60 50; +} +.popover-title { + /*-fx-font-family: "Bree serif"; */ + -fx-font-family: "Source Sans Pro Light"; + -fx-font-size: 20px; + /* -fx-text-fill: white; + -fx-font-weight: bold; */ +} +.popover .button { + -fx-font-family: "Source Sans Pro"; + -fx-font-size: 12px; +} + +.popover-tree-list-cell { + -fx-background-color: white; + /* -fx-border-color: transparent transparent #dfdfdf transparent; */ + -fx-padding: 0 30 0 12; + /*-fx-font-family: "Bree Serif"; */ + -fx-font-size: 15px; + /* -fx-font-weight: bold; */ + -fx-text-fill: #363636; +} +#PopoverBackground { + -fx-background-color: white; +} +.search-result-cell { + -fx-background-color: white; + -fx-padding: 4 30 4 45; +} +.search-result-cell:selected { + /* -fx-background-color: white, #eeeeee; */ + -fx-background-insets: 0, 0 0 0 40; +} +.search-result-cell .title { + /*-fx-font-family: "Bree Serif"; */ + -fx-font-size: 15px; + /* -fx-font-weight: bold; */ + -fx-text-fill: #363636; +} +.search-result-cell .details { + -fx-font-size: 13px; + -fx-text-fill: #444444; +} +.search-icon-pane .label { + -fx-font-family: "Source Sans Pro Semibold"; + -fx-font-size: 16px; + -fx-background-color: #515151; + -fx-background-radius: 3px; + -fx-text-fill: white; + -fx-alignment: center; +} +.sample-tree-list-cell { + -fx-background-color: white; + -fx-border-color: transparent transparent #dfdfdf transparent; + -fx-padding: 0 30 0 20; + -fx-font-size: 15px; + -fx-text-fill: #363636; + -fx-graphic-text-gap: 20px; +} +#PopoverBackground { + -fx-background-color: white; +} \ No newline at end of file diff --git a/client/src/main/java/ctbrec/ui/controls/Popover.java b/client/src/main/java/ctbrec/ui/controls/Popover.java new file mode 100644 index 00000000..a6bec1f2 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/Popover.java @@ -0,0 +1,480 @@ +/* + * Copyright (c) 2008, 2014, Oracle and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package ctbrec.ui.controls; + +import java.util.LinkedList; + +import javafx.animation.Animation; +import javafx.animation.FadeTransition; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.ParallelTransition; +import javafx.animation.ScaleTransition; +import javafx.animation.Timeline; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.event.ActionEvent; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.geometry.Point2D; +import javafx.geometry.VPos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Text; +import javafx.util.Duration; + +/** + * A Popover is a mini-window that pops up and contains some application specific content. + * It's width is defined by the application, but defaults to a hard-coded pref width. + * The height will always be between a minimum height (determined by the application, but + * pre-set with a minimum value) and a maximum height (specified by the application, or + * based on the height of the scene). The value for the pref height is determined by + * inspecting the pref height of the current displayed page. At time this value is animated + * (when switching from page to page). + */ +public class Popover extends Region implements EventHandler{ + private static final int PAGE_GAP = 15; + + /** + * The visual frame of the popover is defined as an addition region, rather than simply styling + * the popover itself as one might expect. The reason for this is that our frame is styled via + * a border image, and it has an inner shadow associated with it, and we want to be able to ensure + * that the shadow is on top of whatever content is drawn within the popover. In addition, the inner + * edge of the frame is rounded, and we want the content to slide under it, only to be clipped beneath + * the frame. So it works best for the frame to be overlaid on top, even though it is not intuitive. + */ + private final Region frameBorder = new Region(); + private final Button leftButton = new Button("Left"); + private final Button rightButton = new Button("Right"); + private final LinkedList pages = new LinkedList(); + private final Pane pagesPane = new Pane(); + private final Rectangle pagesClipRect = new Rectangle(); + private final Pane titlesPane = new Pane(); + private Text title; // the current title + private final Rectangle titlesClipRect = new Rectangle(); + // private final EventHandler popoverScrollHandler; + private final EventHandler popoverHideHandler; + private Runnable onHideCallback = null; + private int maxPopupHeight = -1; + + private DoubleProperty popoverHeight = new SimpleDoubleProperty(400) { + @Override protected void invalidated() { + requestLayout(); + } + }; + + public Popover() { + // TODO Could pagesPane be a region instead? I need to draw some opaque background. Right now when + // TODO animating from one page to another you can see the background "shine through" because the + // TODO group background is transparent. That can't be good for performance either. + getStyleClass().setAll("popover"); + frameBorder.getStyleClass().setAll("popover-frame"); + frameBorder.setMouseTransparent(true); + // setup buttons + leftButton.setOnMouseClicked(this); + leftButton.getStyleClass().add("popover-left-button"); + leftButton.setMinWidth(USE_PREF_SIZE); + rightButton.setOnMouseClicked(this); + rightButton.getStyleClass().add("popover-right-button"); + rightButton.setMinWidth(USE_PREF_SIZE); + pagesClipRect.setSmooth(false); + pagesPane.setClip(pagesClipRect); + titlesClipRect.setSmooth(false); + titlesPane.setClip(titlesClipRect); + getChildren().addAll(pagesPane, frameBorder, titlesPane, leftButton, rightButton); + // always hide to start with + setVisible(false); + setOpacity(0); + setScaleX(.8); + setScaleY(.8); + // create handlers for auto hiding + popoverHideHandler = (MouseEvent t) -> { + // check if event is outside popup + Point2D mouseInFilterPane = sceneToLocal(t.getX(), t.getY()); + if (mouseInFilterPane.getX() < 0 || mouseInFilterPane.getX() > (getWidth()) || + mouseInFilterPane.getY() < 0 || mouseInFilterPane.getY() > (getHeight())) { + hide(); + t.consume(); + } + }; + // popoverScrollHandler = new EventHandler() { + // @Override public void handle(ScrollEvent t) { + // t.consume(); // consume all scroll events + // } + // }; + } + + /** + * Handle mouse clicks on the left and right buttons. + */ + @Override public void handle(Event event) { + if (event.getSource() == leftButton) { + pages.getFirst().handleLeftButton(); + } else if (event.getSource() == rightButton) { + pages.getFirst().handleRightButton(); + } + } + + @Override protected double computeMinWidth(double height) { + Page page = pages.isEmpty() ? null : pages.getFirst(); + if (page != null) { + Node n = page.getPageNode(); + if (n != null) { + Insets insets = getInsets(); + return insets.getLeft() + n.minWidth(-1) + insets.getRight(); + } + } + return 200; + } + + @Override protected double computeMinHeight(double width) { + Insets insets = getInsets(); + return insets.getLeft() + 100 + insets.getRight(); + } + + @Override protected double computePrefWidth(double height) { + Page page = pages.isEmpty() ? null : pages.getFirst(); + if (page != null) { + Node n = page.getPageNode(); + if (n != null) { + Insets insets = getInsets(); + return insets.getLeft() + n.prefWidth(-1) + insets.getRight(); + } + } + return 400; + } + + @Override protected double computePrefHeight(double width) { + double minHeight = minHeight(-1); + double maxHeight = maxHeight(-1); + double prefHeight = popoverHeight.get(); + if (prefHeight == -1) { + Page page = pages.getFirst(); + if (page != null) { + Insets inset = getInsets(); + if (width == -1) { + width = prefWidth(-1); + } + double contentWidth = width - inset.getLeft() - inset.getRight(); + double contentHeight = page.getPageNode().prefHeight(contentWidth); + prefHeight = inset.getTop() + contentHeight + inset.getBottom(); + popoverHeight.set(prefHeight); + } else { + prefHeight = minHeight; + } + } + return boundedSize(minHeight, prefHeight, maxHeight); + } + + static double boundedSize(double min, double pref, double max) { + double a = pref >= min ? pref : min; + double b = min >= max ? min : max; + return a <= b ? a : b; + } + + @Override protected double computeMaxWidth(double height) { + return Double.MAX_VALUE; + } + + @Override protected double computeMaxHeight(double width) { + Scene scene = getScene(); + if (scene != null) { + return scene.getHeight() - 100; + } else { + return Double.MAX_VALUE; + } + } + + @Override protected void layoutChildren() { + if (maxPopupHeight == -1) { + maxPopupHeight = (int)getScene().getHeight()-100; + } + final Insets insets = getInsets(); + final int width = (int)getWidth(); + final int height = (int)getHeight(); + final int top = (int)insets.getTop(); + final int right = (int)insets.getRight(); + final int bottom = (int)insets.getBottom(); + final int left = (int)insets.getLeft(); + + int pageWidth = width - left - right; + int pageHeight = height - top - bottom; + + frameBorder.resize(width, height); + + pagesPane.resizeRelocate(left, top, pageWidth, pageHeight); + pagesClipRect.setWidth(pageWidth); + pagesClipRect.setHeight(pageHeight); + + int pageX = 0; + for (Node page : pagesPane.getChildren()) { + page.resizeRelocate(pageX, 0, pageWidth, pageHeight); + pageX += pageWidth + PAGE_GAP; + } + + int buttonHeight = (int)(leftButton.prefHeight(-1)); + if (buttonHeight < 30) buttonHeight = 30; + final int buttonTop = (int)((top-buttonHeight)/2d); + final int leftButtonWidth = (int)snapSizeX(leftButton.prefWidth(-1)); + leftButton.resizeRelocate(left, buttonTop,leftButtonWidth,buttonHeight); + final int rightButtonWidth = (int)snapSizeX(rightButton.prefWidth(-1)); + rightButton.resizeRelocate(width-right-rightButtonWidth, buttonTop,rightButtonWidth,buttonHeight); + + final double leftButtonRight = leftButton.isVisible() ? (left + leftButtonWidth) : left; + final double rightButtonLeft = rightButton.isVisible() ? (right + rightButtonWidth) : right; + titlesClipRect.setX(leftButtonRight); + titlesClipRect.setWidth(pageWidth - leftButtonRight - rightButtonLeft); + titlesClipRect.setHeight(top); + + if (title != null) { + title.setTranslateY((int) (top / 2d)); + } + } + + public final void clearPages() { + while (!pages.isEmpty()) { + pages.pop().handleHidden(); + } + pagesPane.getChildren().clear(); + titlesPane.getChildren().clear(); + pagesClipRect.setX(0); + pagesClipRect.setWidth(400); + pagesClipRect.setHeight(400); + popoverHeight.set(400); + pagesPane.setTranslateX(0); + titlesPane.setTranslateX(0); + titlesClipRect.setTranslateX(0); + } + + public final void popPage() { + Page oldPage = pages.pop(); + oldPage.handleHidden(); + oldPage.setPopover(null); + Page page = pages.getFirst(); + leftButton.setVisible(page.leftButtonText() != null); + leftButton.setText(page.leftButtonText()); + rightButton.setVisible(page.rightButtonText() != null); + rightButton.setText(page.rightButtonText()); + if (pages.size() > 0) { + final Insets insets = getInsets(); + final int width = (int)prefWidth(-1); + final int right = (int)insets.getRight(); + final int left = (int)insets.getLeft(); + int pageWidth = width - left - right; + final int newPageX = (pageWidth+PAGE_GAP) * (pages.size()-1); + new Timeline( + new KeyFrame(Duration.millis(350), (ActionEvent t) -> { + pagesPane.setCache(false); + pagesPane.getChildren().remove(pagesPane.getChildren().size()-1); + titlesPane.getChildren().remove(titlesPane.getChildren().size()-1); + resizePopoverToNewPage(pages.getFirst().getPageNode()); + }, + new KeyValue(pagesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), + new KeyValue(titlesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), + new KeyValue(pagesClipRect.xProperty(), newPageX, Interpolator.EASE_BOTH), + new KeyValue(titlesClipRect.translateXProperty(), newPageX, Interpolator.EASE_BOTH) + ) + ).play(); + } else { + hide(); + } + } + + public final void pushPage(final Page page) { + final Node pageNode = page.getPageNode(); + pageNode.setManaged(false); + pagesPane.getChildren().add(pageNode); + final Insets insets = getInsets(); + final int pageWidth = (int)(prefWidth(-1) - insets.getLeft() - insets.getRight()); + final int newPageX = (pageWidth + PAGE_GAP) * pages.size(); + leftButton.setVisible(page.leftButtonText() != null); + leftButton.setText(page.leftButtonText()); + rightButton.setVisible(page.rightButtonText() != null); + rightButton.setText(page.rightButtonText()); + + title = new Text(page.getPageTitle()); + title.getStyleClass().add("popover-title"); + //debtest title.setFill(Color.WHITE); + title.setTextOrigin(VPos.CENTER); + title.setTranslateX(newPageX + (int) ((pageWidth - title.getLayoutBounds().getWidth()) / 2d)); + titlesPane.getChildren().add(title); + + if (!pages.isEmpty() && isVisible()) { + final Timeline timeline = new Timeline( + new KeyFrame(Duration.millis(350), (ActionEvent t) -> { + pagesPane.setCache(false); + resizePopoverToNewPage(pageNode); + }, + new KeyValue(pagesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), + new KeyValue(titlesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), + new KeyValue(pagesClipRect.xProperty(), newPageX, Interpolator.EASE_BOTH), + new KeyValue(titlesClipRect.translateXProperty(), newPageX, Interpolator.EASE_BOTH) + ) + ); + timeline.play(); + } + page.setPopover(this); + page.handleShown(); + pages.push(page); + } + + private void resizePopoverToNewPage(final Node newPageNode) { + final Insets insets = getInsets(); + final double width = prefWidth(-1); + final double contentWidth = width - insets.getLeft() - insets.getRight(); + double h = newPageNode.prefHeight(contentWidth); + h += insets.getTop() + insets.getBottom(); + new Timeline( + new KeyFrame(Duration.millis(200), + new KeyValue(popoverHeight, h, Interpolator.EASE_BOTH) + ) + ).play(); + } + + public void show(){ + show(null); + } + + private Animation fadeAnimation = null; + + public void show(Runnable onHideCallback){ + if (!isVisible() || fadeAnimation != null) { + this.onHideCallback = onHideCallback; + getScene().addEventFilter(MouseEvent.MOUSE_CLICKED, popoverHideHandler); + // getScene().addEventFilter(ScrollEvent.ANY,popoverScrollHandler); + + if (fadeAnimation != null) { + fadeAnimation.stop(); + setVisible(true); // for good measure + } else { + popoverHeight.set(-1); + setVisible(true); + } + + FadeTransition fade = new FadeTransition(Duration.seconds(.1), this); + fade.setToValue(1.0); + fade.setOnFinished((ActionEvent event) -> { + fadeAnimation = null; + }); + + ScaleTransition scale = new ScaleTransition(Duration.seconds(.1), this); + scale.setToX(1); + scale.setToY(1); + + ParallelTransition tx = new ParallelTransition(fade, scale); + fadeAnimation = tx; + tx.play(); + } + } + + public void hide(){ + if (isVisible() || fadeAnimation != null) { + getScene().removeEventFilter(MouseEvent.MOUSE_CLICKED, popoverHideHandler); + // getScene().removeEventFilter(ScrollEvent.ANY,popoverScrollHandler); + + if (fadeAnimation != null) { + fadeAnimation.stop(); + } + + FadeTransition fade = new FadeTransition(Duration.seconds(.1), this); + fade.setToValue(0); + fade.setOnFinished((ActionEvent event) -> { + fadeAnimation = null; + setVisible(false); + //clearPages(); + if (onHideCallback != null) onHideCallback.run(); + }); + + ScaleTransition scale = new ScaleTransition(Duration.seconds(.1), this); + scale.setToX(.8); + scale.setToY(.8); + + ParallelTransition tx = new ParallelTransition(fade, scale); + fadeAnimation = tx; + tx.play(); + } + } + + /** + * Represents a page in a popover. + */ + public static interface Page { + public void setPopover(Popover popover); + public Popover getPopover(); + + /** + * Get the node that represents the page. + * + * @return the page node. + */ + public Node getPageNode(); + + /** + * Get the title to display for this page. + * + * @return The page title + */ + public String getPageTitle(); + + /** + * The text for left button, if null then button will be hidden. + * @return The button text + */ + public String leftButtonText(); + + /** + * Called on a click of the left button of the popover. + */ + public void handleLeftButton(); + + /** + * The text for right button, if null then button will be hidden. + * @return The button text + */ + public String rightButtonText(); + + /** + * Called on a click of the right button of the popover. + */ + public void handleRightButton(); + + public void handleShown(); + public void handleHidden(); + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/PopoverTreeList.java b/client/src/main/java/ctbrec/ui/controls/PopoverTreeList.java new file mode 100644 index 00000000..01f6aac1 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/PopoverTreeList.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2008, 2014, Oracle and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package ctbrec.ui.controls; + +import javafx.event.EventHandler; +import javafx.geometry.Bounds; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; +import javafx.util.Callback; + +/** + * Special ListView designed to look like "Text... >" tree list. Perhaps we ought to have customized + * a TreeView instead of a ListView (as the TreeView already has the data model all defined). + * + * This implementation minimizes classes by just having the PopoverTreeList implementing everything + * (it is the Control, the Skin, and the CellFactory all in one). + */ +public class PopoverTreeList extends ListView implements Callback, ListCell> { + protected static final Image RIGHT_ARROW = new Image( + PopoverTreeList.class.getResource("/popover-arrow.png").toExternalForm()); + + public PopoverTreeList(){ + getStyleClass().clear(); + setCellFactory(this); + } + + @Override public ListCell call(ListView p) { + return new TreeItemListCell(); + } + + protected void itemClicked(T item) {} + + private class TreeItemListCell extends ListCell implements EventHandler { + private ImageView arrow = new ImageView(RIGHT_ARROW); + + private TreeItemListCell() { + super(); + getStyleClass().setAll("popover-tree-list-cell"); + setOnMouseClicked(this); + } + + @Override public void handle(MouseEvent t) { + itemClicked(getItem()); + } + + @Override protected double computePrefWidth(double height) { + return 100; + } + + @Override protected double computePrefHeight(double width) { + return 44; + } + + @Override protected void layoutChildren() { + if (getChildren().size() < 2) getChildren().add(arrow); + super.layoutChildren(); + final int w = (int)getWidth(); + final int h = (int)getHeight(); + //final int centerX = (int)(w/2d); + //final int centerY = (int)(h/2d); + final Bounds arrowBounds = arrow.getLayoutBounds(); + arrow.setLayoutX(w - arrowBounds.getWidth() - 12); + arrow.setLayoutY((int)((h - arrowBounds.getHeight())/2d)); + } + + // CELL METHODS + @Override protected void updateItem(T item, boolean empty) { + // let super do its work + super.updateItem(item,empty); + // update our state + if (item == null) { // empty item + setText(null); + arrow.setVisible(false); + } else { + setText(item.toString()); + arrow.setVisible(true); + } + } + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/SearchBox.css b/client/src/main/java/ctbrec/ui/controls/SearchBox.css new file mode 100644 index 00000000..1ec1ebd5 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/SearchBox.css @@ -0,0 +1,34 @@ +.search-box-icon { + -fx-shape: "M10.728,9.893c0.889-1.081,1.375-2.435,1.375-3.842C12.103,2.714,9.388,0,6.051,0C2.715,0,0,2.714,0,6.051c0,3.338,2.715,6.052,6.051,6.052c0.954,0,1.898-0.227,2.744-0.656l3.479,3.478l1.743-1.742L10.728,9.893z M6.051,2.484c1.966,0,3.566,1.602,3.566,3.566c0,1.968-1.6,3.567-3.566,3.567c-1.967,0-3.566-1.6-3.566-3.567C2.485,4.086,4.084,2.484,6.051,2.484z"; + -fx-scale-shape: false; + -fx-background-color: #aaaaaa; +} +.search-box { + /*-fx-font-size: 16px;*/ + /*-fx-text-fill: #363636;*/ + /*-fx-background-radius: 15, 14;*/ + -fx-padding: 0 0 0 30; +} +.search-box:focused { + /*-fx-background-radius: 15,14,16,14;*/ +} +.search-clear-button { + -fx-shape: "M9.521,0.083c-5.212,0-9.438,4.244-9.438,9.479c0,5.234,4.225,9.479,9.438,9.479c5.212,0,9.437-4.244,9.437-9.479C18.958,4.327,14.733,0.083,9.521,0.083z M13.91,13.981c-0.367,0.369-0.963,0.369-1.329,0l-3.019-3.03l-3.019,3.03c-0.367,0.369-0.962,0.369-1.329,0c-0.367-0.368-0.366-0.965,0.001-1.334l3.018-3.031L5.216,6.585C4.849,6.217,4.849,5.618,5.217,5.25c0.366-0.369,0.961-0.368,1.328,0l3.018,3.031l3.019-3.031c0.366-0.368,0.961-0.369,1.328,0c0.366,0.368,0.366,0.967,0,1.335l-3.019,3.031l3.02,3.031C14.276,13.017,14.276,13.613,13.91,13.981z"; + -fx-scale-shape: false; + -fx-background-color: #aaaaaa; + -fx-padding: 9.5px; +} + +.search-tree-list-cell { + -fx-background-color: white; + -fx-border-color: transparent transparent #dfdfdf transparent; + -fx-padding: 0 30 0 20; + -fx-font-size: 15px; + -fx-text-fill: #363636; + -fx-graphic-text-gap: 20px; +} + +.highlight { + -fx-background-color: #0096c9; + -fx-text-fill: white; +} diff --git a/client/src/main/java/ctbrec/ui/controls/SearchBox.java b/client/src/main/java/ctbrec/ui/controls/SearchBox.java new file mode 100644 index 00000000..be893acd --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/SearchBox.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2008, 2014, Oracle and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package ctbrec.ui.controls; + +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.scene.Cursor; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Region; + +/** + * Search field with styling and a clear button + */ +public class SearchBox extends TextField implements ChangeListener{ + private final Button clearButton = new Button(); + private final Region innerBackground = new Region(); + private final Region icon = new Region(); + private final int prefHeight = 26; + + public SearchBox() { + getStyleClass().addAll("search-box"); + icon.getStyleClass().setAll("search-box-icon"); + innerBackground.getStyleClass().setAll("search-box-inner"); + setPromptText("Search"); + textProperty().addListener(this); + setPrefHeight(prefHeight); + clearButton.getStyleClass().setAll("search-clear-button"); + clearButton.setCursor(Cursor.DEFAULT); + clearButton.setOnMouseClicked((MouseEvent t) -> { + setText(""); + requestFocus(); + }); + clearButton.setVisible(false); + clearButton.setManaged(false); + innerBackground.setManaged(false); + icon.setManaged(false); + } + + public SearchBox(boolean icon) { + this(); + this.icon.setVisible(false); + this.icon.getStyleClass().remove("search-box-icon"); + this.setStyle("-fx-padding: 5"); + } + + @Override protected void layoutChildren() { + super.layoutChildren(); + if (clearButton.getParent() != this) getChildren().add(clearButton); + if (innerBackground.getParent() != this) getChildren().add(0,innerBackground); + if (icon.getParent() != this) getChildren().add(icon); + innerBackground.setLayoutX(0); + innerBackground.setLayoutY(0); + innerBackground.resize(getWidth(), getHeight()); + icon.setLayoutX(0); + icon.setLayoutY(0); + icon.resize(35,prefHeight); + clearButton.setLayoutX(getWidth() - prefHeight); + clearButton.setLayoutY(0); + clearButton.resize(prefHeight, prefHeight); + } + + @Override public void changed(ObservableValue ov, String oldValue, String newValue) { + clearButton.setVisible(newValue.length() > 0); + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/SearchPopover.java b/client/src/main/java/ctbrec/ui/controls/SearchPopover.java new file mode 100644 index 00000000..42222952 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/SearchPopover.java @@ -0,0 +1,9 @@ +package ctbrec.ui.controls; + +public class SearchPopover extends Popover { + + + public SearchPopover() { + getStyleClass().add("right-tooth"); + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java new file mode 100644 index 00000000..722cca72 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2008, 2014, Oracle and/or its affiliates. + * All rights reserved. Use is subject to license terms. + * + * This file is available and licensed under the following license: + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the distribution. + * - Neither the name of Oracle Corporation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package ctbrec.ui.controls; + +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Model; +import ctbrec.recorder.Recorder; +import ctbrec.ui.Player; +import javafx.application.Platform; +import javafx.concurrent.Task; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.Skin; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; + +/** + * Popover page that displays a list of samples and sample categories for a given SampleCategory. + */ +public class SearchPopoverTreeList extends PopoverTreeList implements Popover.Page { + private static final transient Logger LOG = LoggerFactory.getLogger(SearchPopoverTreeList.class); + + private Popover popover; + + private Recorder recorder; + + public SearchPopoverTreeList() { + + } + + @Override + public ListCell call(ListView p) { + return new SearchItemListCell(); + } + + @Override + protected void itemClicked(Model model) { + if(model == null) { + return; + } + + setCursor(Cursor.WAIT); + new Thread(() -> { + Platform.runLater(() -> { + boolean started = Player.play(model); + if(started) { + Toast.makeText(getScene(), "Starting Player", 2000, 500, 500); + } + setCursor(Cursor.DEFAULT); + }); + }).start(); + } + + @Override + public void setPopover(Popover popover) { + this.popover = popover; + } + + @Override + public Popover getPopover() { + return popover; + } + + @Override + public Node getPageNode() { + return this; + } + + @Override + public String getPageTitle() { + return "Search Results"; + } + + @Override + public String leftButtonText() { + return null; + } + + @Override + public void handleLeftButton() { + } + + @Override + public String rightButtonText() { + return "Done"; + } + + @Override + public void handleRightButton() { + popover.hide(); + } + + @Override + public void handleShown() { + } + + @Override + public void handleHidden() { + } + + private class SearchItemListCell extends ListCell implements Skin, EventHandler { + + private Label title = new Label(); + private Button follow; + private Button record; + private Model model; + private ImageView thumb = new ImageView(); + private int thumbSize = 64; + private Node tallest = thumb; + + private SearchItemListCell() { + super(); + setSkin(this); + getStyleClass().setAll("search-tree-list-cell"); + setOnMouseClicked(this); + setOnMouseEntered(evt -> { + getStyleClass().add("highlight"); + title.getStyleClass().add("highlight"); + }); + setOnMouseExited(evt -> { + getStyleClass().remove("highlight"); + title.getStyleClass().remove("highlight"); + }); + thumb.setFitWidth(thumbSize); + thumb.setFitHeight(thumbSize); + + follow = new Button("Follow"); + follow.setOnAction((evt) -> { + setCursor(Cursor.WAIT); + new Thread(new Task() { + @Override + protected Boolean call() throws Exception { + return model.follow(); + } + + @Override + protected void done() { + try { + get(); + } catch (Exception e) { + LOG.warn("Search failed: {}", e.getMessage()); + } + Platform.runLater(() -> setCursor(Cursor.DEFAULT)); + } + }).start(); + }); + record = new Button("Record"); + record.setOnAction((evt) -> { + setCursor(Cursor.WAIT); + new Thread(new Task() { + @Override + protected Void call() throws Exception { + recorder.startRecording(model); + return null; + } + + @Override + protected void done() { + Platform.runLater(() -> setCursor(Cursor.DEFAULT)); + } + }).start(); + }); + getChildren().addAll(thumb, title, follow, record); + + record.visibleProperty().bind(title.visibleProperty()); + thumb.visibleProperty().bind(title.visibleProperty()); + } + + @Override + public void handle(MouseEvent t) { + itemClicked(getItem()); + } + + @Override + protected void updateItem(Model model, boolean empty) { + super.updateItem(model, empty); + if (empty) { + follow.setVisible(false); + title.setVisible(false); + this.model = null; + } else { + follow.setVisible(model.getSite().supportsFollow()); + title.setVisible(true); + title.setText(model.getName()); + this.model = model; + String previewUrl = Optional.ofNullable(model.getPreview()).orElse(getClass().getResource("/anonymous.png").toString()); + Image img = new Image(previewUrl, true); + thumb.setImage(img); + } + + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + final Insets insets = getInsets(); + final double left = insets.getLeft(); + final double top = insets.getTop(); + final double w = getWidth() - left - insets.getRight(); + final double h = getHeight() - top - insets.getBottom(); + + thumb.setLayoutX(left); + thumb.setLayoutY((h - thumbSize) / 2); + + final double titleHeight = title.prefHeight(w); + title.setLayoutX(left + thumbSize + 10); + title.setLayoutY((h - titleHeight) / 2); + title.resize(w, titleHeight); + + int buttonW = 50; + int buttonH = 24; + follow.setStyle("-fx-font-size: 10px;"); + follow.setLayoutX(w - buttonW - 20); + follow.setLayoutY((h - buttonH) / 2); + follow.resize(buttonW, buttonH); + + record.setStyle("-fx-font-size: 10px;"); + record.setLayoutX(w - 10); + record.setLayoutY((h - buttonH) / 2); + record.resize(buttonW, buttonH); + } + + @Override + protected double computeMinWidth(double height) { + final Insets insets = getInsets(); + final double h = height = insets.getBottom() - insets.getTop(); + return (int) ((insets.getLeft() + tallest.minWidth(h) + tallest.minWidth(h) + insets.getRight()) + 0.5d); + } + + @Override + protected double computePrefWidth(double height) { + final Insets insets = getInsets(); + final double h = height = insets.getBottom() - insets.getTop(); + return (int) ((insets.getLeft() + tallest.prefWidth(h) + tallest.prefWidth(h) + insets.getRight()) + 0.5d); + } + + @Override + protected double computeMaxWidth(double height) { + final Insets insets = getInsets(); + final double h = height = insets.getBottom() - insets.getTop(); + return (int) ((insets.getLeft() + tallest.maxWidth(h) + tallest.maxWidth(h) + insets.getRight()) + 0.5d); + } + + @Override + protected double computeMinHeight(double width) { + return thumbSize; + } + + @Override + protected double computePrefHeight(double width) { + return thumbSize + 20; + } + + @Override + protected double computeMaxHeight(double width) { + return thumbSize + 20; + } + + @Override + public SearchItemListCell getSkinnable() { + return this; + } + + @Override + public Node getNode() { + return null; + } + + @Override + public void dispose() { + } + } + + public void setRecorder(Recorder recorder) { + this.recorder = recorder; + } +} \ No newline at end of file diff --git a/client/src/main/resources/anonymous.png b/client/src/main/resources/anonymous.png new file mode 100644 index 0000000000000000000000000000000000000000..b0294b265e6abe826ab441fd4c037d92a4564ded GIT binary patch literal 3344 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
    UZ zx`8Hiz^%+kFST4WX3<2mVxHZZMO6&J=tN4bH#gFDO=Z_-BO2pnqd6`rQy1=Hw*U3O+?b!XejO?GE?b+IvBH{7VGaptTJI+te7wxiX9 zNkKOQF$jX_LP${ILr*2yJtUGuA@ZSuE>Jymd8w$VGwNCo+F_XUpTqxs|My?c;hO4- z`Ps{}F$|j@t>j~9pX1oGXQBUIjCZzWFr0vOdJ?8Z6JSAEZvc2y6&pbeh;r-h0Z@iv zSw%`b0TaR|R?<~cbbLr#H4qxZ%F1m+l$rs=8$hF?1&EI~`UzZ-14O-7a0>$y`K%2&zfXRtT)nin;u-mP z=wa21eJ!=#s-dlCkM@{(1tX8UF0_u$J{h?D1YTLTFu%Zm{L+piZ%^fa?`+??M#(+% zg?qYl`>S`&ayOSd*nfNfuT?#vYeyDTzs>IYR@^@{oYnBC@7%${$aQAV%^CBs_$}h% Sk7O<7yzywbn!g&_b>I)V%5zBo literal 0 HcmV?d00001 diff --git a/client/src/main/resources/popover-arrow@2x.png b/client/src/main/resources/popover-arrow@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..bf4d0d177420e3c05635938ab52098c351a7bcc9 GIT binary patch literal 1112 zcmaJ=T}TvB6dqWT%9S8NN+qsSL5aIFv+K^fgPW{7ySdhgtQ$#0HO^dh)cI-V>W)&; z&qF_KP%n{`;X^M`LqKQRT4+Ta)5cMDuSwvLyQqdiCwTHIL%>6lh-*?VE=iHNx z4O$&EaJ&iZ!Dqy%nWJ6A}?wZ-F48ifu3nMY(tX zB&;R~*9IlkjGBe*oTRH>(ZP6a)xd0msIIXMQR;*Uw7@n+3zMJkj*>u;!{p9@Knq3` zwkvggCT!|!NJ)L2Qb;Ciwt#Az!vZQqBCypi&Eo7ZIj76vyuf-igulkEr@ z97UzmX>Z!^)y+1F357zAhR^502#?jPA<_0|*7~dh4=u@545a89a1_NBy&Hu|9O?WO zRAW(Avlh~X6GqvhK`~z1xl$G=2>*ww>LS`gNw^g6KZUJSuK}qfwDfLM!i8&H@3=C! zs0l@+n<-uI%4V^#T}Qgrt{WiQ#DYzNC@Gp_*gVG&1TLXjNYo^l;KL-=@G6STMdI;j zD8~DuEQ3dki9~3BELh72tD?2BU_6-R^19TmLJeiP@)Fmdlj}5tYGBViG?gAG$4y-Y zbC>3n+_BI(^%l5t?pTxp)L9emA7{JRxcIQx`cT9DZsNwK>PnB{NAW1Z zM^g6d%g-MdjMMJCcT+8yb(5rfth}^phHxLAs3@%(2O~WMOKct5c(uNyrs7v}*S5Xn z?T+Kg`qkaz!*r9c=*0JBU&n@-SNS`p>t~-G+ws2Q%=G>8Zxc^WUs~ZR{#bVEOJ@B1 zfsQhF(F@I8{A2f;YZs?7_a6MLe7VP^+*lU zD6ju)(RA&(GbLvmmy4S+G|G_fHzjYo3duH9cz*4bn}xHd4vy4393@=D^TE0c`N64S P=dCAV4gB@meMkQQ>>+R< literal 0 HcmV?d00001 diff --git a/client/src/main/resources/popover-empty.png b/client/src/main/resources/popover-empty.png new file mode 100644 index 0000000000000000000000000000000000000000..c5808ec13a32322453573e04f7ff09279e39f19f GIT binary patch literal 4624 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 ZfzG=*137FSLqNa-Ro7K3SFwHk{{SL7K=1$n literal 0 HcmV?d00001 diff --git a/common/src/main/java/ctbrec/sites/AbstractSite.java b/common/src/main/java/ctbrec/sites/AbstractSite.java index 1d50d186..96d67005 100644 --- a/common/src/main/java/ctbrec/sites/AbstractSite.java +++ b/common/src/main/java/ctbrec/sites/AbstractSite.java @@ -1,5 +1,10 @@ package ctbrec.sites; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import ctbrec.Model; import ctbrec.recorder.Recorder; public abstract class AbstractSite implements Site { @@ -26,4 +31,19 @@ public abstract class AbstractSite implements Site { public Recorder getRecorder() { return recorder; } + + @Override + public boolean supportsSearch() { + return false; + } + + @Override + public List search(String q) throws IOException, InterruptedException { + return Collections.emptyList(); + } + + @Override + public boolean searchRequiresLogin() { + return false; + } } diff --git a/common/src/main/java/ctbrec/sites/Site.java b/common/src/main/java/ctbrec/sites/Site.java index 08fef0f4..cf6f3119 100644 --- a/common/src/main/java/ctbrec/sites/Site.java +++ b/common/src/main/java/ctbrec/sites/Site.java @@ -1,6 +1,7 @@ package ctbrec.sites; import java.io.IOException; +import java.util.List; import ctbrec.Model; import ctbrec.io.HttpClient; @@ -21,8 +22,11 @@ public interface Site { public void shutdown(); public boolean supportsTips(); public boolean supportsFollow(); + public boolean supportsSearch(); public boolean isSiteForModel(Model m); public boolean credentialsAvailable(); public void setEnabled(boolean enabled); public boolean isEnabled(); + public List search(String q) throws IOException, InterruptedException; + public boolean searchRequiresLogin(); } diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java index f763bccc..6b2670d8 100644 --- a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java +++ b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java @@ -1,8 +1,15 @@ package ctbrec.sites.bonga; import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.json.JSONArray; import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.Model; @@ -16,6 +23,8 @@ import okhttp3.Response; public class BongaCams extends AbstractSite { + private static final transient Logger LOG = LoggerFactory.getLogger(BongaCams.class); + public static final String BASE_URL = "https://bongacams.com"; private BongaCamsHttpClient httpClient; @@ -116,6 +125,54 @@ public class BongaCams extends AbstractSite { return false; } + @Override + public boolean supportsSearch() { + return true; + } + + @Override + public boolean searchRequiresLogin() { + return true; + } + + @Override + public List search(String q) throws IOException, InterruptedException { + String url = BASE_URL + "/tools/listing_v3.php?offset=0&model_search[display_name][text]=" + URLEncoder.encode(q, "utf-8"); + Request req = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", BongaCams.BASE_URL) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + try(Response response = getHttpClient().execute(req)) { + if(response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + if(json.optString("status").equals("success")) { + List models = new ArrayList<>(); + JSONArray results = json.getJSONArray("models"); + for (int i = 0; i < results.length(); i++) { + JSONObject result = results.getJSONObject(i); + Model model = createModel(result.getString("username")); + String thumb = result.getString("thumb_image"); + if(thumb != null) { + model.setPreview("https:" + thumb); + } + models.add(model); + } + return models; + } else { + LOG.warn("Search result: " + json.toString(2)); + return Collections.emptyList(); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + @Override public boolean isSiteForModel(Model m) { return m instanceof BongaCamsModel; diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4.java b/common/src/main/java/ctbrec/sites/cam4/Cam4.java index d63ef050..04b032f4 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4.java @@ -1,16 +1,25 @@ package ctbrec.sites.cam4; import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONObject; import ctbrec.Config; import ctbrec.Model; +import ctbrec.StringUtil; import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; import ctbrec.sites.AbstractSite; +import okhttp3.Request; +import okhttp3.Response; 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; @@ -84,6 +93,57 @@ public class Cam4 extends AbstractSite { return true; } + @Override + public boolean supportsSearch() { + return true; + } + + @Override + public boolean searchRequiresLogin() { + return true; + } + + @Override + public List search(String q) throws IOException, InterruptedException { + List result = new ArrayList<>(); + search(q, false, result); + search(q, true, result); + return result; + } + + private void search(String q, boolean offline, List models) throws IOException { + String url = BASE_URI + "/usernameSearch?username=" + URLEncoder.encode(q, "utf-8"); + if(offline) { + url += "&offline=true"; + } + Request req = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .build(); + try(Response response = getHttpClient().execute(req)) { + if(response.isSuccessful()) { + String body = response.body().string(); + JSONArray results = new JSONArray(body); + for (int i = 0; i < results.length(); i++) { + JSONObject result = results.getJSONObject(i); + Model model = createModel(result.getString("username")); + String thumb = null; + if(result.has("thumbnailId")) { + thumb = "https://snapshots.xcdnpro.com/thumbnails/" + model.getName() + "?s=" + result.getString("thumbnailId"); + } else { + thumb = result.getString("profileImageLink"); + } + if(StringUtil.isNotBlank(thumb)) { + model.setPreview(thumb); + } + models.add(model); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + @Override public boolean isSiteForModel(Model m) { return m instanceof Cam4Model; diff --git a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java index c8750bc5..e79688fa 100644 --- a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java +++ b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java @@ -1,8 +1,15 @@ package ctbrec.sites.camsoda; import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.json.JSONArray; import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.Model; @@ -14,6 +21,7 @@ import okhttp3.Response; public class Camsoda extends AbstractSite { + private static final transient Logger LOG = LoggerFactory.getLogger(Camsoda.class); public static final String BASE_URI = "https://www.camsoda.com"; private HttpClient httpClient; @@ -105,6 +113,44 @@ public class Camsoda extends AbstractSite { return true; } + @Override + public boolean supportsSearch() { + return true; + } + + @Override + public List search(String q) throws IOException, InterruptedException { + String url = BASE_URI + "/api/v1/browse/autocomplete?s=" + URLEncoder.encode(q, "utf-8"); + Request req = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .build(); + try(Response response = getHttpClient().execute(req)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.optBoolean("status")) { + List models = new ArrayList<>(); + JSONArray results = json.getJSONArray("results"); + for (int i = 0; i < results.length(); i++) { + JSONObject result = results.getJSONObject(i); + CamsodaModel model = (CamsodaModel) createModel(result.getString("username")); + String thumb = result.getString("thumb"); + if(thumb != null) { + model.setPreview("https:" + thumb); + } + models.add(model); + } + return models; + } else { + LOG.warn("Search result: " + json.toString(2)); + return Collections.emptyList(); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + @Override public boolean isSiteForModel(Model m) { return m instanceof CamsodaModel; diff --git a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java index 105be013..36125f85 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java @@ -3,6 +3,10 @@ package ctbrec.sites.chaturbate; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; +import java.net.URLEncoder; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -68,6 +72,7 @@ public class Chaturbate extends AbstractSite { ChaturbateModel m = new ChaturbateModel(this); m.setName(name); m.setUrl(getBaseUrl() + '/' + name + '/'); + m.setPreview("https://roomimg.stream.highwebmedia.com/ri/" + name + ".jpg?" + Instant.now().getEpochSecond()); return m; } @@ -124,6 +129,44 @@ public class Chaturbate extends AbstractSite { return true; } + @Override + public boolean supportsSearch() { + return true; + } + + @Override + public List search(String q) throws IOException, InterruptedException { + String url = BASE_URI + "?keywords=" + URLEncoder.encode(q, "utf-8"); + List result = new ArrayList<>(); + + // search online models + Request req = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .build(); + try(Response resp = getHttpClient().execute(req)) { + if(resp.isSuccessful()) { + result.addAll(ChaturbateModelParser.parseModels(this, resp.body().string())); + } + } + + // since chaturbate does not return offline models, we at least try, if the profile page + // exists for the search string + url = BASE_URI + '/' + q; + req = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .build(); + try(Response resp = getHttpClient().execute(req)) { + if(resp.isSuccessful()) { + Model model = createModel(q); + result.add(model); + } + } + + return result; + } + @Override public boolean isSiteForModel(Model m) { return m instanceof ChaturbateModel; diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java index bb0d4c13..146c834a 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java @@ -1,6 +1,7 @@ package ctbrec.sites.mfc; import java.io.IOException; +import java.util.List; import org.jsoup.select.Elements; @@ -97,6 +98,16 @@ public class MyFreeCams extends AbstractSite { return true; } + @Override + public boolean supportsSearch() { + return true; + } + + @Override + public List search(String q) throws IOException, InterruptedException { + return client.search(q); + } + @Override public boolean isSiteForModel(Model m) { return m instanceof MyFreeCamsModel; diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index 78b9e864..04c168c4 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -7,7 +7,9 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ExecutorService; @@ -15,6 +17,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; import org.json.JSONArray; import org.json.JSONObject; @@ -60,6 +63,8 @@ public class MyFreeCamsClient { private int sessionId; private long heartBeat; private volatile boolean connecting = false; + private static int messageId = 31415; // starting with 31415 just for fun + private Map> responseHandlers = new HashMap<>(); private EvictingQueue receivedTextHistory = EvictingQueue.create(100); @@ -193,6 +198,7 @@ public class MyFreeCamsClient { case LOGIN: LOG.debug("LOGIN: {}", message); sessionId = message.getReceiver(); + LOG.debug("Session ID {}", sessionId); break; case DETAILS: case ROOMHELPER: @@ -201,7 +207,6 @@ public class MyFreeCamsClient { case CMESG: case PMESG: case TXPROFILE: - case USERNAMELOOKUP: case MYCAMSTATE: case MYWEBCAM: case JOINCHAN: @@ -217,6 +222,18 @@ public class MyFreeCamsClient { } } break; + case USERNAMELOOKUP: + // LOG.debug("{}", message.getType()); + // LOG.debug("{}", message.getSender()); + // LOG.debug("{}", message.getReceiver()); + // LOG.debug("{}", message.getArg1()); + // LOG.debug("{}", message.getArg2()); + // LOG.debug("{}", message.getMessage()); + Consumer responseHandler = responseHandlers.remove(message.getArg1()); + if(responseHandler != null) { + responseHandler.accept(message); + } + break; case TAGS: JSONObject json = new JSONObject(message.getMessage()); String[] names = JSONObject.getNames(json); @@ -571,4 +588,34 @@ public class MyFreeCamsClient { public ServerConfig getServerConfig() { return serverConfig; } + + public List search(String q) throws InterruptedException { + LOG.debug("Sending USERNAMELOOKUP for {}", q); + int msgId = messageId++; + Object monitor = new Object(); + List result = new ArrayList<>(); + responseHandlers.put(msgId, msg -> { + LOG.debug("Search result: " + msg); + if(StringUtil.isNotBlank(msg.getMessage()) && !Objects.equals(msg.getMessage(), q)) { + JSONObject json = new JSONObject(msg.getMessage()); + String name = json.getString("nm"); + MyFreeCamsModel model = mfc.createModel(name); + model.setUid(json.getInt("uid")); + model.setState(State.of(json.getInt("vs"))); + String uid = Integer.toString(model.getUid()); + String uidStart = uid.substring(0, 3); + String previewUrl = "https://img.mfcimg.com/photos2/"+uidStart+'/'+uid+"/avatar.90x90.jpg"; + model.setPreview(previewUrl); + result.add(model); + } + synchronized (monitor) { + monitor.notify(); + } + }); + ws.send("10 " + sessionId + " 0 " + msgId + " 0 " + q + "\n"); + synchronized (monitor) { + monitor.wait(); + } + return result; + } } From 4c7cabaa261ffb7037a2bcc2498b12b6a0b45616 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 23 Nov 2018 20:34:06 +0100 Subject: [PATCH 009/231] Show a toast message when the player is started The toast is supposed to put across, that it might take a second for the player to show up --- client/src/main/java/ctbrec/ui/Player.java | 14 +++-- .../java/ctbrec/ui/RecordedModelsTab.java | 10 +++- .../main/java/ctbrec/ui/RecordingsTab.java | 11 +++- client/src/main/java/ctbrec/ui/ThumbCell.java | 10 +++- .../main/java/ctbrec/ui/controls/Toast.java | 53 +++++++++++++++++++ 5 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/controls/Toast.java diff --git a/client/src/main/java/ctbrec/ui/Player.java b/client/src/main/java/ctbrec/ui/Player.java index 593e379b..f9c0d9fd 100644 --- a/client/src/main/java/ctbrec/ui/Player.java +++ b/client/src/main/java/ctbrec/ui/Player.java @@ -23,7 +23,7 @@ public class Player { private static final transient Logger LOG = LoggerFactory.getLogger(Player.class); private static PlayerThread playerThread; - public static void play(String url) { + public static boolean play(String url) { boolean singlePlayer = Config.getInstance().getSettings().singlePlayer; try { if (singlePlayer && playerThread != null && playerThread.isRunning()) { @@ -31,12 +31,14 @@ public class Player { } playerThread = new PlayerThread(url); + return true; } catch (Exception e1) { LOG.error("Couldn't start player", e1); + return false; } } - public static void play(Recording rec) { + public static boolean play(Recording rec) { boolean singlePlayer = Config.getInstance().getSettings().singlePlayer; try { if (singlePlayer && playerThread != null && playerThread.isRunning()) { @@ -44,12 +46,14 @@ public class Player { } playerThread = new PlayerThread(rec); + return true; } catch (Exception e1) { LOG.error("Couldn't start player", e1); + return false; } } - public static void play(Model model) { + public static boolean play(Model model) { try { if(model.isOnline(true)) { boolean singlePlayer = Config.getInstance().getSettings().singlePlayer; @@ -60,7 +64,7 @@ public class Player { Collections.sort(sources); StreamSource best = sources.get(sources.size()-1); LOG.debug("Playing {}", best.getMediaPlaylistUrl()); - Player.play(best.getMediaPlaylistUrl()); + return Player.play(best.getMediaPlaylistUrl()); } else { Platform.runLater(() -> { Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION); @@ -68,6 +72,7 @@ public class Player { alert.setHeaderText("Room is currently not public"); alert.showAndWait(); }); + return false; } } catch (Exception e1) { LOG.error("Couldn't get stream information for model {}", model, e1); @@ -78,6 +83,7 @@ public class Player { alert.setContentText(e1.getLocalizedMessage()); alert.showAndWait(); }); + return false; } } diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index 5874f8c2..e633a97b 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -25,6 +25,7 @@ import ctbrec.Recording; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; import ctbrec.ui.autofilltextbox.AutoFillTextField; +import ctbrec.ui.controls.Toast; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -328,8 +329,13 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { private void openInPlayer(JavaFxModel selectedModel) { table.setCursor(Cursor.WAIT); new Thread(() -> { - Player.play(selectedModel); - Platform.runLater(() -> table.setCursor(Cursor.DEFAULT)); + boolean started = Player.play(selectedModel); + Platform.runLater(() -> { + if(started) { + Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500); + } + table.setCursor(Cursor.DEFAULT); + }); }).start(); } diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index b1f3d47b..5605dbee 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -33,6 +33,7 @@ import ctbrec.Recording.STATUS; import ctbrec.recorder.Recorder; import ctbrec.recorder.download.MergedHlsDownload; import ctbrec.sites.Site; +import ctbrec.ui.controls.Toast; import javafx.application.Platform; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; @@ -483,7 +484,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener { new Thread() { @Override public void run() { - Player.play(recording); + boolean started = Player.play(recording); + if(started) { + Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500)); + } } }.start(); } else { @@ -492,7 +496,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener { new Thread() { @Override public void run() { - Player.play(url); + boolean started = Player.play(url); + if(started) { + Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500)); + } } }.start(); } diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index 8b563344..6d5a03f5 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -15,6 +15,7 @@ import com.iheartradio.m3u8.ParseException; import ctbrec.Config; import ctbrec.Model; import ctbrec.recorder.Recorder; +import ctbrec.ui.controls.Toast; import javafx.animation.FadeTransition; import javafx.animation.FillTransition; import javafx.animation.ParallelTransition; @@ -302,8 +303,13 @@ public class ThumbCell extends StackPane { void startPlayer() { setCursor(Cursor.WAIT); new Thread(() -> { - Player.play(model); - Platform.runLater(() -> setCursor(Cursor.DEFAULT)); + boolean started = Player.play(model); + Platform.runLater(() -> { + setCursor(Cursor.DEFAULT); + if (started) { + Toast.makeText(getScene(), "Starting Player", 2000, 500, 500); + } + }); }).start(); } diff --git a/client/src/main/java/ctbrec/ui/controls/Toast.java b/client/src/main/java/ctbrec/ui/controls/Toast.java new file mode 100644 index 00000000..bde087ce --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/Toast.java @@ -0,0 +1,53 @@ +package ctbrec.ui.controls; + +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.scene.Scene; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.scene.text.Text; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.util.Duration; + +public final class Toast { + public static void makeText(Scene owner, String toastMsg, int toastDelay, int fadeInDelay, int fadeOutDelay) { + Stage toastStage = new Stage(); + toastStage.initOwner(owner.getWindow()); + toastStage.setResizable(false); + toastStage.initStyle(StageStyle.TRANSPARENT); + + Text text = new Text(toastMsg); + text.setFont(Font.font(30)); + text.setFill(Color.WHITE); + + StackPane root = new StackPane(text); + root.setStyle("-fx-background-radius: 20; -fx-background-color: rgba(0, 0, 0, 0.4); -fx-padding: 50px;"); + root.setOpacity(0); + + Scene scene = new Scene(root); + scene.setFill(Color.TRANSPARENT); + toastStage.setScene(scene); + toastStage.show(); + + Timeline fadeInTimeline = new Timeline(); + KeyFrame fadeInKey1 = new KeyFrame(Duration.millis(fadeInDelay), new KeyValue(toastStage.getScene().getRoot().opacityProperty(), 1)); + fadeInTimeline.getKeyFrames().add(fadeInKey1); + fadeInTimeline.setOnFinished((ae) -> { + new Thread(() -> { + try { + Thread.sleep(toastDelay); + } catch (InterruptedException e) { + } + Timeline fadeOutTimeline = new Timeline(); + KeyFrame fadeOutKey1 = new KeyFrame(Duration.millis(fadeOutDelay), new KeyValue(toastStage.getScene().getRoot().opacityProperty(), 0)); + fadeOutTimeline.getKeyFrames().add(fadeOutKey1); + fadeOutTimeline.setOnFinished((aeb) -> toastStage.close()); + fadeOutTimeline.play(); + }).start(); + }); + fadeInTimeline.play(); + } +} \ No newline at end of file From 2121f56804d0ee43336f73d4783e71dbe006f9a0 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 23 Nov 2018 20:47:56 +0100 Subject: [PATCH 010/231] Make Toast a little bit more opaque --- client/src/main/java/ctbrec/ui/controls/Toast.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/controls/Toast.java b/client/src/main/java/ctbrec/ui/controls/Toast.java index bde087ce..53b26a75 100644 --- a/client/src/main/java/ctbrec/ui/controls/Toast.java +++ b/client/src/main/java/ctbrec/ui/controls/Toast.java @@ -24,7 +24,7 @@ public final class Toast { text.setFill(Color.WHITE); StackPane root = new StackPane(text); - root.setStyle("-fx-background-radius: 20; -fx-background-color: rgba(0, 0, 0, 0.4); -fx-padding: 50px;"); + root.setStyle("-fx-background-radius: 20; -fx-background-color: rgba(0, 0, 0, 0.8); -fx-padding: 50px;"); root.setOpacity(0); Scene scene = new Scene(root); From 3445fa5ca0c826861884ff7402f42109b1ef8135 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 23 Nov 2018 21:15:04 +0100 Subject: [PATCH 011/231] Revert change, which prevents ChaturbateModel to work correctly --- .../sites/chaturbate/ChaturbateModel.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java index 4a97dbe1..3095c4be 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java @@ -26,7 +26,6 @@ import okhttp3.Response; public class ChaturbateModel extends AbstractModel { private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateModel.class); - private Chaturbate chaturbate; /** * This constructor exists only for deserialization. Please don't call it directly @@ -36,31 +35,30 @@ public class ChaturbateModel extends AbstractModel { ChaturbateModel(Chaturbate site) { this.site = site; - this.chaturbate = site; } @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { StreamInfo info; if(ignoreCache) { - info = chaturbate.loadStreamInfo(getName()); + info = getChaturbate().loadStreamInfo(getName()); LOG.trace("Model {} room status: {}", getName(), info.room_status); } else { - info = chaturbate.getStreamInfo(getName()); + info = getChaturbate().getStreamInfo(getName()); } return Objects.equals("public", info.room_status); } @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { - int[] resolution = chaturbate.streamResolutionCache.getIfPresent(getName()); + int[] resolution = getChaturbate().streamResolutionCache.getIfPresent(getName()); if(resolution != null) { - return chaturbate.getResolution(getName()); + return getChaturbate().getResolution(getName()); } else { if(failFast) { return new int[2]; } else { - return chaturbate.getResolution(getName()); + return getChaturbate().getResolution(getName()); } } } @@ -71,8 +69,8 @@ public class ChaturbateModel extends AbstractModel { */ @Override public void invalidateCacheEntries() { - chaturbate.streamInfoCache.invalidate(getName()); - chaturbate.streamResolutionCache.invalidate(getName()); + getChaturbate().streamInfoCache.invalidate(getName()); + getChaturbate().streamResolutionCache.invalidate(getName()); } public String getOnlineState() throws IOException, ExecutionException { @@ -81,20 +79,20 @@ public class ChaturbateModel extends AbstractModel { @Override public String getOnlineState(boolean failFast) throws IOException, ExecutionException { - StreamInfo info = chaturbate.streamInfoCache.getIfPresent(getName()); + StreamInfo info = getChaturbate().streamInfoCache.getIfPresent(getName()); return info != null ? info.room_status : "n/a"; } public StreamInfo getStreamInfo() throws IOException, ExecutionException { - return chaturbate.getStreamInfo(getName()); + return getChaturbate().getStreamInfo(getName()); } public MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException, ExecutionException { - return chaturbate.getMasterPlaylist(getName()); + return getChaturbate().getMasterPlaylist(getName()); } @Override public void receiveTip(int tokens) throws IOException { - chaturbate.sendTip(getName(), tokens); + getChaturbate().sendTip(getName(), tokens); } @Override @@ -167,4 +165,8 @@ public class ChaturbateModel extends AbstractModel { throw new IOException("HTTP status " + resp.code() + " " + resp.message()); } } + + private Chaturbate getChaturbate() { + return (Chaturbate) site; + } } From 85fee70e604b25a2b66113ab2503f0b4d9c2d8d2 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 23 Nov 2018 21:15:30 +0100 Subject: [PATCH 012/231] Use TimeUnit for wait-calls --- common/src/main/java/ctbrec/recorder/LocalRecorder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 381a276a..245a5a75 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; @@ -423,7 +424,7 @@ public class LocalRecorder implements Recorder { try { if (running) - Thread.sleep(10000); + Thread.sleep(TimeUnit.SECONDS.toMillis(60)); } catch (InterruptedException e) { LOG.trace("Sleep interrupted"); } From edb11a0efc74e4aabca51d72749adf1c1d386987 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 24 Nov 2018 15:16:51 +0100 Subject: [PATCH 013/231] Fix possible NPE in update --- common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index f4e0f046..f0776843 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -219,9 +219,10 @@ public class MyFreeCamsModel extends AbstractModel { public void update(SessionState state, String streamUrl) { uid = Integer.parseInt(state.getUid().toString()); setName(state.getNm()); - setCamScore(state.getM().getCamscore()); setState(State.of(state.getVs())); setStreamUrl(streamUrl); + Optional camScore = Optional.of(state.getM()).map(m -> m.getCamscore()); + setCamScore(camScore.orElse(0.0)); // preview String uid = state.getUid().toString(); From 72064eb55b4908b2a66972d93c758e7f8e722adc Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 24 Nov 2018 15:20:45 +0100 Subject: [PATCH 014/231] Fix possible NPE in getStreamUrl --- common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index 04c168c4..c07c5afe 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -538,8 +538,8 @@ public class MyFreeCamsClient { } public String getStreamUrl(SessionState state) { - Integer camserv = state.getU().getCamserv(); - if(camserv != null) { + Integer camserv = Optional.ofNullable(state.getU()).map(u -> u.getCamserv()).orElse(-1); + if(camserv != null && camserv != -1) { int userChannel = 100000000 + state.getUid(); String streamUrl = ""; String phase = state.getU().getPhase() != null ? state.getU().getPhase() : "z"; From 6fa9de4a32abd00fbeb340d27e78cdc295a4c3aa Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 24 Nov 2018 15:33:48 +0100 Subject: [PATCH 015/231] Ensure the correct model is updated by checking the uid The uid should be a value > 0 --- common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index c07c5afe..1eeecf86 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -527,7 +527,7 @@ public class MyFreeCamsClient { for (SessionState state : sessionStates.asMap().values()) { String nm = Optional.ofNullable(state.getNm()).orElse(""); String name = Optional.ofNullable(model.getName()).orElse(""); - if(Objects.equals(nm.toLowerCase(), name.toLowerCase()) || Objects.equals(model.getUid(), state.getUid())) { + if(Objects.equals(nm.toLowerCase(), name.toLowerCase()) || Objects.equals(model.getUid(), state.getUid()) && state.getUid() > 0) { model.update(state, getStreamUrl(state)); return; } From 76efd16c96c110ce92218835d912dd903c20eb9c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 24 Nov 2018 18:45:42 +0100 Subject: [PATCH 016/231] Change logfile name to server.log in server/logback.xml --- server/src/main/resources/logback.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml index a2555eb9..4285a070 100644 --- a/server/src/main/resources/logback.xml +++ b/server/src/main/resources/logback.xml @@ -11,7 +11,7 @@ - ctbrec.log + server.log false DEBUG @@ -32,7 +32,6 @@ - From 88cfa9b8a7d5bd2835151989215176e7dd73646a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 24 Nov 2018 18:47:44 +0100 Subject: [PATCH 017/231] Add log files to .gitignore --- server/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/.gitignore b/server/.gitignore index fc247909..2d132e83 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -2,7 +2,7 @@ /target/ *~ *.bak -/ctbrec.log +*.log /ctbrec-tunnel.sh /jre/ /server-local.sh From 69fe63ba99f3598144d7bdb19cc1157274c69b2c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 24 Nov 2018 18:48:23 +0100 Subject: [PATCH 018/231] Remove background color Use the themes background color --- client/src/main/java/ctbrec/ui/DonateTabFx.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/DonateTabFx.java b/client/src/main/java/ctbrec/ui/DonateTabFx.java index f00ea63a..311fa515 100644 --- a/client/src/main/java/ctbrec/ui/DonateTabFx.java +++ b/client/src/main/java/ctbrec/ui/DonateTabFx.java @@ -9,13 +9,9 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.Tab; import javafx.scene.image.ImageView; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.BorderPane; -import javafx.scene.layout.CornerRadii; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.scene.text.TextAlignment; @@ -26,7 +22,6 @@ public class DonateTabFx extends Tab { setText("Donate"); BorderPane container = new BorderPane(); container.setPadding(new Insets(10)); - container.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, new Insets(0)))); setContent(container); VBox headerVbox = new VBox(10); @@ -53,21 +48,21 @@ public class DonateTabFx extends Tab { tokenDesc.setTextAlignment(TextAlignment.CENTER); tokenBox.getChildren().addAll(tokenImage, tokenButton, tokenDesc); - ImageView coffeeImage = new ImageView(getClass().getResource("/html/buymeacoffee-fancy.png").toString()); + ImageView coffeeImage = new ImageView(getClass().getResource("/buymeacoffee-round.png").toString()); Button coffeeButton = new Button("Buy me a coffee"); coffeeButton.setOnMouseClicked((e) -> { DesktopIntegration.open("https://www.buymeacoffee.com/0xboobface"); }); VBox buyCoffeeBox = new VBox(5); buyCoffeeBox.setAlignment(Pos.TOP_CENTER); buyCoffeeBox.getChildren().addAll(coffeeImage, coffeeButton); - ImageView paypalImage = new ImageView(getClass().getResource("/html/pp196.png").toString()); + ImageView paypalImage = new ImageView(getClass().getResource("/paypal-round.png").toString()); Button paypalButton = new Button("PayPal"); paypalButton.setOnMouseClicked((e) -> { DesktopIntegration.open("https://www.paypal.me/0xb00bface"); }); VBox paypalBox = new VBox(5); paypalBox.setAlignment(Pos.TOP_CENTER); paypalBox.getChildren().addAll(paypalImage, paypalButton); - ImageView patreonImage = new ImageView(getClass().getResource("/html/patreon-logo.png").toString()); + ImageView patreonImage = new ImageView(getClass().getResource("/patreon-round.png").toString()); Button patreonButton = new Button("Become a Patron"); patreonButton.setOnMouseClicked((e) -> { DesktopIntegration.open("https://www.patreon.com/0xb00bface"); }); VBox patreonBox = new VBox(5); From 146e327830b550b50ae345f7c754a53edf035979 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 15:52:41 +0100 Subject: [PATCH 019/231] Add CSS dark theme for the application --- client/src/main/resources/dark.css | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 client/src/main/resources/dark.css diff --git a/client/src/main/resources/dark.css b/client/src/main/resources/dark.css new file mode 100644 index 00000000..a7af0985 --- /dev/null +++ b/client/src/main/resources/dark.css @@ -0,0 +1,7 @@ +.root { + -fx-base: #4d4d4d; + -fx-accent: #0096c9; + -fx-default-button: -fx-accent; + -fx-focus-color: -fx-accent; + -fx-control-inner-background-alt: derive(-fx-base, 95%); +} \ No newline at end of file From cf14d8c3fe231362372b69882cbfd4e2313e88a2 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 15:54:25 +0100 Subject: [PATCH 020/231] Add round images for the donate tab --- client/src/main/resources/buymeacoffee-round.png | Bin 0 -> 9832 bytes client/src/main/resources/patreon-round.png | Bin 0 -> 7770 bytes client/src/main/resources/paypal-round.png | Bin 0 -> 8842 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 client/src/main/resources/buymeacoffee-round.png create mode 100644 client/src/main/resources/patreon-round.png create mode 100644 client/src/main/resources/paypal-round.png diff --git a/client/src/main/resources/buymeacoffee-round.png b/client/src/main/resources/buymeacoffee-round.png new file mode 100644 index 0000000000000000000000000000000000000000..341a0f8e2087c75286d2199b84a0b23bda646fa6 GIT binary patch literal 9832 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 literal 0 HcmV?d00001 diff --git a/client/src/main/resources/patreon-round.png b/client/src/main/resources/patreon-round.png new file mode 100644 index 0000000000000000000000000000000000000000..22c9edac95c448290334b180bdfd0b93bb42fbfd GIT binary patch literal 7770 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 nPu!SJNw6tN?-;V##Useh^vijpB;|k@8 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 literal 0 HcmV?d00001 diff --git a/client/src/main/resources/paypal-round.png b/client/src/main/resources/paypal-round.png new file mode 100644 index 0000000000000000000000000000000000000000..481490eafcbc5bfbe5050400126c777ea7c8a45c GIT binary patch literal 8842 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 Date: Sun, 25 Nov 2018 15:55:16 +0100 Subject: [PATCH 021/231] Use SVG paths instead of images for the popover background --- .../main/java/ctbrec/ui/controls/Popover.css | 82 ++++--------------- .../main/java/ctbrec/ui/controls/Popover.java | 44 ++++------ 2 files changed, 36 insertions(+), 90 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/controls/Popover.css b/client/src/main/java/ctbrec/ui/controls/Popover.css index 7fc3e632..bb0886ec 100644 --- a/client/src/main/java/ctbrec/ui/controls/Popover.css +++ b/client/src/main/java/ctbrec/ui/controls/Popover.css @@ -1,74 +1,28 @@ .popover { -fx-padding: 43 7 7 7; } + .popover-frame { - -fx-border-image-source: url("/popover-empty.png"); - -fx-border-image-slice: 78 50 60 120 fill; - -fx-border-image-width: 78 50 60 120; - -fx-border-image-insets: -32 -37 -47 -37; -} -.popover.right-tooth .popover-frame { - -fx-border-image-slice: 78 120 60 50 fill; - -fx-border-image-width: 78 120 60 50; -} -.popover-title { - /*-fx-font-family: "Bree serif"; */ - -fx-font-family: "Source Sans Pro Light"; - -fx-font-size: 20px; - /* -fx-text-fill: white; - -fx-font-weight: bold; */ -} -.popover .button { - -fx-font-family: "Source Sans Pro"; - -fx-font-size: 12px; + -fx-background-color: -fx-outer-border, -fx-inner-border, -fx-base; + -fx-background-insets: 0 0 -1 0, 0, 1, 2; + -fx-background-radius: 10px, 10px, 10px, 10px; + -fx-padding: 1; + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.8), 20, 0, 0, 0); } -.popover-tree-list-cell { - -fx-background-color: white; - /* -fx-border-color: transparent transparent #dfdfdf transparent; */ - -fx-padding: 0 30 0 12; - /*-fx-font-family: "Bree Serif"; */ - -fx-font-size: 15px; - /* -fx-font-weight: bold; */ - -fx-text-fill: #363636; +.popover.left-tooth .popover-frame { + -fx-shape: "m 33.34215,51.52967 4.782653,4.746482 4.333068,4.299995 h 94.637639 c 1.108,0 1.99987,0.891879 1.99987,1.999877 V 164.22046 c 0,1.10801 -0.89187,1.99988 -1.99987,1.99988 H 12.205971 c -1.107998,0 -2.000392,-0.89187 -2.000392,-1.99988 V 62.576024 c 0,-1.107998 0.892394,-1.999877 2.000392,-1.999877 h 12.020455 l 4.333071,-4.299995 z"; } -#PopoverBackground { - -fx-background-color: white; + +.popover.right-tooth .popover-frame { + -fx-shape: "M 438.26953 194.75781 L 420.19336 212.69727 L 403.81641 228.94922 L 46.130859 228.94922 C 41.943143 228.94922 38.572266 232.3201 38.572266 236.50781 L 38.572266 620.67578 C 38.572266 624.8635 41.943143 628.23438 46.130859 628.23438 L 518.1543 628.23438 C 522.34201 628.23438 525.71484 624.8635 525.71484 620.67578 L 525.71484 236.50781 C 525.71484 232.3201 522.34201 228.94922 518.1543 228.94922 L 472.72266 228.94922 L 456.3457 212.69727 L 438.26953 194.75781 z"; } -.search-result-cell { - -fx-background-color: white; - -fx-padding: 4 30 4 45; + +.popover-title { + -fx-font-size: 20px; + -fx-text-fill: -fx-text-background-color; } -.search-result-cell:selected { - /* -fx-background-color: white, #eeeeee; */ - -fx-background-insets: 0, 0 0 0 40; -} -.search-result-cell .title { - /*-fx-font-family: "Bree Serif"; */ - -fx-font-size: 15px; - /* -fx-font-weight: bold; */ - -fx-text-fill: #363636; -} -.search-result-cell .details { - -fx-font-size: 13px; - -fx-text-fill: #444444; -} -.search-icon-pane .label { - -fx-font-family: "Source Sans Pro Semibold"; - -fx-font-size: 16px; - -fx-background-color: #515151; - -fx-background-radius: 3px; - -fx-text-fill: white; - -fx-alignment: center; -} -.sample-tree-list-cell { - -fx-background-color: white; - -fx-border-color: transparent transparent #dfdfdf transparent; - -fx-padding: 0 30 0 20; - -fx-font-size: 15px; - -fx-text-fill: #363636; - -fx-graphic-text-gap: 20px; -} -#PopoverBackground { - -fx-background-color: white; + +.popover .button { + -fx-font-size: 12px; } \ No newline at end of file diff --git a/client/src/main/java/ctbrec/ui/controls/Popover.java b/client/src/main/java/ctbrec/ui/controls/Popover.java index a6bec1f2..4b16a172 100644 --- a/client/src/main/java/ctbrec/ui/controls/Popover.java +++ b/client/src/main/java/ctbrec/ui/controls/Popover.java @@ -48,15 +48,15 @@ import javafx.event.Event; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Point2D; -import javafx.geometry.VPos; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Button; +import javafx.scene.control.Label; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.scene.shape.Rectangle; -import javafx.scene.text.Text; +import javafx.scene.text.TextAlignment; import javafx.util.Duration; /** @@ -86,9 +86,7 @@ public class Popover extends Region implements EventHandler{ private final Pane pagesPane = new Pane(); private final Rectangle pagesClipRect = new Rectangle(); private final Pane titlesPane = new Pane(); - private Text title; // the current title - private final Rectangle titlesClipRect = new Rectangle(); - // private final EventHandler popoverScrollHandler; + private Label title; // the current title private final EventHandler popoverHideHandler; private Runnable onHideCallback = null; private int maxPopupHeight = -1; @@ -114,10 +112,10 @@ public class Popover extends Region implements EventHandler{ rightButton.getStyleClass().add("popover-right-button"); rightButton.setMinWidth(USE_PREF_SIZE); pagesClipRect.setSmooth(false); + pagesClipRect.setArcHeight(10); + pagesClipRect.arcWidthProperty().bind(pagesClipRect.arcHeightProperty()); pagesPane.setClip(pagesClipRect); - titlesClipRect.setSmooth(false); - titlesPane.setClip(titlesClipRect); - getChildren().addAll(pagesPane, frameBorder, titlesPane, leftButton, rightButton); + getChildren().addAll(frameBorder, titlesPane, leftButton, rightButton, pagesPane); // always hide to start with setVisible(false); setOpacity(0); @@ -228,10 +226,11 @@ public class Popover extends Region implements EventHandler{ final Insets insets = getInsets(); final int width = (int)getWidth(); final int height = (int)getHeight(); - final int top = (int)insets.getTop(); + final int top = (int)insets.getTop() + 40; final int right = (int)insets.getRight(); final int bottom = (int)insets.getBottom(); final int left = (int)insets.getLeft(); + final int offset = 18; int pageWidth = width - left - right; int pageHeight = height - top - bottom; @@ -252,18 +251,15 @@ public class Popover extends Region implements EventHandler{ if (buttonHeight < 30) buttonHeight = 30; final int buttonTop = (int)((top-buttonHeight)/2d); final int leftButtonWidth = (int)snapSizeX(leftButton.prefWidth(-1)); - leftButton.resizeRelocate(left, buttonTop,leftButtonWidth,buttonHeight); + leftButton.resizeRelocate(left, buttonTop + offset,leftButtonWidth,buttonHeight); final int rightButtonWidth = (int)snapSizeX(rightButton.prefWidth(-1)); - rightButton.resizeRelocate(width-right-rightButtonWidth, buttonTop,rightButtonWidth,buttonHeight); - - final double leftButtonRight = leftButton.isVisible() ? (left + leftButtonWidth) : left; - final double rightButtonLeft = rightButton.isVisible() ? (right + rightButtonWidth) : right; - titlesClipRect.setX(leftButtonRight); - titlesClipRect.setWidth(pageWidth - leftButtonRight - rightButtonLeft); - titlesClipRect.setHeight(top); + rightButton.resizeRelocate(width-right-rightButtonWidth, buttonTop + offset,rightButtonWidth,buttonHeight); if (title != null) { - title.setTranslateY((int) (top / 2d)); + double tw = title.getWidth(); + double th = title.getHeight(); + title.setTranslateX((width - tw) / 2); + title.setTranslateY((top - th) / 2 + offset); } } @@ -279,7 +275,6 @@ public class Popover extends Region implements EventHandler{ popoverHeight.set(400); pagesPane.setTranslateX(0); titlesPane.setTranslateX(0); - titlesClipRect.setTranslateX(0); } public final void popPage() { @@ -307,8 +302,7 @@ public class Popover extends Region implements EventHandler{ }, new KeyValue(pagesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), new KeyValue(titlesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), - new KeyValue(pagesClipRect.xProperty(), newPageX, Interpolator.EASE_BOTH), - new KeyValue(titlesClipRect.translateXProperty(), newPageX, Interpolator.EASE_BOTH) + new KeyValue(pagesClipRect.xProperty(), newPageX, Interpolator.EASE_BOTH) ) ).play(); } else { @@ -328,10 +322,9 @@ public class Popover extends Region implements EventHandler{ rightButton.setVisible(page.rightButtonText() != null); rightButton.setText(page.rightButtonText()); - title = new Text(page.getPageTitle()); + title = new Label(page.getPageTitle()); title.getStyleClass().add("popover-title"); - //debtest title.setFill(Color.WHITE); - title.setTextOrigin(VPos.CENTER); + title.setTextAlignment(TextAlignment.CENTER); title.setTranslateX(newPageX + (int) ((pageWidth - title.getLayoutBounds().getWidth()) / 2d)); titlesPane.getChildren().add(title); @@ -343,8 +336,7 @@ public class Popover extends Region implements EventHandler{ }, new KeyValue(pagesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), new KeyValue(titlesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), - new KeyValue(pagesClipRect.xProperty(), newPageX, Interpolator.EASE_BOTH), - new KeyValue(titlesClipRect.translateXProperty(), newPageX, Interpolator.EASE_BOTH) + new KeyValue(pagesClipRect.xProperty(), newPageX, Interpolator.EASE_BOTH) ) ); timeline.play(); From d571afaa448f55fa6beefa26bcc466980eae0006 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 15:56:43 +0100 Subject: [PATCH 022/231] Use theme colors * Use theme colors * Improve aspect ratio handling for images. * Display images with rounded corners --- client/src/main/java/ctbrec/ui/ThumbCell.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index 0ffce655..eb316656 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -81,7 +81,7 @@ public class ThumbCell extends StackPane { this.recorder = recorder; recording = recorder.isRecording(model); model.setSuspended(recorder.isSuspended(model)); - this.setStyle("-fx-background-color: lightgray"); + this.setStyle("-fx-background-color: -fx-base"); iv = new ImageView(); iv.setSmooth(true); @@ -530,8 +530,14 @@ public class ThumbCell extends StackPane { } private void setSize(int w, int h) { - iv.setFitWidth(w); - iv.setFitHeight(h); + if(iv.getImage() != null) { + double aspectRatio = iv.getImage().getWidth() / iv.getImage().getHeight(); + if(aspectRatio > 1) { + iv.setFitWidth(w); + } else { + iv.setFitHeight(h); + } + } setMinSize(w, h); setPrefSize(w, h); nameBackground.setWidth(w); @@ -545,5 +551,10 @@ public class ThumbCell extends StackPane { topic.setWrappingWidth(w-margin*2); selectionOverlay.setWidth(w); selectionOverlay.setHeight(getHeight()); + + Rectangle clip = new Rectangle(w, h); + clip.setArcWidth(10); + clip.arcHeightProperty().bind(clip.arcWidthProperty()); + this.setClip(clip); } } From 9cabc21cae2f55f940dfa7fe0b2c5603eb6d877e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 15:57:13 +0100 Subject: [PATCH 023/231] Don't show images in DEV mode --- .../ui/controls/SearchPopoverTreeList.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java index 722cca72..206a3855 100644 --- a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java +++ b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java @@ -31,6 +31,8 @@ */ package ctbrec.ui.controls; +import java.net.URL; +import java.util.Objects; import java.util.Optional; import org.slf4j.Logger; @@ -53,6 +55,7 @@ import javafx.scene.control.Skin; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; +import javafx.scene.shape.Rectangle; /** * Popover page that displays a list of samples and sample categories for a given SampleCategory. @@ -161,8 +164,13 @@ public class SearchPopoverTreeList extends PopoverTreeList implements Pop getStyleClass().remove("highlight"); title.getStyleClass().remove("highlight"); }); + + Rectangle clip = new Rectangle(thumbSize, thumbSize); + clip.setArcWidth(20); + clip.arcHeightProperty().bind(clip.arcWidthProperty()); thumb.setFitWidth(thumbSize); thumb.setFitHeight(thumbSize); + thumb.setClip(clip); follow = new Button("Follow"); follow.setOnAction((evt) -> { @@ -223,9 +231,15 @@ public class SearchPopoverTreeList extends PopoverTreeList implements Pop title.setVisible(true); title.setText(model.getName()); this.model = model; - String previewUrl = Optional.ofNullable(model.getPreview()).orElse(getClass().getResource("/anonymous.png").toString()); - Image img = new Image(previewUrl, true); - thumb.setImage(img); + URL anonymousPng = getClass().getResource("/anonymous.png"); + String previewUrl = Optional.ofNullable(model.getPreview()).orElse(anonymousPng.toString()); + if(!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { + Image img = new Image(previewUrl, true); + thumb.setImage(img); + } else { + Image img = new Image(anonymousPng.toString(), true); + thumb.setImage(img); + } } } From 6282cd76bc99197f6fea80418bee0443eaf7761b Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 15:57:27 +0100 Subject: [PATCH 024/231] Use theme colors --- .../src/main/java/ctbrec/ui/controls/SearchBox.css | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/controls/SearchBox.css b/client/src/main/java/ctbrec/ui/controls/SearchBox.css index 1ec1ebd5..3877c82f 100644 --- a/client/src/main/java/ctbrec/ui/controls/SearchBox.css +++ b/client/src/main/java/ctbrec/ui/controls/SearchBox.css @@ -1,7 +1,7 @@ .search-box-icon { -fx-shape: "M10.728,9.893c0.889-1.081,1.375-2.435,1.375-3.842C12.103,2.714,9.388,0,6.051,0C2.715,0,0,2.714,0,6.051c0,3.338,2.715,6.052,6.051,6.052c0.954,0,1.898-0.227,2.744-0.656l3.479,3.478l1.743-1.742L10.728,9.893z M6.051,2.484c1.966,0,3.566,1.602,3.566,3.566c0,1.968-1.6,3.567-3.566,3.567c-1.967,0-3.566-1.6-3.566-3.567C2.485,4.086,4.084,2.484,6.051,2.484z"; -fx-scale-shape: false; - -fx-background-color: #aaaaaa; + -fx-background-color: -fx-mark-color; } .search-box { /*-fx-font-size: 16px;*/ @@ -15,20 +15,20 @@ .search-clear-button { -fx-shape: "M9.521,0.083c-5.212,0-9.438,4.244-9.438,9.479c0,5.234,4.225,9.479,9.438,9.479c5.212,0,9.437-4.244,9.437-9.479C18.958,4.327,14.733,0.083,9.521,0.083z M13.91,13.981c-0.367,0.369-0.963,0.369-1.329,0l-3.019-3.03l-3.019,3.03c-0.367,0.369-0.962,0.369-1.329,0c-0.367-0.368-0.366-0.965,0.001-1.334l3.018-3.031L5.216,6.585C4.849,6.217,4.849,5.618,5.217,5.25c0.366-0.369,0.961-0.368,1.328,0l3.018,3.031l3.019-3.031c0.366-0.368,0.961-0.369,1.328,0c0.366,0.368,0.366,0.967,0,1.335l-3.019,3.031l3.02,3.031C14.276,13.017,14.276,13.613,13.91,13.981z"; -fx-scale-shape: false; - -fx-background-color: #aaaaaa; + -fx-background-color: -fx-mark-color; -fx-padding: 9.5px; } .search-tree-list-cell { - -fx-background-color: white; - -fx-border-color: transparent transparent #dfdfdf transparent; + -fx-background-color: -fx-background; + -fx-border-color: transparent transparent -fx-base transparent; -fx-padding: 0 30 0 20; -fx-font-size: 15px; - -fx-text-fill: #363636; + -fx-text-fill: -fx-mid-text-color; -fx-graphic-text-gap: 20px; } .highlight { - -fx-background-color: #0096c9; - -fx-text-fill: white; + -fx-background-color: -fx-focus-color; + -fx-text-fill: -fx-light-text-color; } From cab0ac469ba57c1c579d0afdfaad79593048ca21 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 15:58:33 +0100 Subject: [PATCH 025/231] Reposition popover slightly for the new SVG background --- client/src/main/java/ctbrec/ui/ThumbOverviewTab.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index 5fb861e4..618b0bcd 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -160,10 +160,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { popover.setMinWidth(400); popover.maxHeightProperty().bind(popover.minHeightProperty()); popover.prefHeightProperty().bind(popover.minHeightProperty()); - popover.setMinHeight(400); + popover.setMinHeight(450); popover.pushPage(popoverTreelist); StackPane.setAlignment(popover, Pos.TOP_RIGHT); - StackPane.setMargin(popover, new Insets(50, 50, 0, 0)); + StackPane.setMargin(popover, new Insets(35, 50, 0, 0)); HBox topBar = new HBox(5); HBox.setHgrow(filterInput, Priority.ALWAYS); From 5c4d0d5290ed429c2ee58e5da71d5566b47f4033 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 16:14:13 +0100 Subject: [PATCH 026/231] Load stylesheet from config dir if it exists On startup ctbrec looks for style.css in the config directory. If it exists, it is added to JavaFX's list of stylesheets --- client/src/main/java/ctbrec/ui/CamrecApplication.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index d9814e87..51249043 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -1,6 +1,7 @@ package ctbrec.ui; import java.io.BufferedReader; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -122,6 +123,7 @@ public class CamrecApplication extends Application { switchToStartTab(); + loadUserStyleSheet(primaryStage); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/ThumbCell.css"); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/SearchBox.css"); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/Popover.css"); @@ -186,6 +188,13 @@ public class CamrecApplication extends Application { }); } + private void loadUserStyleSheet(Stage primaryStage) { + File userCss = new File(Config.getInstance().getConfigDir(), "style.css"); + if(userCss.exists() && userCss.isFile()) { + primaryStage.getScene().getStylesheets().add(userCss.toURI().toString()); + } + } + private void switchToStartTab() { String startTab = Config.getInstance().getSettings().startTab; if(StringUtil.isNotBlank(startTab)) { From 459734f48e9737fe63138fe2c65134c22b55ef45 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 16:19:10 +0100 Subject: [PATCH 027/231] Remove not needed images --- .../ctbrec/ui/controls/PopoverTreeList.java | 21 ------------------ client/src/main/resources/popover-arrow.png | Bin 1104 -> 0 bytes .../src/main/resources/popover-arrow@2x.png | Bin 1112 -> 0 bytes client/src/main/resources/popover-empty.png | Bin 4624 -> 0 bytes .../src/main/resources/popover-empty@2x.png | Bin 9431 -> 0 bytes 5 files changed, 21 deletions(-) delete mode 100644 client/src/main/resources/popover-arrow.png delete mode 100644 client/src/main/resources/popover-arrow@2x.png delete mode 100644 client/src/main/resources/popover-empty.png delete mode 100644 client/src/main/resources/popover-empty@2x.png diff --git a/client/src/main/java/ctbrec/ui/controls/PopoverTreeList.java b/client/src/main/java/ctbrec/ui/controls/PopoverTreeList.java index 01f6aac1..c914ba51 100644 --- a/client/src/main/java/ctbrec/ui/controls/PopoverTreeList.java +++ b/client/src/main/java/ctbrec/ui/controls/PopoverTreeList.java @@ -32,11 +32,8 @@ package ctbrec.ui.controls; import javafx.event.EventHandler; -import javafx.geometry.Bounds; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.util.Callback; @@ -48,8 +45,6 @@ import javafx.util.Callback; * (it is the Control, the Skin, and the CellFactory all in one). */ public class PopoverTreeList extends ListView implements Callback, ListCell> { - protected static final Image RIGHT_ARROW = new Image( - PopoverTreeList.class.getResource("/popover-arrow.png").toExternalForm()); public PopoverTreeList(){ getStyleClass().clear(); @@ -63,8 +58,6 @@ public class PopoverTreeList extends ListView implements Callback implements EventHandler { - private ImageView arrow = new ImageView(RIGHT_ARROW); - private TreeItemListCell() { super(); getStyleClass().setAll("popover-tree-list-cell"); @@ -83,18 +76,6 @@ public class PopoverTreeList extends ListView implements Callback extends ListView implements Callback`rQy1=Hw*U3O+?b!XejO?GE?b+IvBH{7VGaptTJI+te7wxiX9 zNkKOQF$jX_LP${ILr*2yJtUGuA@ZSuE>Jymd8w$VGwNCo+F_XUpTqxs|My?c;hO4- z`Ps{}F$|j@t>j~9pX1oGXQBUIjCZzWFr0vOdJ?8Z6JSAEZvc2y6&pbeh;r-h0Z@iv zSw%`b0TaR|R?<~cbbLr#H4qxZ%F1m+l$rs=8$hF?1&EI~`UzZ-14O-7a0>$y`K%2&zfXRtT)nin;u-mP z=wa21eJ!=#s-dlCkM@{(1tX8UF0_u$J{h?D1YTLTFu%Zm{L+piZ%^fa?`+??M#(+% zg?qYl`>S`&ayOSd*nfNfuT?#vYeyDTzs>IYR@^@{oYnBC@7%${$aQAV%^CBs_$}h% Sk7O<7yzywbn!g&_b>I)V%5zBo diff --git a/client/src/main/resources/popover-arrow@2x.png b/client/src/main/resources/popover-arrow@2x.png deleted file mode 100644 index bf4d0d177420e3c05635938ab52098c351a7bcc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1112 zcmaJ=T}TvB6dqWT%9S8NN+qsSL5aIFv+K^fgPW{7ySdhgtQ$#0HO^dh)cI-V>W)&; z&qF_KP%n{`;X^M`LqKQRT4+Ta)5cMDuSwvLyQqdiCwTHIL%>6lh-*?VE=iHNx z4O$&EaJ&iZ!Dqy%nWJ6A}?wZ-F48ifu3nMY(tX zB&;R~*9IlkjGBe*oTRH>(ZP6a)xd0msIIXMQR;*Uw7@n+3zMJkj*>u;!{p9@Knq3` zwkvggCT!|!NJ)L2Qb;Ciwt#Az!vZQqBCypi&Eo7ZIj76vyuf-igulkEr@ z97UzmX>Z!^)y+1F357zAhR^502#?jPA<_0|*7~dh4=u@545a89a1_NBy&Hu|9O?WO zRAW(Avlh~X6GqvhK`~z1xl$G=2>*ww>LS`gNw^g6KZUJSuK}qfwDfLM!i8&H@3=C! zs0l@+n<-uI%4V^#T}Qgrt{WiQ#DYzNC@Gp_*gVG&1TLXjNYo^l;KL-=@G6STMdI;j zD8~DuEQ3dki9~3BELh72tD?2BU_6-R^19TmLJeiP@)Fmdlj}5tYGBViG?gAG$4y-Y zbC>3n+_BI(^%l5t?pTxp)L9emA7{JRxcIQx`cT9DZsNwK>PnB{NAW1Z zM^g6d%g-MdjMMJCcT+8yb(5rfth}^phHxLAs3@%(2O~WMOKct5c(uNyrs7v}*S5Xn z?T+Kg`qkaz!*r9c=*0JBU&n@-SNS`p>t~-G+ws2Q%=G>8Zxc^WUs~ZR{#bVEOJ@B1 zfsQhF(F@I8{A2f;YZs?7_a6MLe7VP^+*lU zD6ju)(RA&(GbLvmmy4S+G|G_fHzjYo3duH9cz*4bn}xHd4vy4393@=D^TE0c`N64S P=dCAV4gB@meMkQQ>>+R< diff --git a/client/src/main/resources/popover-empty.png b/client/src/main/resources/popover-empty.png deleted file mode 100644 index c5808ec13a32322453573e04f7ff09279e39f19f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4624 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 ZfzG=*137FSLqNa-Ro7K3SFwHk{{SL7K=1$n From 9965f352e3fdd69c919aed5c5c4b94e55249f1bd Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 16:28:17 +0100 Subject: [PATCH 028/231] Improve MFC search by also searching in the models cache --- .../src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index 1eeecf86..98f43d30 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -616,6 +616,15 @@ public class MyFreeCamsClient { synchronized (monitor) { monitor.wait(); } + + for (MyFreeCamsModel model : models.asMap().values()) { + if(StringUtil.isNotBlank(model.getName())) { + if(model.getName().contains(q)) { + result.add(model); + } + } + } + return result; } } From 240e5e0d927a60af8f1442c0461b748244ad13b1 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 17:04:49 +0100 Subject: [PATCH 029/231] Add setting for online check interval --- common/src/main/java/ctbrec/Settings.java | 1 + .../java/ctbrec/recorder/LocalRecorder.java | 32 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index e7ff8ceb..5937bee3 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -72,4 +72,5 @@ public class Settings { public int windowY; public int splitRecordings = 0; public List disabledSites = new ArrayList<>(); + public long onlineCheckIntervalInSecs = 60; } diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 245a5a75..5587d7ab 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -9,6 +9,7 @@ import java.nio.file.Files; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; +import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; @@ -404,15 +405,14 @@ public class LocalRecorder implements Recorder { public void run() { running = true; while (running) { + Instant begin = Instant.now(); for (Model model : getModelsRecording()) { try { - if (!model.isSuspended() && !recordingProcesses.containsKey(model)) { - boolean isOnline = model.isOnline(IGNORE_CACHE); - LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline")); - if (isOnline) { - LOG.info("Model {}'s room back to public. Starting recording", model); - startRecordingProcess(model); - } + boolean isOnline = model.isOnline(IGNORE_CACHE); + LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline")); + if (isOnline && !isSuspended(model) && !recordingProcesses.containsKey(model)) { + LOG.info("Model {}'s room back to public. Starting recording", model); + startRecordingProcess(model); } } catch (HttpException e) { LOG.error("Couldn't check if model {} is online. HTTP Response: {} - {}", @@ -421,12 +421,20 @@ public class LocalRecorder implements Recorder { LOG.error("Couldn't check if model {} is online", model.getName(), e); } } + Instant end = Instant.now(); + Duration timeCheckTook = Duration.between(begin, end); - try { - if (running) - Thread.sleep(TimeUnit.SECONDS.toMillis(60)); - } catch (InterruptedException e) { - LOG.trace("Sleep interrupted"); + long sleepTime = Config.getInstance().getSettings().onlineCheckIntervalInSecs; + if(timeCheckTook.getSeconds() < sleepTime) { + try { + if (running) { + long millis = TimeUnit.SECONDS.toMillis(sleepTime - timeCheckTook.getSeconds()); + LOG.trace("Sleeping {}ms", millis); + Thread.sleep(millis); + } + } catch (InterruptedException e) { + LOG.trace("Sleep interrupted"); + } } } LOG.debug(getName() + " terminated"); From 84dfeb94846a9741f446339a78802e01390198e2 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 19:11:15 +0100 Subject: [PATCH 030/231] Add setting to SettingsTab to define the color scheme of the app --- .../java/ctbrec/ui/CamrecApplication.java | 35 +++++++-- .../main/java/ctbrec/ui/ColorSettingsPane.css | 10 +++ .../java/ctbrec/ui/ColorSettingsPane.java | 73 +++++++++++++++++++ .../src/main/java/ctbrec/ui/SettingsTab.java | 11 ++- common/src/main/java/ctbrec/Settings.java | 3 +- 5 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/ColorSettingsPane.css create mode 100644 client/src/main/java/ctbrec/ui/ColorSettingsPane.java diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 51249043..bf9bc55b 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -2,6 +2,7 @@ package ctbrec.ui; import java.io.BufferedReader; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -44,6 +45,7 @@ import javafx.scene.control.Alert; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.image.Image; +import javafx.scene.paint.Color; import javafx.stage.Stage; import okhttp3.Request; import okhttp3.Response; @@ -122,8 +124,13 @@ public class CamrecApplication extends Application { rootPane.getTabs().add(new DonateTabFx()); switchToStartTab(); - - loadUserStyleSheet(primaryStage); + writeColorSchemeStyleSheet(primaryStage); + Color base = Color.web(Config.getInstance().getSettings().colorBase); + if(!base.equals(Color.WHITE)) { + loadStyleSheet(primaryStage, "color.css"); + } + loadStyleSheet(primaryStage, "style.css"); + primaryStage.getScene().getStylesheets().add("/ctbrec/ui/ColorSettingsPane.css"); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/ThumbCell.css"); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/SearchBox.css"); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/Popover.css"); @@ -188,10 +195,26 @@ public class CamrecApplication extends Application { }); } - private void loadUserStyleSheet(Stage primaryStage) { - File userCss = new File(Config.getInstance().getConfigDir(), "style.css"); - if(userCss.exists() && userCss.isFile()) { - primaryStage.getScene().getStylesheets().add(userCss.toURI().toString()); + private void writeColorSchemeStyleSheet(Stage primaryStage) { + File colorCss = new File(Config.getInstance().getConfigDir(), "color.css"); + try(FileOutputStream fos = new FileOutputStream(colorCss)) { + String content = ".root {\n" + + " -fx-base: "+Config.getInstance().getSettings().colorBase+";\n" + + " -fx-accent: "+Config.getInstance().getSettings().colorAccent+";\n" + + " -fx-default-button: -fx-accent;\n" + + " -fx-focus-color: -fx-accent;\n" + + " -fx-control-inner-background-alt: derive(-fx-base, 95%);\n" + + "}"; + fos.write(content.getBytes("utf-8")); + } catch(Exception e) { + LOG.error("Couldn't write stylesheet for user defined color theme"); + } + } + + private void loadStyleSheet(Stage primaryStage, String filename) { + File css = new File(Config.getInstance().getConfigDir(), filename); + if(css.exists() && css.isFile()) { + primaryStage.getScene().getStylesheets().add(css.toURI().toString()); } } diff --git a/client/src/main/java/ctbrec/ui/ColorSettingsPane.css b/client/src/main/java/ctbrec/ui/ColorSettingsPane.css new file mode 100644 index 00000000..5b29c0fc --- /dev/null +++ b/client/src/main/java/ctbrec/ui/ColorSettingsPane.css @@ -0,0 +1,10 @@ +ColorSettingsPane .color-picker .color-picker-label .text { + visibility: false; +} +/* +ColorSettingsPane .color-picker > .arrow-button, +ColorSettingsPane .color-picker > .arrow-button:hover +{ + visibility: false; +} +*/ \ No newline at end of file diff --git a/client/src/main/java/ctbrec/ui/ColorSettingsPane.java b/client/src/main/java/ctbrec/ui/ColorSettingsPane.java new file mode 100644 index 00000000..073152ec --- /dev/null +++ b/client/src/main/java/ctbrec/ui/ColorSettingsPane.java @@ -0,0 +1,73 @@ +package ctbrec.ui; + +import ctbrec.Config; +import javafx.scene.control.Button; +import javafx.scene.control.ColorPicker; +import javafx.scene.control.Label; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; + +public class ColorSettingsPane extends Pane { + + Label labelBaseColor = new Label("Base"); + ColorPicker baseColor = new ColorPicker(); + Label labelAccentColor = new Label("Accent"); + ColorPicker accentColor = new ColorPicker(); + Button reset = new Button("Reset"); + Pane foobar = new Pane(); + + public ColorSettingsPane() { + getChildren().add(labelBaseColor); + getChildren().add(baseColor); + getChildren().add(labelAccentColor); + getChildren().add(accentColor); + getChildren().add(reset); + + baseColor.setValue(Color.web(Config.getInstance().getSettings().colorBase)); + accentColor.setValue(Color.web(Config.getInstance().getSettings().colorAccent)); + + baseColor.setOnAction(evt -> Config.getInstance().getSettings().colorBase = toWeb(baseColor.getValue())); + accentColor.setOnAction(evt -> Config.getInstance().getSettings().colorAccent = toWeb(accentColor.getValue())); + reset.setOnAction(evt -> { + baseColor.setValue(Color.WHITE); + Config.getInstance().getSettings().colorBase = toWeb(Color.WHITE); + accentColor.setValue(Color.WHITE); + Config.getInstance().getSettings().colorAccent = toWeb(Color.WHITE); + }); + } + + private String toWeb(Color value) { + StringBuilder sb = new StringBuilder("#"); + sb.append(toHex((int) (value.getRed() * 255))); + sb.append(toHex((int) (value.getGreen() * 255))); + sb.append(toHex((int) (value.getBlue() * 255))); + if(!value.isOpaque()) { + sb.append(toHex((int) (value.getOpacity() * 255))); + } + return sb.toString(); + } + + private CharSequence toHex(int v) { + StringBuilder sb = new StringBuilder(); + if(v < 16) { + sb.append('0'); + } + sb.append(Integer.toHexString(v)); + return sb; + } + + @Override + protected void layoutChildren() { + labelBaseColor.resize(32, 25); + baseColor.resize(44, 25); + labelAccentColor.resize(46, 25); + accentColor.resize(44, 25); + reset.resize(60, 25); + + labelBaseColor.setTranslateX(0); + baseColor.setTranslateX(labelBaseColor.getWidth() + 10); + labelAccentColor.setTranslateX(baseColor.getTranslateX() + baseColor.getWidth() + 15); + accentColor.setTranslateX(labelAccentColor.getTranslateX() + labelAccentColor.getWidth() + 10); + reset.setTranslateX(accentColor.getTranslateX() + accentColor.getWidth() + 50); + } +} diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java index 11287e82..7e3a6459 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/SettingsTab.java @@ -121,12 +121,12 @@ public class SettingsTab extends Tab implements TabSelectionListener { leftSide.getChildren().add(createGeneralPanel()); leftSide.getChildren().add(createLocationsPanel()); leftSide.getChildren().add(createRecordLocationPanel()); - proxySettingsPane = new ProxySettingsPane(this); - leftSide.getChildren().add(proxySettingsPane); //right side rightSide.getChildren().add(createSiteSelectionPanel()); rightSide.getChildren().add(credentialsAccordion); + proxySettingsPane = new ProxySettingsPane(this); + rightSide.getChildren().add(proxySettingsPane); for (int i = 0; i < sites.size(); i++) { Site site = sites.get(i); ConfigUI siteConfig = SiteUiFactory.getUi(site).getConfigUI(); @@ -384,6 +384,13 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(startTab, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + l = new Label("Colors"); + layout.add(l, 0, row); + ColorSettingsPane colorSettingsPane = new ColorSettingsPane(); + layout.add(colorSettingsPane, 1, row++); + GridPane.setMargin(l, new Insets(0, 0, 0, 0)); + GridPane.setMargin(colorSettingsPane, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + splitAfter.prefWidthProperty().bind(startTab.widthProperty()); maxResolution.prefWidthProperty().bind(startTab.widthProperty()); diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 5937bee3..4772116b 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -72,5 +72,6 @@ public class Settings { public int windowY; public int splitRecordings = 0; public List disabledSites = new ArrayList<>(); - public long onlineCheckIntervalInSecs = 60; + public String colorBase = "#FFFFFF"; + public String colorAccent = "#FFFFFF"; } From 3bfb76e44179a71f82b00fffd15fdfba9c5aa85a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 19:12:27 +0100 Subject: [PATCH 031/231] Re-add onlineCheckIntervalInSecs Was removed by accident --- common/src/main/java/ctbrec/Settings.java | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 4772116b..62003b51 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -74,4 +74,5 @@ public class Settings { public List disabledSites = new ArrayList<>(); public String colorBase = "#FFFFFF"; public String colorAccent = "#FFFFFF"; + public long onlineCheckIntervalInSecs = 60; } From a6be8b4b6396704cef11a72e46d7c9459a94debe Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 19:16:04 +0100 Subject: [PATCH 032/231] Show restart message, when colors are changed --- .../src/main/java/ctbrec/ui/ColorSettingsPane.java | 13 ++++++++++--- client/src/main/java/ctbrec/ui/SettingsTab.java | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/ColorSettingsPane.java b/client/src/main/java/ctbrec/ui/ColorSettingsPane.java index 073152ec..fdba86a9 100644 --- a/client/src/main/java/ctbrec/ui/ColorSettingsPane.java +++ b/client/src/main/java/ctbrec/ui/ColorSettingsPane.java @@ -16,7 +16,7 @@ public class ColorSettingsPane extends Pane { Button reset = new Button("Reset"); Pane foobar = new Pane(); - public ColorSettingsPane() { + public ColorSettingsPane(SettingsTab settingsTab) { getChildren().add(labelBaseColor); getChildren().add(baseColor); getChildren().add(labelAccentColor); @@ -26,13 +26,20 @@ public class ColorSettingsPane extends Pane { baseColor.setValue(Color.web(Config.getInstance().getSettings().colorBase)); accentColor.setValue(Color.web(Config.getInstance().getSettings().colorAccent)); - baseColor.setOnAction(evt -> Config.getInstance().getSettings().colorBase = toWeb(baseColor.getValue())); - accentColor.setOnAction(evt -> Config.getInstance().getSettings().colorAccent = toWeb(accentColor.getValue())); + baseColor.setOnAction(evt -> { + Config.getInstance().getSettings().colorBase = toWeb(baseColor.getValue()); + settingsTab.showRestartRequired(); + }); + accentColor.setOnAction(evt -> { + Config.getInstance().getSettings().colorAccent = toWeb(accentColor.getValue()); + settingsTab.showRestartRequired(); + }); reset.setOnAction(evt -> { baseColor.setValue(Color.WHITE); Config.getInstance().getSettings().colorBase = toWeb(Color.WHITE); accentColor.setValue(Color.WHITE); Config.getInstance().getSettings().colorAccent = toWeb(Color.WHITE); + settingsTab.showRestartRequired(); }); } diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java index 7e3a6459..7d828426 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/SettingsTab.java @@ -386,7 +386,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { l = new Label("Colors"); layout.add(l, 0, row); - ColorSettingsPane colorSettingsPane = new ColorSettingsPane(); + ColorSettingsPane colorSettingsPane = new ColorSettingsPane(this); layout.add(colorSettingsPane, 1, row++); GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(colorSettingsPane, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); From a57a9877b8953e8043b22cd428de5831bf517b89 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 20:10:27 +0100 Subject: [PATCH 033/231] Bumb version to 1.11.0 --- client/pom.xml | 2 +- common/pom.xml | 2 +- master/pom.xml | 2 +- server/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index 5042475d..c0e4bb57 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.10.0 + 1.11.0 ../master diff --git a/common/pom.xml b/common/pom.xml index 9152962b..54db7db8 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.10.0 + 1.11.0 ../master diff --git a/master/pom.xml b/master/pom.xml index cb6e8dca..8d905ac5 100644 --- a/master/pom.xml +++ b/master/pom.xml @@ -6,7 +6,7 @@ ctbrec master pom - 1.10.0 + 1.11.0 ../common diff --git a/server/pom.xml b/server/pom.xml index 721af621..873084e8 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.10.0 + 1.11.0 ../master From 9a51cff240ea68b6f0cbeff754ddae7f0eb01a2e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 20:10:50 +0100 Subject: [PATCH 034/231] Update changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 534bd5f9..76d34deb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ -1.10.1 +1.11.0 ======================== +* Added model search function +* Added color settings to change the appearance of the application +* Added setting for the online check interval +* Added setting to define the tab the application opens on start * Double-click starts playback of recordings * Refresh of thumbnails can be disabled From e5ff778d6ba52a651bd35ba7d472a6bf16dca9dc Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 20:58:55 +0100 Subject: [PATCH 035/231] Write config immediately after a value changed --- .../java/ctbrec/ui/ColorSettingsPane.java | 3 + .../java/ctbrec/ui/ProxySettingsPane.java | 6 ++ .../src/main/java/ctbrec/ui/SettingsTab.java | 78 +++++++++++++++---- .../ctbrec/ui/sites/AbstractConfigUI.java | 22 ++++++ .../ui/sites/bonga/BongaCamsConfigUI.java | 15 ++-- .../ctbrec/ui/sites/cam4/Cam4ConfigUI.java | 15 ++-- .../ui/sites/camsoda/CamsodaConfigUI.java | 15 ++-- .../sites/chaturbate/ChaturbateConfigUi.java | 14 +++- .../sites/myfreecams/MyFreeCamsConfigUI.java | 16 ++-- 9 files changed, 142 insertions(+), 42 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/AbstractConfigUI.java diff --git a/client/src/main/java/ctbrec/ui/ColorSettingsPane.java b/client/src/main/java/ctbrec/ui/ColorSettingsPane.java index fdba86a9..be6feb4e 100644 --- a/client/src/main/java/ctbrec/ui/ColorSettingsPane.java +++ b/client/src/main/java/ctbrec/ui/ColorSettingsPane.java @@ -29,10 +29,12 @@ public class ColorSettingsPane extends Pane { baseColor.setOnAction(evt -> { Config.getInstance().getSettings().colorBase = toWeb(baseColor.getValue()); settingsTab.showRestartRequired(); + settingsTab.saveConfig(); }); accentColor.setOnAction(evt -> { Config.getInstance().getSettings().colorAccent = toWeb(accentColor.getValue()); settingsTab.showRestartRequired(); + settingsTab.saveConfig(); }); reset.setOnAction(evt -> { baseColor.setValue(Color.WHITE); @@ -40,6 +42,7 @@ public class ColorSettingsPane extends Pane { accentColor.setValue(Color.WHITE); Config.getInstance().getSettings().colorAccent = toWeb(Color.WHITE); settingsTab.showRestartRequired(); + settingsTab.saveConfig(); }); } diff --git a/client/src/main/java/ctbrec/ui/ProxySettingsPane.java b/client/src/main/java/ctbrec/ui/ProxySettingsPane.java index bd5c3f5b..1d2ff789 100644 --- a/client/src/main/java/ctbrec/ui/ProxySettingsPane.java +++ b/client/src/main/java/ctbrec/ui/ProxySettingsPane.java @@ -51,18 +51,23 @@ public class ProxySettingsPane extends TitledPane implements EventHandler settingsTab.saveConfig()); l = new Label("Port"); layout.add(l, 0, 2); layout.add(proxyPort, 1, 2); + proxyPort.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig()); l = new Label("Username"); layout.add(l, 0, 3); layout.add(proxyUser, 1, 3); + proxyUser.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig()); + l = new Label("Password"); layout.add(l, 0, 4); layout.add(proxyPassword, 1, 4); + proxyPassword.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig()); } private void loadConfig() { @@ -86,6 +91,7 @@ public class ProxySettingsPane extends TitledPane implements EventHandler { + server.textProperty().addListener((ob, o, n) -> { if(!server.getText().isEmpty()) { Config.getInstance().getSettings().httpServer = server.getText(); + saveConfig(); } }); GridPane.setFillWidth(server, true); @@ -202,18 +205,27 @@ public class SettingsTab extends Tab implements TabSelectionListener { layout.add(new Label("Port"), 0, 2); port = new TextField(Integer.toString(Config.getInstance().getSettings().httpPort)); - port.focusedProperty().addListener((e) -> { + port.textProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue.matches("\\d*")) { + port.setText(newValue.replaceAll("[^\\d]", "")); + } if(!port.getText().isEmpty()) { - try { - Config.getInstance().getSettings().httpPort = Integer.parseInt(port.getText()); - port.setBorder(Border.EMPTY); - port.setTooltip(null); - } catch (NumberFormatException e1) { - port.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2)))); - port.setTooltip(new Tooltip("Port has to be a number in the range 1 - 65536")); - } + Config.getInstance().getSettings().httpPort = Integer.parseInt(port.getText()); + saveConfig(); } }); + // port.focusedProperty().addListener((e) -> { + // if(!port.getText().isEmpty()) { + // try { + // Config.getInstance().getSettings().httpPort = Integer.parseInt(port.getText()); + // port.setBorder(Border.EMPTY); + // port.setTooltip(null); + // } catch (NumberFormatException e1) { + // port.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2)))); + // port.setTooltip(new Tooltip("Port has to be a number in the range 1 - 65536")); + // } + // } + // }); GridPane.setFillWidth(port, true); GridPane.setHgrow(port, Priority.ALWAYS); GridPane.setColumnSpan(port, 2); @@ -229,6 +241,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { if(key == null) { key = Hmac.generateKey(); Config.getInstance().getSettings().key = key; + saveConfig(); } TextInputDialog keyDialog = new TextInputDialog(); keyDialog.setResizable(true); @@ -273,7 +286,10 @@ public class SettingsTab extends Tab implements TabSelectionListener { options.add(ONE_PER_RECORDING); directoryStructure = new ComboBox<>(FXCollections.observableList(options)); directoryStructure.setValue(Config.getInstance().getSettings().recordingsDirStructure); - directoryStructure.setOnAction((evt) -> Config.getInstance().getSettings().recordingsDirStructure = directoryStructure.getValue()); + directoryStructure.setOnAction((evt) -> { + Config.getInstance().getSettings().recordingsDirStructure = directoryStructure.getValue(); + saveConfig(); + }); GridPane.setColumnSpan(directoryStructure, 2); GridPane.setMargin(directoryStructure, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(directoryStructure, 1, row++); @@ -313,6 +329,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { loadResolution.setSelected(Config.getInstance().getSettings().determineResolution); loadResolution.setOnAction((e) -> { Config.getInstance().getSettings().determineResolution = loadResolution.isSelected(); + saveConfig(); if(!loadResolution.isSelected()) { ThumbOverviewTab.queue.clear(); } @@ -324,7 +341,10 @@ public class SettingsTab extends Tab implements TabSelectionListener { l = new Label("Allow multiple players"); layout.add(l, 0, row); multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer); - multiplePlayers.setOnAction((e) -> Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected()); + multiplePlayers.setOnAction((e) -> { + Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected(); + saveConfig(); + }); GridPane.setMargin(l, new Insets(3, 0, 0, 0)); GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(multiplePlayers, 1, row++); @@ -332,7 +352,10 @@ public class SettingsTab extends Tab implements TabSelectionListener { l = new Label("Manually select stream quality"); layout.add(l, 0, row); chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality); - chooseStreamQuality.setOnAction((e) -> Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected()); + chooseStreamQuality.setOnAction((e) -> { + Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected(); + saveConfig(); + }); GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(chooseStreamQuality, 1, row++); @@ -340,7 +363,10 @@ public class SettingsTab extends Tab implements TabSelectionListener { l = new Label("Update thumbnails"); layout.add(l, 0, row); updateThumbnails.setSelected(Config.getInstance().getSettings().updateThumbnails); - updateThumbnails.setOnAction((e) -> Config.getInstance().getSettings().updateThumbnails = updateThumbnails.isSelected()); + updateThumbnails.setOnAction((e) -> { + Config.getInstance().getSettings().updateThumbnails = updateThumbnails.isSelected(); + saveConfig(); + }); GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(updateThumbnails, 1, row++); @@ -355,7 +381,10 @@ public class SettingsTab extends Tab implements TabSelectionListener { resolutionOptions.add(0); maxResolution = new ComboBox<>(FXCollections.observableList(resolutionOptions)); setMaxResolutionValue(); - maxResolution.setOnAction((e) -> Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem()); + maxResolution.setOnAction((e) -> { + Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem(); + saveConfig(); + }); layout.add(maxResolution, 1, row++); GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); GridPane.setMargin(maxResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); @@ -372,7 +401,10 @@ public class SettingsTab extends Tab implements TabSelectionListener { splitAfter = new ComboBox<>(FXCollections.observableList(options)); layout.add(splitAfter, 1, row++); setSplitAfterValue(); - splitAfter.setOnAction((e) -> Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue()); + splitAfter.setOnAction((e) -> { + Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue(); + saveConfig(); + }); GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); @@ -380,7 +412,10 @@ public class SettingsTab extends Tab implements TabSelectionListener { layout.add(l, 0, row); startTab = new ComboBox<>(); layout.add(startTab, 1, row++); - startTab.setOnAction((e) -> Config.getInstance().getSettings().startTab = startTab.getSelectionModel().getSelectedItem()); + startTab.setOnAction((e) -> { + Config.getInstance().getSettings().startTab = startTab.getSelectionModel().getSelectedItem(); + saveConfig(); + }); GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(startTab, new Insets(0, 0, 0, CHECKBOX_MARGIN)); @@ -488,6 +523,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { setPostProcessing(program); } else { Config.getInstance().getSettings().postProcessing = ""; + saveConfig(); } } } @@ -501,6 +537,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { mediaPlayer.setTooltip(new Tooltip(msg)); } else { Config.getInstance().getSettings().mediaPlayer = mediaPlayer.getText(); + saveConfig(); } } @@ -511,6 +548,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { postProcessing.setTooltip(new Tooltip(msg)); } else { Config.getInstance().getSettings().postProcessing = postProcessing.getText(); + saveConfig(); } } @@ -587,6 +625,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { String path = dir.getCanonicalPath(); Config.getInstance().getSettings().recordingsDir = path; recordingsDirectory.setText(path); + saveConfig(); } catch (IOException e1) { LOG.error("Couldn't determine directory path", e1); Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); @@ -625,6 +664,11 @@ public class SettingsTab extends Tab implements TabSelectionListener { public void saveConfig() { proxySettingsPane.saveConfig(); + try { + Config.getInstance().save(); + } catch (IOException e) { + LOG.error("Couldn't save config", e); + } } public static class SplitAfterOption { diff --git a/client/src/main/java/ctbrec/ui/sites/AbstractConfigUI.java b/client/src/main/java/ctbrec/ui/sites/AbstractConfigUI.java new file mode 100644 index 00000000..69e2fd9a --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/AbstractConfigUI.java @@ -0,0 +1,22 @@ +package ctbrec.ui.sites; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.sites.ConfigUI; + +public abstract class AbstractConfigUI implements ConfigUI { + + private static final transient Logger LOG = LoggerFactory.getLogger(AbstractConfigUI.class); + + protected void save() { + try { + Config.getInstance().save(); + } catch (IOException e) { + LOG.error("Couldn't save config"); + } + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java index 4c74d258..ce8381dd 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java @@ -1,10 +1,10 @@ package ctbrec.ui.sites.bonga; import ctbrec.Config; -import ctbrec.sites.ConfigUI; import ctbrec.sites.bonga.BongaCams; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.SettingsTab; +import ctbrec.ui.sites.AbstractConfigUI; import javafx.geometry.Insets; import javafx.scene.Parent; import javafx.scene.control.Button; @@ -14,8 +14,7 @@ import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; -public class BongaCamsConfigUI implements ConfigUI { - +public class BongaCamsConfigUI extends AbstractConfigUI { private BongaCams bongaCams; public BongaCamsConfigUI(BongaCams bongaCams) { @@ -27,7 +26,10 @@ public class BongaCamsConfigUI implements ConfigUI { GridPane layout = SettingsTab.createGridLayout(); layout.add(new Label("BongaCams User"), 0, 0); TextField username = new TextField(Config.getInstance().getSettings().bongaUsername); - username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().bongaUsername = username.getText()); + username.textProperty().addListener((ob, o, n) -> { + Config.getInstance().getSettings().bongaUsername = username.getText(); + save(); + }); GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); GridPane.setColumnSpan(username, 2); @@ -36,7 +38,10 @@ public class BongaCamsConfigUI implements ConfigUI { layout.add(new Label("BongaCams Password"), 0, 1); PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().bongaPassword); - password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().bongaPassword = password.getText()); + password.focusedProperty().addListener((e) -> { + Config.getInstance().getSettings().bongaPassword = password.getText(); + save(); + }); GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); GridPane.setColumnSpan(password, 2); diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java index 4429c729..bacea351 100644 --- a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java @@ -1,10 +1,10 @@ package ctbrec.ui.sites.cam4; import ctbrec.Config; -import ctbrec.sites.ConfigUI; import ctbrec.sites.cam4.Cam4; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.SettingsTab; +import ctbrec.ui.sites.AbstractConfigUI; import javafx.geometry.Insets; import javafx.scene.Parent; import javafx.scene.control.Button; @@ -14,14 +14,16 @@ import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; -public class Cam4ConfigUI implements ConfigUI { - +public class Cam4ConfigUI extends AbstractConfigUI { @Override public Parent createConfigPanel() { 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()); + username.textProperty().addListener((ob, o, n) -> { + Config.getInstance().getSettings().cam4Username = username.getText(); + save(); + }); GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); GridPane.setColumnSpan(username, 2); @@ -30,7 +32,10 @@ public class Cam4ConfigUI implements ConfigUI { 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()); + password.focusedProperty().addListener((e) -> { + Config.getInstance().getSettings().cam4Password = password.getText(); + save(); + }); GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); GridPane.setColumnSpan(password, 2); diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java index c8468a82..b19abc95 100644 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java @@ -1,10 +1,10 @@ package ctbrec.ui.sites.camsoda; import ctbrec.Config; -import ctbrec.sites.ConfigUI; import ctbrec.sites.camsoda.Camsoda; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.SettingsTab; +import ctbrec.ui.sites.AbstractConfigUI; import javafx.geometry.Insets; import javafx.scene.Parent; import javafx.scene.control.Button; @@ -14,8 +14,7 @@ import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; -public class CamsodaConfigUI implements ConfigUI { - +public class CamsodaConfigUI extends AbstractConfigUI { private Camsoda camsoda; public CamsodaConfigUI(Camsoda camsoda) { @@ -27,7 +26,10 @@ public class CamsodaConfigUI implements ConfigUI { GridPane layout = SettingsTab.createGridLayout(); layout.add(new Label("CamSoda User"), 0, 0); TextField username = new TextField(Config.getInstance().getSettings().camsodaUsername); - username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().camsodaUsername = username.getText()); + username.textProperty().addListener((ob, o, n) -> { + Config.getInstance().getSettings().camsodaUsername = username.getText(); + save(); + }); GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); GridPane.setColumnSpan(username, 2); @@ -36,7 +38,10 @@ public class CamsodaConfigUI implements ConfigUI { layout.add(new Label("CamSoda Password"), 0, 1); PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().camsodaPassword); - password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().camsodaPassword = password.getText()); + password.textProperty().addListener((ob, o, n) -> { + Config.getInstance().getSettings().camsodaPassword = password.getText(); + save(); + }); GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); GridPane.setColumnSpan(password, 2); diff --git a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java index 1a824c0c..e48a7892 100644 --- a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java +++ b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java @@ -1,10 +1,10 @@ package ctbrec.ui.sites.chaturbate; import ctbrec.Config; -import ctbrec.sites.ConfigUI; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.SettingsTab; +import ctbrec.ui.sites.AbstractConfigUI; import javafx.geometry.Insets; import javafx.scene.Parent; import javafx.scene.control.Button; @@ -14,14 +14,17 @@ import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; -public class ChaturbateConfigUi implements ConfigUI { +public class ChaturbateConfigUi extends AbstractConfigUI { @Override public Parent createConfigPanel() { GridPane layout = SettingsTab.createGridLayout(); layout.add(new Label("Chaturbate User"), 0, 0); TextField username = new TextField(Config.getInstance().getSettings().username); - username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().username = username.getText()); + username.textProperty().addListener((ob, o, n) -> { + Config.getInstance().getSettings().username = username.getText(); + save(); + }); GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); GridPane.setColumnSpan(username, 2); @@ -30,7 +33,10 @@ public class ChaturbateConfigUi implements ConfigUI { layout.add(new Label("Chaturbate Password"), 0, 1); PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().password); - password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().password = password.getText()); + password.textProperty().addListener((ob, o, n) -> { + Config.getInstance().getSettings().password = password.getText(); + save(); + }); GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); GridPane.setColumnSpan(password, 2); diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java index e74f63d8..79d3bdc9 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java @@ -1,10 +1,10 @@ package ctbrec.ui.sites.myfreecams; import ctbrec.Config; -import ctbrec.sites.ConfigUI; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.SettingsTab; +import ctbrec.ui.sites.AbstractConfigUI; import javafx.geometry.Insets; import javafx.scene.Parent; import javafx.scene.control.Button; @@ -14,8 +14,7 @@ import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; -public class MyFreeCamsConfigUI implements ConfigUI { - +public class MyFreeCamsConfigUI extends AbstractConfigUI { private MyFreeCams myFreeCams; public MyFreeCamsConfigUI(MyFreeCams myFreeCams) { @@ -27,7 +26,10 @@ public class MyFreeCamsConfigUI implements ConfigUI { GridPane layout = SettingsTab.createGridLayout(); layout.add(new Label("MyFreeCams User"), 0, 0); TextField username = new TextField(Config.getInstance().getSettings().mfcUsername); - username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().mfcUsername = username.getText()); + username.textProperty().addListener((ob, o, n) -> { + Config.getInstance().getSettings().mfcUsername = username.getText(); + save(); + }); GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); GridPane.setColumnSpan(username, 2); @@ -36,7 +38,10 @@ public class MyFreeCamsConfigUI implements ConfigUI { layout.add(new Label("MyFreeCams Password"), 0, 1); PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().mfcPassword); - password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().mfcPassword = password.getText()); + password.textProperty().addListener((ob, o, n) -> { + Config.getInstance().getSettings().mfcPassword = password.getText(); + save(); + }); GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); GridPane.setColumnSpan(password, 2); @@ -51,5 +56,4 @@ public class MyFreeCamsConfigUI implements ConfigUI { GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); return layout; } - } From 9817fdfb422f59ca93be263f7e6d823256cb83a7 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 21:17:17 +0100 Subject: [PATCH 036/231] Add setting for the online check interval --- .../src/main/java/ctbrec/ui/SettingsTab.java | 30 +++++++++++-------- common/src/main/java/ctbrec/Settings.java | 2 +- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java index ce241df8..2752e20f 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/SettingsTab.java @@ -64,6 +64,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private TextField postProcessing; private TextField server; private TextField port; + private TextField onlineCheckIntervalInSecs; private CheckBox loadResolution; private CheckBox secureCommunication = new CheckBox(); private CheckBox chooseStreamQuality = new CheckBox(); @@ -214,18 +215,6 @@ public class SettingsTab extends Tab implements TabSelectionListener { saveConfig(); } }); - // port.focusedProperty().addListener((e) -> { - // if(!port.getText().isEmpty()) { - // try { - // Config.getInstance().getSettings().httpPort = Integer.parseInt(port.getText()); - // port.setBorder(Border.EMPTY); - // port.setTooltip(null); - // } catch (NumberFormatException e1) { - // port.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2)))); - // port.setTooltip(new Tooltip("Port has to be a number in the range 1 - 65536")); - // } - // } - // }); GridPane.setFillWidth(port, true); GridPane.setHgrow(port, Priority.ALWAYS); GridPane.setColumnSpan(port, 2); @@ -408,6 +397,20 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + layout.add(new Label("Check online state every (secs"), 0, row); + onlineCheckIntervalInSecs = new TextField(Integer.toString(Config.getInstance().getSettings().onlineCheckIntervalInSecs)); + onlineCheckIntervalInSecs.textProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue.matches("\\d*")) { + onlineCheckIntervalInSecs.setText(newValue.replaceAll("[^\\d]", "")); + } + if(!onlineCheckIntervalInSecs.getText().isEmpty()) { + Config.getInstance().getSettings().onlineCheckIntervalInSecs = Integer.parseInt(onlineCheckIntervalInSecs.getText()); + saveConfig(); + } + }); + GridPane.setMargin(onlineCheckIntervalInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + layout.add(onlineCheckIntervalInSecs, 1, row++); + l = new Label("Start Tab"); layout.add(l, 0, row); startTab = new ComboBox<>(); @@ -426,8 +429,11 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(colorSettingsPane, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + splitAfter.prefWidthProperty().bind(startTab.widthProperty()); maxResolution.prefWidthProperty().bind(startTab.widthProperty()); + onlineCheckIntervalInSecs.prefWidthProperty().bind(startTab.widthProperty()); + onlineCheckIntervalInSecs.maxWidthProperty().bind(startTab.widthProperty()); TitledPane general = new TitledPane("General", layout); general.setCollapsible(false); diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 62003b51..fc114a28 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -74,5 +74,5 @@ public class Settings { public List disabledSites = new ArrayList<>(); public String colorBase = "#FFFFFF"; public String colorAccent = "#FFFFFF"; - public long onlineCheckIntervalInSecs = 60; + public int onlineCheckIntervalInSecs = 60; } From faf6240b454c51b4dc6c20824e93ae5c3818c9dd Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 21:23:37 +0100 Subject: [PATCH 037/231] Save changes to the list of recorded models immediately ... to make sure, that the settings are persisted in case the process dies or gets killed. --- common/src/main/java/ctbrec/recorder/LocalRecorder.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 5587d7ab..3afb3ee4 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -98,6 +98,9 @@ public class LocalRecorder implements Recorder { try { models.add(model); config.getSettings().models.add(model); + config.save(); + } catch (IOException e) { + LOG.error("Couldn't save config", e); } finally { lock.unlock(); } @@ -115,6 +118,7 @@ public class LocalRecorder implements Recorder { stopRecordingProcess(model); } LOG.info("Model {} removed", model); + config.save(); } else { throw new NoSuchElementException("Model " + model.getName() + " ["+model.getUrl()+"] not found in list of recorded models"); } @@ -689,6 +693,7 @@ public class LocalRecorder implements Recorder { stopRecordingProcess(model); } tryRestartRecording(model); + config.save(); } @Override @@ -699,10 +704,13 @@ public class LocalRecorder implements Recorder { int index = models.indexOf(model); models.get(index).setSuspended(true); model.setSuspended(true); + config.save(); } else { LOG.warn("Couldn't suspend model {}. Not found in list", model.getName()); return; } + } catch (IOException e) { + LOG.error("Couldn't save config", e); } finally { lock.unlock(); } @@ -723,6 +731,7 @@ public class LocalRecorder implements Recorder { m.setSuspended(false); startRecordingProcess(m); model.setSuspended(false); + config.save(); } else { LOG.warn("Couldn't resume model {}. Not found in list", model.getName()); return; From b869c4a82c3014e0ec6a5d35f9e5db8594c8d0ea Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 22:10:12 +0100 Subject: [PATCH 038/231] Save and restore table states Save and the restore the table state (sorting and column widths) for the recorded models and the recordings tables --- CHANGELOG.md | 2 + .../java/ctbrec/ui/CamrecApplication.java | 2 + .../java/ctbrec/ui/RecordedModelsTab.java | 51 +++++++++++++++++++ .../main/java/ctbrec/ui/RecordingsTab.java | 39 ++++++++++++++ .../src/main/java/ctbrec/ui/SettingsTab.java | 4 +- common/src/main/java/ctbrec/Settings.java | 6 +++ 6 files changed, 103 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76d34deb..ff02c63e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ * Added setting to define the tab the application opens on start * Double-click starts playback of recordings * Refresh of thumbnails can be disabled +* Changed settings are saved immediately (including changes of the + list of recorded models) 1.10.0 ======================== diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index bf9bc55b..72c0259d 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -155,6 +155,8 @@ public class CamrecApplication extends Application { new Thread() { @Override public void run() { + modelsTab.saveState(); + recordingsTab.saveState(); settingsTab.saveConfig(); recorder.shutdown(); for (Site site : sites) { diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index e3d44c7a..2fc735fd 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -20,8 +20,10 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ctbrec.Config; import ctbrec.Model; import ctbrec.Recording; +import ctbrec.StringUtil; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; import ctbrec.ui.controls.AutoFillTextField; @@ -42,6 +44,7 @@ import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.control.TableColumn; +import javafx.scene.control.TableColumn.SortType; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.control.cell.CheckBoxTableCell; @@ -135,6 +138,16 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { stopAction(); } }); + table.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if(event.getCode() == KeyCode.S) { + for (TableColumn col : table.getSortOrder()) { + System.out.println(col.getText()); + System.out.println(col.getSortType()); + System.out.println(col.getComparator()); + } + } + }); + scrollPane.setContent(table); HBox addModelBox = new HBox(5); @@ -156,6 +169,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { root.setTop(addModelBox); root.setCenter(scrollPane); setContent(root); + + restoreState(); } private void addModel(ActionEvent e) { @@ -223,6 +238,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { iterator.remove(); } } + + table.sort(); }); updateService.setOnFailed((event) -> { LOG.info("Couldn't get list of models from recorder", event.getSource().getException()); @@ -464,5 +481,39 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } }.start(); } + } + + public void saveState() { + if(!table.getSortOrder().isEmpty()) { + TableColumn col = table.getSortOrder().get(0); + Config.getInstance().getSettings().recordedModelsSortColumn = col.getText(); + Config.getInstance().getSettings().recordedModelsSortType = col.getSortType().toString(); + } + double[] columnWidths = new double[table.getColumns().size()]; + for (int i = 0; i < columnWidths.length; i++) { + columnWidths[i] = table.getColumns().get(i).getWidth(); + } + Config.getInstance().getSettings().recordedModelsColumnWidths = columnWidths; }; + + private void restoreState() { + String sortCol = Config.getInstance().getSettings().recordedModelsSortColumn; + if(StringUtil.isNotBlank(sortCol)) { + for (TableColumn col : table.getColumns()) { + if(Objects.equals(sortCol, col.getText())) { + col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordedModelsSortType)); + table.getSortOrder().clear(); + table.getSortOrder().add(col); + break; + } + } + } + + double[] columnWidths = Config.getInstance().getSettings().recordedModelsColumnWidths; + if(columnWidths != null && columnWidths.length == table.getColumns().size()) { + for (int i = 0; i < columnWidths.length; i++) { + table.getColumns().get(i).setPrefWidth(columnWidths[i]); + } + } + } } diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index 5605dbee..2c7f4fb2 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -16,6 +16,7 @@ import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; @@ -30,6 +31,7 @@ import com.iheartradio.m3u8.PlaylistException; import ctbrec.Config; import ctbrec.Recording; import ctbrec.Recording.STATUS; +import ctbrec.StringUtil; import ctbrec.recorder.Recorder; import ctbrec.recorder.download.MergedHlsDownload; import ctbrec.sites.Site; @@ -50,6 +52,7 @@ import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; +import javafx.scene.control.TableColumn.SortType; import javafx.scene.control.TableView; import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.input.ContextMenuEvent; @@ -180,6 +183,8 @@ public class RecordingsTab extends Tab implements TabSelectionListener { root.setPadding(new Insets(5)); root.setCenter(scrollPane); setContent(root); + + restoreState(); } void initializeUpdateService() { @@ -537,4 +542,38 @@ public class RecordingsTab extends Tab implements TabSelectionListener { table.setCursor(Cursor.DEFAULT); } } + + public void saveState() { + if(!table.getSortOrder().isEmpty()) { + TableColumn col = table.getSortOrder().get(0); + Config.getInstance().getSettings().recordingsSortColumn = col.getText(); + Config.getInstance().getSettings().recordingsSortType = col.getSortType().toString(); + } + double[] columnWidths = new double[table.getColumns().size()]; + for (int i = 0; i < columnWidths.length; i++) { + columnWidths[i] = table.getColumns().get(i).getWidth(); + } + Config.getInstance().getSettings().recordingsColumnWidths = columnWidths; + }; + + private void restoreState() { + String sortCol = Config.getInstance().getSettings().recordingsSortColumn; + if(StringUtil.isNotBlank(sortCol)) { + for (TableColumn col : table.getColumns()) { + if(Objects.equals(sortCol, col.getText())) { + col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordingsSortType)); + table.getSortOrder().clear(); + table.getSortOrder().add(col); + break; + } + } + } + + double[] columnWidths = Config.getInstance().getSettings().recordingsColumnWidths; + if(columnWidths != null && columnWidths.length == table.getColumns().size()) { + for (int i = 0; i < columnWidths.length; i++) { + table.getColumns().get(i).setPrefWidth(columnWidths[i]); + } + } + } } diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java index 2752e20f..45adc8a7 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/SettingsTab.java @@ -669,7 +669,9 @@ public class SettingsTab extends Tab implements TabSelectionListener { } public void saveConfig() { - proxySettingsPane.saveConfig(); + if(proxySettingsPane != null) { + proxySettingsPane.saveConfig(); + } try { Config.getInstance().save(); } catch (IOException e) { diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index fc114a28..a34e24e9 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -75,4 +75,10 @@ public class Settings { public String colorBase = "#FFFFFF"; public String colorAccent = "#FFFFFF"; public int onlineCheckIntervalInSecs = 60; + public String recordedModelsSortColumn = ""; + public String recordedModelsSortType = ""; + public double[] recordedModelsColumnWidths = new double[0]; + public String recordingsSortColumn = ""; + public String recordingsSortType = ""; + public double[] recordingsColumnWidths = new double[0]; } From 3b9927a591c8178436443bcd4f83224efe08ae08 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 23:01:14 +0100 Subject: [PATCH 039/231] Make search case insensitive --- common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index 98f43d30..013dea20 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -619,7 +619,7 @@ public class MyFreeCamsClient { for (MyFreeCamsModel model : models.asMap().values()) { if(StringUtil.isNotBlank(model.getName())) { - if(model.getName().contains(q)) { + if(model.getName().toLowerCase().contains(q.toLowerCase())) { result.add(model); } } From 0fa716abdb2f966016a75f043fd219eeec742544 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 23:01:27 +0100 Subject: [PATCH 040/231] Fix label --- client/src/main/java/ctbrec/ui/SettingsTab.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java index 45adc8a7..54091aa9 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/SettingsTab.java @@ -397,7 +397,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(new Label("Check online state every (secs"), 0, row); + layout.add(new Label("Check online state every (seconds)"), 0, row); onlineCheckIntervalInSecs = new TextField(Integer.toString(Config.getInstance().getSettings().onlineCheckIntervalInSecs)); onlineCheckIntervalInSecs.textProperty().addListener((observable, oldValue, newValue) -> { if (!newValue.matches("\\d*")) { From 6ee39cb6f4393eadd09b3c3f37d7ef314cbf70b8 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 23:01:54 +0100 Subject: [PATCH 041/231] Set smooth property for search thumbnail --- .../src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java index 206a3855..8b949ece 100644 --- a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java +++ b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java @@ -171,6 +171,7 @@ public class SearchPopoverTreeList extends PopoverTreeList implements Pop thumb.setFitWidth(thumbSize); thumb.setFitHeight(thumbSize); thumb.setClip(clip); + thumb.setSmooth(true); follow = new Button("Follow"); follow.setOnAction((evt) -> { From eb27defe81d3b3872c06e6ee182b7f1334a42296 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 25 Nov 2018 23:52:57 +0100 Subject: [PATCH 042/231] Update download links to 1.11.0 --- docs/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.html b/docs/index.html index 079938dd..705ac05b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -118,19 +118,19 @@
    - + Download for Linux! From 91ea7d65a33e63e8766ccad11993b80ac27f58e5 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 26 Nov 2018 00:13:53 +0100 Subject: [PATCH 043/231] Sort by height only if the height is set If the height is not available, it is set to Integer.MAX_VALUE. IT makes not sense to compare by that value. Instead compare the bitrates. --- common/src/main/java/ctbrec/recorder/download/StreamSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/recorder/download/StreamSource.java b/common/src/main/java/ctbrec/recorder/download/StreamSource.java index 1968df81..dbb86f03 100644 --- a/common/src/main/java/ctbrec/recorder/download/StreamSource.java +++ b/common/src/main/java/ctbrec/recorder/download/StreamSource.java @@ -54,7 +54,7 @@ public class StreamSource implements Comparable { @Override public int compareTo(StreamSource o) { int heightDiff = height - o.height; - if(heightDiff != 0) { + if(heightDiff != 0 && height != Integer.MAX_VALUE && o.height != Integer.MAX_VALUE) { return heightDiff; } else { return bandwidth - o.bandwidth; From 8fdb24bad18d011e2b638711fe006882eaf1a327 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 26 Nov 2018 13:49:42 +0100 Subject: [PATCH 044/231] Add methods to get the free and total space --- .../java/ctbrec/recorder/LocalRecorder.java | 17 +++++++++ .../main/java/ctbrec/recorder/Recorder.java | 14 +++++++ .../java/ctbrec/recorder/RemoteRecorder.java | 38 +++++++++++++++++++ .../recorder/server/RecorderServlet.java | 6 ++- 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 3afb3ee4..f4fb1f1f 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -5,6 +5,7 @@ import static ctbrec.Recording.STATUS.*; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; +import java.nio.file.FileStore; import java.nio.file.Files; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -745,4 +746,20 @@ public class LocalRecorder implements Recorder { public HttpClient getHttpClient() { return client; } + + @Override + public long getTotalSpaceBytes() throws IOException { + return getRecordingsFileStore().getTotalSpace(); + } + + @Override + public long getFreeSpaceBytes() throws IOException { + return getRecordingsFileStore().getUsableSpace(); + } + + private FileStore getRecordingsFileStore() throws IOException { + File recordingsDir = new File(Config.getInstance().getSettings().recordingsDir); + FileStore store = Files.getFileStore(recordingsDir.toPath()); + return store; + } } diff --git a/common/src/main/java/ctbrec/recorder/Recorder.java b/common/src/main/java/ctbrec/recorder/Recorder.java index bc4e60cf..1a9bf682 100644 --- a/common/src/main/java/ctbrec/recorder/Recorder.java +++ b/common/src/main/java/ctbrec/recorder/Recorder.java @@ -42,4 +42,18 @@ public interface Recorder { public List getOnlineModels(); public HttpClient getHttpClient(); + + /** + * Get the total size of the filesystem we are recording to + * @return the total size in bytes + * @throws IOException + */ + public long getTotalSpaceBytes() throws IOException; + + /** + * Get the free space left on the filesystem we are recording to + * @return the free space in bytes + * @throws IOException + */ + public long getFreeSpaceBytes() throws IOException; } diff --git a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java index dd648301..7c3ced0a 100644 --- a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -8,6 +8,7 @@ import java.time.Instant; import java.util.Collections; import java.util.List; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,6 +46,8 @@ public class RemoteRecorder implements Recorder { private List models = Collections.emptyList(); private List onlineModels = Collections.emptyList(); private List sites; + private long spaceTotal = -1; + private long spaceFree = -1; private Config config; private HttpClient client; @@ -150,10 +153,35 @@ public class RemoteRecorder implements Recorder { while(running) { syncModels(); syncOnlineModels(); + syncSpace(); sleep(); } } + private void syncSpace() { + try { + String msg = "{\"action\": \"space\"}"; + RequestBody body = RequestBody.create(JSON, msg); + Request.Builder builder = new Request.Builder() + .url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") + .post(body); + addHmacIfNeeded(msg, builder); + Request request = builder.build(); + try(Response response = client.execute(request)) { + String json = response.body().string(); + if(response.isSuccessful()) { + JSONObject resp = new JSONObject(json); + spaceTotal = resp.getLong("spaceTotal"); + spaceFree = resp.getLong("spaceFree"); + } else { + LOG.error("Couldn't synchronize with server. HTTP status: {} - {}", response.code(), json); + } + } + } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) { + LOG.error("Couldn't synchronize with server", e); + } + } + private void syncModels() { try { String msg = "{\"action\": \"list\"}"; @@ -362,4 +390,14 @@ public class RemoteRecorder implements Recorder { public HttpClient getHttpClient() { return client; } + + @Override + public long getTotalSpaceBytes() throws IOException { + return spaceTotal; + } + + @Override + public long getFreeSpaceBytes() { + return spaceFree; + } } diff --git a/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java b/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java index ad6f81dd..c213ce2f 100644 --- a/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java +++ b/server/src/main/java/ctbrec/recorder/server/RecorderServlet.java @@ -137,9 +137,13 @@ public class RecorderServlet extends AbstractCtbrecServlet { response = "{\"status\": \"success\", \"msg\": \"Recording resumed\"}"; resp.getWriter().write(response); break; + case "space": + response = "{\"status\": \"success\", \"spaceTotal\": "+recorder.getTotalSpaceBytes()+", \"spaceFree\": "+recorder.getFreeSpaceBytes()+"}"; + resp.getWriter().write(response); + break; default: resp.setStatus(SC_BAD_REQUEST); - response = "{\"status\": \"error\", \"msg\": \"Unknown action\"}"; + response = "{\"status\": \"error\", \"msg\": \"Unknown action ["+request.action+"]\"}"; resp.getWriter().write(response); break; } From 5708d7f259428e8917709602150196e69824260f Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 26 Nov 2018 14:03:40 +0100 Subject: [PATCH 045/231] Add display to show space left on device --- .../main/java/ctbrec/ui/RecordingsTab.java | 101 +++++++++++++----- 1 file changed, 77 insertions(+), 24 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index 2c7f4fb2..7efba008 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -8,6 +8,7 @@ import java.io.IOException; import java.net.URL; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.text.DecimalFormat; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; @@ -47,13 +48,16 @@ import javafx.scene.Cursor; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.ButtonType; import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; import javafx.scene.control.MenuItem; +import javafx.scene.control.ProgressBar; import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn.SortType; import javafx.scene.control.TableView; +import javafx.scene.control.Tooltip; import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.input.ContextMenuEvent; import javafx.scene.input.KeyCode; @@ -62,6 +66,9 @@ import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.text.Font; import javafx.stage.FileChooser; import javafx.util.Callback; import javafx.util.Duration; @@ -74,12 +81,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener { private Recorder recorder; @SuppressWarnings("unused") private List sites; + private long spaceTotal = -1; + private long spaceFree = -1; FlowPane grid = new FlowPane(); ScrollPane scrollPane = new ScrollPane(); TableView table = new TableView(); ObservableList observableRecordings = FXCollections.observableArrayList(); ContextMenu popup; + ProgressBar spaceLeft; + Label spaceLabel; public RecordingsTab(String title, Recorder recorder, Config config, List sites) { super(title); @@ -179,8 +190,21 @@ public class RecordingsTab extends Tab implements TabSelectionListener { }); scrollPane.setContent(table); + HBox spaceBox = new HBox(5); + Label l = new Label("Space left on device"); + HBox.setMargin(l, new Insets(2, 0, 0, 0)); + spaceBox.getChildren().add(l); + spaceLeft = new ProgressBar(0); + spaceLeft.setPrefSize(200, 22); + spaceLabel = new Label(); + spaceLabel.setFont(Font.font(11)); + StackPane stack = new StackPane(spaceLeft, spaceLabel); + spaceBox.getChildren().add(stack); + BorderPane.setMargin(spaceBox, new Insets(5)); + BorderPane root = new BorderPane(); root.setPadding(new Insets(5)); + root.setTop(spaceBox); root.setCenter(scrollPane); setContent(root); @@ -191,30 +215,8 @@ public class RecordingsTab extends Tab implements TabSelectionListener { updateService = createUpdateService(); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2))); updateService.setOnSucceeded((event) -> { - List recordings = updateService.getValue(); - if (recordings == null) { - return; - } - - for (Iterator iterator = observableRecordings.iterator(); iterator.hasNext();) { - JavaFxRecording old = iterator.next(); - if (!recordings.contains(old)) { - // remove deleted recordings - iterator.remove(); - } - } - for (JavaFxRecording recording : recordings) { - if (!observableRecordings.contains(recording)) { - // add new recordings - observableRecordings.add(recording); - } else { - // update existing ones - int index = observableRecordings.indexOf(recording); - JavaFxRecording old = observableRecordings.get(index); - old.update(recording); - } - } - table.sort(); + updateRecordingsTable(); + updateFreeSpaceDisplay(); }); updateService.setOnFailed((event) -> { LOG.info("Couldn't get list of recordings from recorder", event.getSource().getException()); @@ -226,6 +228,46 @@ public class RecordingsTab extends Tab implements TabSelectionListener { }); } + private void updateFreeSpaceDisplay() { + if(spaceTotal != -1 && spaceFree != -1) { + double free = ((double)spaceFree) / spaceTotal; + spaceLeft.setProgress(free); + double totalGiB = ((double) spaceTotal) / 1024 / 1024 / 1024; + double freeGiB = ((double) spaceFree) / 1024 / 1024 / 1024; + DecimalFormat df = new DecimalFormat("0.00"); + String tt = df.format(freeGiB) + " / " + df.format(totalGiB) + " GiB"; + spaceLeft.setTooltip(new Tooltip(tt)); + spaceLabel.setText(tt); + } + } + + private void updateRecordingsTable() { + List recordings = updateService.getValue(); + if (recordings == null) { + return; + } + + for (Iterator iterator = observableRecordings.iterator(); iterator.hasNext();) { + JavaFxRecording old = iterator.next(); + if (!recordings.contains(old)) { + // remove deleted recordings + iterator.remove(); + } + } + for (JavaFxRecording recording : recordings) { + if (!observableRecordings.contains(recording)) { + // add new recordings + observableRecordings.add(recording); + } else { + // update existing ones + int index = observableRecordings.indexOf(recording); + JavaFxRecording old = observableRecordings.get(index); + old.update(recording); + } + } + table.sort(); + } + private ScheduledService> createUpdateService() { ScheduledService> updateService = new ScheduledService>() { @Override @@ -233,12 +275,23 @@ public class RecordingsTab extends Tab implements TabSelectionListener { return new Task>() { @Override public List call() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { + updateSpace(); + List recordings = new ArrayList<>(); for (Recording rec : recorder.getRecordings()) { recordings.add(new JavaFxRecording(rec)); } return recordings; } + + private void updateSpace() { + try { + spaceTotal = recorder.getTotalSpaceBytes(); + spaceFree = recorder.getFreeSpaceBytes(); + } catch (IOException e) { + LOG.error("Couldn't update free space", e); + } + } }; } }; From cc2aa3c8d5f4ec6a65f1a1c85de0cecd352f79e4 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 26 Nov 2018 15:28:44 +0100 Subject: [PATCH 046/231] Add threshold setting for minimum space on disk If there is less space left on the device than specified by the setting, the recorder will stop all recordings and don't start new ones until the free space rises above this threshold again. --- common/src/main/java/ctbrec/Settings.java | 1 + .../java/ctbrec/recorder/LocalRecorder.java | 30 +++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index a34e24e9..3b613845 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -37,6 +37,7 @@ public class Settings { public String httpServer = "localhost"; public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec"; public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT; + public long minimumSpaceLeftInBytes = 0; public String mediaPlayer = "/usr/bin/mpv"; public String postProcessing = ""; public String username = ""; // chaturbate username TODO maybe rename this onetime diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index f4fb1f1f..14aca3a7 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -64,6 +64,7 @@ public class LocalRecorder implements Recorder { private List deleteInProgress = Collections.synchronizedList(new ArrayList<>()); private RecorderHttpClient client = new RecorderHttpClient(); private ReentrantLock lock = new ReentrantLock(); + private long lastSpaceMessage = 0; public LocalRecorder(Config config) { this.config = config; @@ -134,7 +135,6 @@ public class LocalRecorder implements Recorder { return; } - LOG.debug("Starting recording for model {}", model.getName()); if (recordingProcesses.containsKey(model)) { LOG.error("A recording for model {} is already running", model); return; @@ -150,6 +150,16 @@ public class LocalRecorder implements Recorder { lock.unlock(); } + if(!enoughSpaceForRecording()) { + long now = System.currentTimeMillis(); + if( (now - lastSpaceMessage) > TimeUnit.MINUTES.toMillis(1)) { + LOG.info("Not enough space for recording, not starting recording for {}", model); + lastSpaceMessage = now; + } + return; + } + + LOG.debug("Starting recording for model {}", model.getName()); Download download; if (Config.getInstance().isServerMode()) { download = new HlsDownload(client); @@ -330,6 +340,15 @@ public class LocalRecorder implements Recorder { public void run() { running = true; while (running) { + try { + if(!enoughSpaceForRecording() && !recordingProcesses.isEmpty()) { + LOG.info("No space left -> Stopping all recordings"); + stopRecordingProcesses(); + } + } catch (IOException e1) { + LOG.warn("Couldn't check free space left", e1); + } + List restart = new ArrayList<>(); for (Iterator> iterator = recordingProcesses.entrySet().iterator(); iterator.hasNext();) { Entry entry = iterator.next(); @@ -416,7 +435,7 @@ public class LocalRecorder implements Recorder { boolean isOnline = model.isOnline(IGNORE_CACHE); LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline")); if (isOnline && !isSuspended(model) && !recordingProcesses.containsKey(model)) { - LOG.info("Model {}'s room back to public. Starting recording", model); + LOG.info("Model {}'s room back to public", model); startRecordingProcess(model); } } catch (HttpException e) { @@ -758,8 +777,13 @@ public class LocalRecorder implements Recorder { } private FileStore getRecordingsFileStore() throws IOException { - File recordingsDir = new File(Config.getInstance().getSettings().recordingsDir); + File recordingsDir = new File(config.getSettings().recordingsDir); FileStore store = Files.getFileStore(recordingsDir.toPath()); return store; } + + private boolean enoughSpaceForRecording() throws IOException { + long minimum = config.getSettings().minimumSpaceLeftInBytes; + return getFreeSpaceBytes() > minimum; + } } From a7cc0882f6af347fb7541768bdff8c3518decf05 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 26 Nov 2018 15:45:12 +0100 Subject: [PATCH 047/231] Reorganize settings tab --- .../src/main/java/ctbrec/ui/SettingsTab.java | 178 +++++++++--------- 1 file changed, 88 insertions(+), 90 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java index 54091aa9..b5d74b22 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/SettingsTab.java @@ -120,7 +120,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { // left side leftSide.getChildren().add(createGeneralPanel()); - leftSide.getChildren().add(createLocationsPanel()); + leftSide.getChildren().add(createRecorderPanel()); leftSide.getChildren().add(createRecordLocationPanel()); //right side @@ -253,9 +253,20 @@ public class SettingsTab extends Tab implements TabSelectionListener { return recordLocation; } - private Node createLocationsPanel() { + private Node createRecorderPanel() { int row = 0; GridPane layout = createGridLayout(); + layout.add(new Label("Post-Processing"), 0, row); + postProcessing = new TextField(Config.getInstance().getSettings().postProcessing); + postProcessing.focusedProperty().addListener(createPostProcessingFocusListener()); + GridPane.setFillWidth(postProcessing, true); + GridPane.setHgrow(postProcessing, Priority.ALWAYS); + GridPane.setColumnSpan(postProcessing, 2); + GridPane.setMargin(postProcessing, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + layout.add(postProcessing, 1, row); + postProcessingDirectoryButton = createPostProcessingBrowseButton(); + layout.add(postProcessingDirectoryButton, 3, row++); + layout.add(new Label("Recordings Directory"), 0, row); recordingsDirectory = new TextField(Config.getInstance().getSettings().recordingsDir); recordingsDirectory.focusedProperty().addListener(createRecordingsDirectoryFocusListener()); @@ -283,16 +294,67 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(directoryStructure, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(directoryStructure, 1, row++); - layout.add(new Label("Post-Processing"), 0, row); - postProcessing = new TextField(Config.getInstance().getSettings().postProcessing); - postProcessing.focusedProperty().addListener(createPostProcessingFocusListener()); - GridPane.setFillWidth(postProcessing, true); - GridPane.setHgrow(postProcessing, Priority.ALWAYS); - GridPane.setColumnSpan(postProcessing, 2); - GridPane.setMargin(postProcessing, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(postProcessing, 1, row); - postProcessingDirectoryButton = createPostProcessingBrowseButton(); - layout.add(postProcessingDirectoryButton, 3, row++); + Label l = new Label("Maximum resolution (0 = unlimited)"); + layout.add(l, 0, row); + List resolutionOptions = new ArrayList<>(); + resolutionOptions.add(1080); + resolutionOptions.add(720); + resolutionOptions.add(600); + resolutionOptions.add(480); + resolutionOptions.add(0); + maxResolution = new ComboBox<>(FXCollections.observableList(resolutionOptions)); + setMaxResolutionValue(); + maxResolution.setOnAction((e) -> { + Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem(); + saveConfig(); + }); + maxResolution.prefWidthProperty().bind(directoryStructure.widthProperty()); + layout.add(maxResolution, 1, row++); + GridPane.setMargin(l, new Insets(0, 0, 0, 0)); + GridPane.setMargin(maxResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + + l = new Label("Split recordings after (minutes)"); + layout.add(l, 0, row); + List splitOptions = new ArrayList<>(); + splitOptions.add(new SplitAfterOption("disabled", 0)); + splitOptions.add(new SplitAfterOption("10 min", 10 * 60)); + splitOptions.add(new SplitAfterOption("15 min", 15 * 60)); + splitOptions.add(new SplitAfterOption("20 min", 20 * 60)); + splitOptions.add(new SplitAfterOption("30 min", 30 * 60)); + splitOptions.add(new SplitAfterOption("60 min", 60 * 60)); + splitAfter = new ComboBox<>(FXCollections.observableList(splitOptions)); + layout.add(splitAfter, 1, row++); + setSplitAfterValue(); + splitAfter.setOnAction((e) -> { + Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue(); + saveConfig(); + }); + splitAfter.prefWidthProperty().bind(directoryStructure.widthProperty()); + GridPane.setMargin(l, new Insets(0, 0, 0, 0)); + GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + + layout.add(new Label("Check online state every (seconds)"), 0, row); + onlineCheckIntervalInSecs = new TextField(Integer.toString(Config.getInstance().getSettings().onlineCheckIntervalInSecs)); + onlineCheckIntervalInSecs.textProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue.matches("\\d*")) { + onlineCheckIntervalInSecs.setText(newValue.replaceAll("[^\\d]", "")); + } + if(!onlineCheckIntervalInSecs.getText().isEmpty()) { + Config.getInstance().getSettings().onlineCheckIntervalInSecs = Integer.parseInt(onlineCheckIntervalInSecs.getText()); + saveConfig(); + } + }); + GridPane.setMargin(onlineCheckIntervalInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + layout.add(onlineCheckIntervalInSecs, 1, row++); + + TitledPane locations = new TitledPane("Recorder", layout); + locations.setCollapsible(false); + return locations; + } + + private Node createGeneralPanel() { + GridPane layout = createGridLayout(); + int row = 0; layout.add(new Label("Player"), 0, row); mediaPlayer = new TextField(Config.getInstance().getSettings().mediaPlayer); @@ -304,15 +366,18 @@ public class SettingsTab extends Tab implements TabSelectionListener { layout.add(mediaPlayer, 1, row); layout.add(createMpvBrowseButton(), 3, row++); - TitledPane locations = new TitledPane("Locations", layout); - locations.setCollapsible(false); - return locations; - } + Label l = new Label("Allow multiple players"); + layout.add(l, 0, row); + multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer); + multiplePlayers.setOnAction((e) -> { + Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected(); + saveConfig(); + }); + GridPane.setMargin(l, new Insets(3, 0, 0, 0)); + GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + layout.add(multiplePlayers, 1, row++); - private Node createGeneralPanel() { - GridPane layout = createGridLayout(); - int row = 0; - Label l = new Label("Display stream resolution in overview"); + l = new Label("Display stream resolution in overview"); layout.add(l, 0, row); loadResolution = new CheckBox(); loadResolution.setSelected(Config.getInstance().getSettings().determineResolution); @@ -323,20 +388,10 @@ public class SettingsTab extends Tab implements TabSelectionListener { ThumbOverviewTab.queue.clear(); } }); - //GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); - GridPane.setMargin(loadResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); + GridPane.setMargin(loadResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(loadResolution, 1, row++); - l = new Label("Allow multiple players"); - layout.add(l, 0, row); - multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer); - multiplePlayers.setOnAction((e) -> { - Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected(); - saveConfig(); - }); - GridPane.setMargin(l, new Insets(3, 0, 0, 0)); - GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - layout.add(multiplePlayers, 1, row++); l = new Label("Manually select stream quality"); layout.add(l, 0, row); @@ -357,60 +412,9 @@ public class SettingsTab extends Tab implements TabSelectionListener { saveConfig(); }); GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); - GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, CHECKBOX_MARGIN, CHECKBOX_MARGIN)); layout.add(updateThumbnails, 1, row++); - l = new Label("Maximum resolution (0 = unlimited)"); - layout.add(l, 0, row); - List resolutionOptions = new ArrayList<>(); - resolutionOptions.add(1080); - resolutionOptions.add(720); - resolutionOptions.add(600); - resolutionOptions.add(480); - resolutionOptions.add(0); - maxResolution = new ComboBox<>(FXCollections.observableList(resolutionOptions)); - setMaxResolutionValue(); - maxResolution.setOnAction((e) -> { - Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem(); - saveConfig(); - }); - layout.add(maxResolution, 1, row++); - GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); - GridPane.setMargin(maxResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - - l = new Label("Split recordings after (minutes)"); - layout.add(l, 0, row); - List options = new ArrayList<>(); - options.add(new SplitAfterOption("disabled", 0)); - options.add(new SplitAfterOption("10 min", 10 * 60)); - options.add(new SplitAfterOption("15 min", 15 * 60)); - options.add(new SplitAfterOption("20 min", 20 * 60)); - options.add(new SplitAfterOption("30 min", 30 * 60)); - options.add(new SplitAfterOption("60 min", 60 * 60)); - splitAfter = new ComboBox<>(FXCollections.observableList(options)); - layout.add(splitAfter, 1, row++); - setSplitAfterValue(); - splitAfter.setOnAction((e) -> { - Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue(); - saveConfig(); - }); - GridPane.setMargin(l, new Insets(0, 0, 0, 0)); - GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - - layout.add(new Label("Check online state every (seconds)"), 0, row); - onlineCheckIntervalInSecs = new TextField(Integer.toString(Config.getInstance().getSettings().onlineCheckIntervalInSecs)); - onlineCheckIntervalInSecs.textProperty().addListener((observable, oldValue, newValue) -> { - if (!newValue.matches("\\d*")) { - onlineCheckIntervalInSecs.setText(newValue.replaceAll("[^\\d]", "")); - } - if(!onlineCheckIntervalInSecs.getText().isEmpty()) { - Config.getInstance().getSettings().onlineCheckIntervalInSecs = Integer.parseInt(onlineCheckIntervalInSecs.getText()); - saveConfig(); - } - }); - GridPane.setMargin(onlineCheckIntervalInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(onlineCheckIntervalInSecs, 1, row++); - l = new Label("Start Tab"); layout.add(l, 0, row); startTab = new ComboBox<>(); @@ -429,12 +433,6 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(colorSettingsPane, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - - splitAfter.prefWidthProperty().bind(startTab.widthProperty()); - maxResolution.prefWidthProperty().bind(startTab.widthProperty()); - onlineCheckIntervalInSecs.prefWidthProperty().bind(startTab.widthProperty()); - onlineCheckIntervalInSecs.maxWidthProperty().bind(startTab.widthProperty()); - TitledPane general = new TitledPane("General", layout); general.setCollapsible(false); return general; From ba4ac952e2ba9844b7512ab4500c2d40c95f4018 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 26 Nov 2018 16:04:08 +0100 Subject: [PATCH 048/231] Add threshold for space left on device to SettingsTab --- .../src/main/java/ctbrec/ui/SettingsTab.java | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java index b5d74b22..e20f1e5b 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/SettingsTab.java @@ -55,6 +55,7 @@ import javafx.stage.FileChooser;; public class SettingsTab extends Tab implements TabSelectionListener { private static final transient Logger LOG = LoggerFactory.getLogger(SettingsTab.class); + private static final int ONE_GiB_IN_BYTES = 1024 * 1024 * 1024; public static final int CHECKBOX_MARGIN = 6; private TextField recordingsDirectory; @@ -65,6 +66,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private TextField server; private TextField port; private TextField onlineCheckIntervalInSecs; + private TextField leaveSpaceOnDevice; private CheckBox loadResolution; private CheckBox secureCommunication = new CheckBox(); private CheckBox chooseStreamQuality = new CheckBox(); @@ -333,8 +335,12 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(new Label("Check online state every (seconds)"), 0, row); + Tooltip tt = new Tooltip("Check every x seconds, if a model came online"); + l = new Label("Check online state every (seconds)"); + l.setTooltip(tt); + layout.add(l, 0, row); onlineCheckIntervalInSecs = new TextField(Integer.toString(Config.getInstance().getSettings().onlineCheckIntervalInSecs)); + onlineCheckIntervalInSecs.setTooltip(tt); onlineCheckIntervalInSecs.textProperty().addListener((observable, oldValue, newValue) -> { if (!newValue.matches("\\d*")) { onlineCheckIntervalInSecs.setText(newValue.replaceAll("[^\\d]", "")); @@ -347,6 +353,27 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(onlineCheckIntervalInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(onlineCheckIntervalInSecs, 1, row++); + tt = new Tooltip("Stop recording, if the free space on the device gets below this threshold"); + l = new Label("Leave space on device (GiB)"); + l.setTooltip(tt); + layout.add(l, 0, row); + long minimumSpaceLeftInBytes = Config.getInstance().getSettings().minimumSpaceLeftInBytes; + int minimumSpaceLeftInGiB = (int) (minimumSpaceLeftInBytes / ONE_GiB_IN_BYTES); + leaveSpaceOnDevice = new TextField(Integer.toString(minimumSpaceLeftInGiB)); + leaveSpaceOnDevice.setTooltip(tt); + leaveSpaceOnDevice.textProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue.matches("\\d*")) { + leaveSpaceOnDevice.setText(newValue.replaceAll("[^\\d]", "")); + } + if(!leaveSpaceOnDevice.getText().isEmpty()) { + long spaceLeftInGiB = Long.parseLong(leaveSpaceOnDevice.getText()); + Config.getInstance().getSettings().minimumSpaceLeftInBytes = spaceLeftInGiB * ONE_GiB_IN_BYTES; + saveConfig(); + } + }); + GridPane.setMargin(leaveSpaceOnDevice, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + layout.add(leaveSpaceOnDevice, 1, row++); + TitledPane locations = new TitledPane("Recorder", layout); locations.setCollapsible(false); return locations; @@ -479,6 +506,8 @@ public class SettingsTab extends Tab implements TabSelectionListener { postProcessing.setDisable(!local); postProcessingDirectoryButton.setDisable(!local); directoryStructure.setDisable(!local); + onlineCheckIntervalInSecs.setDisable(!local); + leaveSpaceOnDevice.setDisable(!local); } private ChangeListener createRecordingsDirectoryFocusListener() { From bf8a3c72400babb4b4473e340c9c33e0f0bc0143 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 27 Nov 2018 13:25:30 +0100 Subject: [PATCH 049/231] Fill start tab combobox only once --- client/src/main/java/ctbrec/ui/SettingsTab.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java index e20f1e5b..0d2722e3 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/SettingsTab.java @@ -680,9 +680,10 @@ public class SettingsTab extends Tab implements TabSelectionListener { @Override public void selected() { - startTab.getItems().clear(); - for(Tab tab : getTabPane().getTabs()) { - startTab.getItems().add(tab.getText()); + if(startTab.getItems().isEmpty()) { + for(Tab tab : getTabPane().getTabs()) { + startTab.getItems().add(tab.getText()); + } } String startTabName = Config.getInstance().getSettings().startTab; if(StringUtil.isNotBlank(startTabName)) { From 8826de38b2c51c2949c4e6d191c35b09f3812bc2 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 27 Nov 2018 13:36:00 +0100 Subject: [PATCH 050/231] Improve calculation of size property --- .../main/java/ctbrec/ui/JavaFxRecording.java | 11 +++--- .../main/java/ctbrec/ui/RecordingsTab.java | 34 +++++++++++++++++-- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/JavaFxRecording.java b/client/src/main/java/ctbrec/ui/JavaFxRecording.java index 313a7e75..af9f0241 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxRecording.java +++ b/client/src/main/java/ctbrec/ui/JavaFxRecording.java @@ -1,10 +1,11 @@ package ctbrec.ui; -import java.text.DecimalFormat; import java.time.Instant; import ctbrec.Config; import ctbrec.Recording; +import javafx.beans.property.LongProperty; +import javafx.beans.property.SimpleLongProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; @@ -12,7 +13,7 @@ public class JavaFxRecording extends Recording { private transient StringProperty statusProperty = new SimpleStringProperty(); private transient StringProperty progressProperty = new SimpleStringProperty(); - private transient StringProperty sizeProperty = new SimpleStringProperty(); + private transient LongProperty sizeProperty = new SimpleLongProperty(); private Recording delegate; @@ -89,9 +90,7 @@ public class JavaFxRecording extends Recording { @Override public void setSizeInByte(long sizeInByte) { delegate.setSizeInByte(sizeInByte); - double sizeInGiB = sizeInByte / 1024.0 / 1024 / 1024; - DecimalFormat df = new DecimalFormat("0.00"); - sizeProperty.setValue(df.format(sizeInGiB) + " GiB"); + sizeProperty.set(sizeInByte); } public StringProperty getProgressProperty() { @@ -151,7 +150,7 @@ public class JavaFxRecording extends Recording { return delegate.getSizeInByte(); } - public StringProperty getSizeProperty() { + public LongProperty getSizeProperty() { return sizeProperty; } diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index 7efba008..6581bcb5 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -147,9 +147,39 @@ public class RecordingsTab extends Tab implements TabSelectionListener { TableColumn progress = new TableColumn<>("Progress"); progress.setCellValueFactory((cdf) -> cdf.getValue().getProgressProperty()); progress.setPrefWidth(100); - TableColumn size = new TableColumn<>("Size"); - size.setCellValueFactory((cdf) -> cdf.getValue().getSizeProperty()); + TableColumn size = new TableColumn<>("Size"); + size.setStyle("-fx-alignment: CENTER-RIGHT;"); size.setPrefWidth(100); + size.setCellValueFactory(cdf -> cdf.getValue().getSizeProperty()); + size.setCellFactory(new Callback, TableCell>() { + @Override + public TableCell call(TableColumn param) { + TableCell cell = new TableCell() { + @Override + protected void updateItem(Number sizeInByte, boolean empty) { + if(empty || sizeInByte == null) { + setText(null); + } else { + DecimalFormat df = new DecimalFormat("0.00"); + String unit = "Bytes"; + double size = sizeInByte.doubleValue(); + if(size > 1024.0 * 1024 * 1024) { + size = size / 1024.0 / 1024 / 1024; + unit = "GiB"; + } else if(size > 1024.0 * 1024) { + size = size / 1024.0 / 1024; + unit = "MiB"; + } else if(size > 1024.0) { + size = size / 1024.0; + unit = "KiB"; + } + setText(df.format(size) + ' ' + unit); + } + } + }; + return cell; + } + }); table.getColumns().addAll(name, date, status, progress, size); table.setItems(observableRecordings); From 55b219d27122325d4ef265a823921299a9d2fa9c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 27 Nov 2018 14:11:52 +0100 Subject: [PATCH 051/231] Move size formatting code to StringUtil --- .../main/java/ctbrec/ui/RecordingsTab.java | 15 +-------------- common/src/main/java/ctbrec/StringUtil.java | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index 6581bcb5..b60d439b 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -160,20 +160,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { if(empty || sizeInByte == null) { setText(null); } else { - DecimalFormat df = new DecimalFormat("0.00"); - String unit = "Bytes"; - double size = sizeInByte.doubleValue(); - if(size > 1024.0 * 1024 * 1024) { - size = size / 1024.0 / 1024 / 1024; - unit = "GiB"; - } else if(size > 1024.0 * 1024) { - size = size / 1024.0 / 1024; - unit = "MiB"; - } else if(size > 1024.0) { - size = size / 1024.0; - unit = "KiB"; - } - setText(df.format(size) + ' ' + unit); + setText(StringUtil.formatSize(sizeInByte)); } } }; diff --git a/common/src/main/java/ctbrec/StringUtil.java b/common/src/main/java/ctbrec/StringUtil.java index 229e44b1..d9ae9796 100644 --- a/common/src/main/java/ctbrec/StringUtil.java +++ b/common/src/main/java/ctbrec/StringUtil.java @@ -1,5 +1,7 @@ package ctbrec; +import java.text.DecimalFormat; + public class StringUtil { public static boolean isBlank(String s) { return s == null || s.trim().isEmpty(); @@ -8,4 +10,21 @@ public class StringUtil { public static boolean isNotBlank(String s) { return !isBlank(s); } + + public static String formatSize(Number sizeInByte) { + DecimalFormat df = new DecimalFormat("0.00"); + String unit = "Bytes"; + double size = sizeInByte.doubleValue(); + if(size > 1024.0 * 1024 * 1024) { + size = size / 1024.0 / 1024 / 1024; + unit = "GiB"; + } else if(size > 1024.0 * 1024) { + size = size / 1024.0 / 1024; + unit = "MiB"; + } else if(size > 1024.0) { + size = size / 1024.0; + unit = "KiB"; + } + return df.format(size) + ' ' + unit; + } } From c543af64291f941814aaa7a93fd7153cc8721248 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 27 Nov 2018 17:53:37 +0100 Subject: [PATCH 052/231] Set online state to offline if model details cannot be loaded --- common/src/main/java/ctbrec/sites/cam4/Cam4Model.java | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java index 30b87e9a..8bcd8d8e 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java @@ -64,6 +64,7 @@ public class Cam4Model extends AbstractModel { if(response.isSuccessful()) { JSONArray json = new JSONArray(response.body().string()); if(json.length() == 0) { + onlineState = "offline"; throw new ModelDetailsEmptyException("Model details are empty"); } JSONObject details = json.getJSONObject(0); From e29634001684e3fea93bc5b942abf189c2f6e79c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 27 Nov 2018 17:54:43 +0100 Subject: [PATCH 053/231] Ignore log files --- client/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/.gitignore b/client/.gitignore index fc247909..2c4e8e27 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -2,7 +2,7 @@ /target/ *~ *.bak -/ctbrec.log +/*.log /ctbrec-tunnel.sh /jre/ /server-local.sh From 33642705a05121c3e7e3d3f0225463f55a9af9ad Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 27 Nov 2018 18:53:22 +0100 Subject: [PATCH 054/231] Check playlistUrl in isOnline If the playlistUrl is empty, we cannot record, so the model is offline --- common/src/main/java/ctbrec/sites/cam4/Cam4Model.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java index 8bcd8d8e..2afa1100 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java @@ -24,6 +24,7 @@ import com.iheartradio.m3u8.data.PlaylistData; import ctbrec.AbstractModel; import ctbrec.Config; +import ctbrec.StringUtil; import ctbrec.io.HtmlParser; import ctbrec.io.HttpException; import ctbrec.recorder.download.StreamSource; @@ -53,7 +54,7 @@ public class Cam4Model extends AbstractModel { return false; } } - return Objects.equals("NORMAL", onlineState); + return Objects.equals("NORMAL", onlineState) && StringUtil.isNotBlank(playlistUrl); } private void loadModelDetails() throws IOException, ModelDetailsEmptyException { From 7edc79b0e335267a37d516d0675bf61ba2d93efd Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 11:47:40 +0100 Subject: [PATCH 055/231] Take boolean privateRoom into account for online state --- common/src/main/java/ctbrec/sites/cam4/Cam4Model.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java index 2afa1100..68b24354 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java @@ -5,6 +5,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ExecutionException; import org.json.JSONArray; @@ -39,6 +40,7 @@ public class Cam4Model extends AbstractModel { private String playlistUrl; private String onlineState = "offline"; private int[] resolution = null; + private boolean privateRoom = false; @Override public boolean isOnline() throws IOException, ExecutionException, InterruptedException { @@ -54,7 +56,9 @@ public class Cam4Model extends AbstractModel { return false; } } - return Objects.equals("NORMAL", onlineState) && StringUtil.isNotBlank(playlistUrl); + return (Objects.equals("NORMAL", onlineState) || Objects.equals("GROUP_SHOW_SELLING_TICKETS", onlineState)) + && StringUtil.isNotBlank(playlistUrl) + && !privateRoom; } private void loadModelDetails() throws IOException, ModelDetailsEmptyException { @@ -71,6 +75,7 @@ public class Cam4Model extends AbstractModel { JSONObject details = json.getJSONObject(0); onlineState = details.getString("showType"); playlistUrl = details.getString("hlsPreviewUrl"); + privateRoom = details.getBoolean("privateRoom"); if(details.has("resolution")) { String res = details.getString("resolution"); String[] tokens = res.split(":"); @@ -106,7 +111,7 @@ public class Cam4Model extends AbstractModel { if (playlist.hasStreamInfo()) { StreamSource src = new StreamSource(); src.bandwidth = playlist.getStreamInfo().getBandwidth(); - src.height = playlist.getStreamInfo().getResolution().height; + src.height = Optional.ofNullable(playlist.getStreamInfo()).map(si -> si.getResolution()).map(res -> res.height).orElse(0); String masterUrl = getPlaylistUrl(); String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); String segmentUri = baseUrl + playlist.getUri(); From c4c8fe83fa44b9ccdb3506fdf3f1faeab15ee22a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 15:22:42 +0100 Subject: [PATCH 056/231] Improve MergedHlsDownload * Add better exception handling * Check, if the model is still online, when an error occurs * Download segments in parallel, so that less segments are missed --- .../recorder/download/MergedHlsDownload.java | 212 ++++++++++++------ .../download/MissingSegmentException.java | 11 + .../main/java/org/taktik/mpegts/Streamer.java | 31 ++- 3 files changed, 179 insertions(+), 75 deletions(-) create mode 100644 common/src/main/java/ctbrec/recorder/download/MissingSegmentException.java diff --git a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java index 84c4c7d6..d4a935e3 100644 --- a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java @@ -19,7 +19,16 @@ import java.text.DecimalFormat; import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; +import java.util.LinkedList; +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; import org.slf4j.LoggerFactory; @@ -53,6 +62,8 @@ public class MergedHlsDownload extends AbstractHlsDownload { private File targetFile; private DecimalFormat df = new DecimalFormat("00000"); private int splitCounter = 0; + private BlockingQueue downloadQueue = new LinkedBlockingQueue<>(50); + private ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue); public MergedHlsDownload(HttpClient client) { super(client); @@ -81,7 +92,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { downloadSegments(segmentPlaylistUri, false); LOG.debug("Waiting for merge thread to finish"); mergeThread.join(); - LOG.debug("Merge thread to finished"); + LOG.debug("Merge thread finished"); } catch(ParseException e) { throw new IOException("Couldn't parse stream information", e); } catch(PlaylistException e) { @@ -92,7 +103,12 @@ public class MergedHlsDownload extends AbstractHlsDownload { throw new IOException("Couldn't add HMAC to playlist url", e); } finally { alive = false; - streamer.stop(); + try { + streamer.stop(); + } catch(Exception e) { + LOG.error("Couldn't stop streamer", e); + } + downloadThreadPool.shutdown(); LOG.debug("Download terminated for {}", segmentPlaylistUri); } } @@ -129,7 +145,11 @@ public class MergedHlsDownload extends AbstractHlsDownload { } finally { alive = false; if(streamer != null) { - streamer.stop(); + try { + streamer.stop(); + } catch(Exception e) { + LOG.error("Couldn't stop streamer", e); + } } LOG.debug("Download for {} terminated", model); } @@ -138,36 +158,109 @@ public class MergedHlsDownload extends AbstractHlsDownload { private void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException { int lastSegment = 0; int nextSegment = 0; + long playlistNotFoundFirstEncounter = -1; while(running) { try { + if(playlistNotFoundFirstEncounter != -1) { + LOG.debug("Downloading playlist {}", segmentPlaylistUri); + } SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri); + playlistNotFoundFirstEncounter = -1; if(!livestreamDownload) { multiSource.setTotalSegments(lsp.segments.size()); } - // download segments, which might have been skipped - downloadMissedSegments(lsp, nextSegment); - // download new segments + long downloadStart = System.currentTimeMillis(); downloadNewSegments(lsp, nextSegment); + long downloadTookMillis = System.currentTimeMillis() - downloadStart; + + // download segments, which might have been skipped + //downloadMissedSegments(lsp, nextSegment); + if(nextSegment > 0 && lsp.seq > nextSegment) { + LOG.warn("Missed segments {} < {} in download for {}. Download took {}ms. Playlist is {}sec", nextSegment, lsp.seq, lsp.url, downloadTookMillis, lsp.totalDuration); + } if(livestreamDownload) { // split up the recording, if configured splitRecording(); // wait some time until requesting the segment playlist again to not hammer the server - waitForNewSegments(lsp, lastSegment); + waitForNewSegments(lsp, lastSegment, downloadTookMillis); lastSegment = lsp.seq; nextSegment = lastSegment + lsp.segments.size(); } else { break; } - } catch(HttpException e) { - if(e.getResponseCode() == 404) { - // playlist is gone -> model probably logged out - LOG.debug("Playlist not found. Assuming model went offline"); - running = false; + } catch(Exception e) { + LOG.info("Unexpected error while downloading ", model.getName()); + running = false; + } + } + } + + private void downloadNewSegments(SegmentPlaylist lsp, int nextSegment) throws MalformedURLException, MissingSegmentException, ExecutionException, HttpException { + int skip = nextSegment - lsp.seq; + if(lsp.segments.isEmpty()) { + LOG.debug("Empty playlist: {}", lsp.url); + } + + // add segments to download threadpool + Queue> downloads = new LinkedList<>(); + if(downloadQueue.remainingCapacity() == 0) { + LOG.warn("Download to slow for this stream. Download queue is full. Skipping segment"); + } else { + for (String segment : lsp.segments) { + if(!running) { + break; + } + if(skip > 0) { + skip--; + } else { + URL segmentUrl = new URL(segment); + Future download = downloadThreadPool.submit(new SegmentDownload(segmentUrl, client)); + downloads.add(download); + } + } + } + + // get completed downloads and write them to the file + writeFinishedSegments(downloads); + } + + private void writeFinishedSegments(Queue> downloads) throws ExecutionException, HttpException { + for (Future downloadFuture : downloads) { + try { + byte[] segmentData = downloadFuture.get(); + writeSegment(segmentData); + } catch (InterruptedException e) { + LOG.error("Error while downloading segment", e); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if(cause instanceof MissingSegmentException) { + if(model != null && !isModelOnline()) { + LOG.debug("Error while downloading segment, because model {} is offline. Stopping now", model.getName()); + running = false; + } else { + LOG.debug("Segment not available, but model {} still online. Going on", model.getName()); + } + } else if(cause instanceof HttpException) { + HttpException he = (HttpException) cause; + if(model != null && !isModelOnline()) { + LOG.debug("Error {} while downloading segment, because model {} is offline. Stopping now", he.getResponseCode(), model.getName()); + running = false; + } else { + if(he.getResponseCode() == 404) { + LOG.info("Playlist for {} not found [HTTP 404]. Stopping now", model.getName()); + running = false; + } else if(he.getResponseCode() == 403) { + LOG.info("Playlist for {} not accessible [HTTP 403]. Stopping now", model.getName()); + running = false; + } else { + throw he; + } + } } else { throw e; } @@ -175,43 +268,6 @@ public class MergedHlsDownload extends AbstractHlsDownload { } } - private void downloadMissedSegments(SegmentPlaylist lsp, int nextSegment) throws MalformedURLException { - if(nextSegment > 0 && lsp.seq > nextSegment) { - LOG.warn("Missed segments {} < {} in download for {}", nextSegment, lsp.seq, lsp.url); - String first = lsp.segments.get(0); - int seq = lsp.seq; - for (int i = nextSegment; i < lsp.seq; i++) { - URL segmentUrl = new URL(first.replaceAll(Integer.toString(seq), Integer.toString(i))); - LOG.debug("Loading missed segment {} for model {}", i, lsp.url); - byte[] segmentData; - try { - segmentData = new SegmentDownload(segmentUrl, client).call(); - writeSegment(segmentData); - } catch (Exception e) { - LOG.error("Error while downloading segment {}", segmentUrl, e); - } - } - // TODO switch to a lower bitrate/resolution ?!? - } - } - - private void downloadNewSegments(SegmentPlaylist lsp, int nextSegment) throws MalformedURLException { - int skip = nextSegment - lsp.seq; - for (String segment : lsp.segments) { - if(skip > 0) { - skip--; - } else { - URL segmentUrl = new URL(segment); - try { - byte[] segmentData = new SegmentDownload(segmentUrl, client).call(); - writeSegment(segmentData); - } catch (Exception e) { - LOG.error("Error while downloading segment {}", segmentUrl, e); - } - } - } - } - private void writeSegment(byte[] segmentData) throws InterruptedException { InputStream in = new ByteArrayInputStream(segmentData); InputStreamMTSSource source = InputStreamMTSSource.builder().setInputStream(in).build(); @@ -232,12 +288,18 @@ public class MergedHlsDownload extends AbstractHlsDownload { } } - private void waitForNewSegments(SegmentPlaylist lsp, int lastSegment) { + private void waitForNewSegments(SegmentPlaylist lsp, int lastSegment, long downloadTookMillis) { try { long wait = 0; if (lastSegment == lsp.seq) { - // playlist didn't change -> wait for at least half the target duration - wait = (long) lsp.targetDuration * 1000 / 2; + int timeLeftMillis = (int)(lsp.totalDuration * 1000 - downloadTookMillis); + if(timeLeftMillis < 3000) { // we have less than 3 seconds to get the new playlist and start downloading it + wait = 1; + } else { + // wait a second to be nice to the server (don't hammer it with requests) + // 1 second seems to be a good compromise. every other calculation resulted in more missing segments + wait = 1000; + } LOG.trace("Playlist didn't change... waiting for {}ms", wait); } else { // playlist did change -> wait for at least last segment duration @@ -256,7 +318,9 @@ public class MergedHlsDownload extends AbstractHlsDownload { public void stop() { running = false; alive = false; - streamer.stop(); + if(streamer != null) { + streamer.stop(); + } LOG.debug("Download stopped"); } @@ -281,6 +345,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { .setSink(sink) .setSleepingEnabled(liveStream) .setBufferSize(10) + .setName(model.getName()) .build(); // Start streaming @@ -295,9 +360,10 @@ public class MergedHlsDownload extends AbstractHlsDownload { } finally { closeFile(channel); deleteEmptyRecording(targetFile); + running = false; } }); - t.setName("Segment Merger Thread"); + t.setName("Segment Merger Thread [" + model.getName() + "]"); t.setDaemon(true); return t; } @@ -308,7 +374,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { Files.delete(targetFile.toPath()); Files.delete(targetFile.getParentFile().toPath()); } - } catch (IOException e) { + } catch (Exception e) { LOG.error("Error while deleting empty recording {}", targetFile); } } @@ -318,12 +384,12 @@ public class MergedHlsDownload extends AbstractHlsDownload { if (channel != null) { channel.close(); } - } catch (IOException e) { + } catch (Exception e) { LOG.error("Error while closing file channel", e); } } - private static class SegmentDownload implements Callable { + private class SegmentDownload implements Callable { private URL url; private HttpClient client; @@ -333,24 +399,38 @@ public class MergedHlsDownload extends AbstractHlsDownload { } @Override - public byte[] call() throws Exception { + public byte[] call() throws IOException { LOG.trace("Downloading segment " + url.getFile()); int maxTries = 3; - for (int i = 1; i <= maxTries; i++) { - try { - Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build(); - Response response = client.execute(request); - byte[] segment = response.body().bytes(); - return segment; + for (int i = 1; i <= maxTries && running; i++) { + Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build(); + try (Response response = client.execute(request)) { + if(response.isSuccessful()) { + byte[] segment = response.body().bytes(); + return segment; + } else { + throw new HttpException(response.code(), response.message()); + } } catch(Exception e) { if (i == maxTries) { LOG.warn("Error while downloading segment. Segment {} finally failed", url.getFile()); } else { - LOG.warn("Error while downloading segment {} on try {}", url.getFile(), i); + LOG.warn("Error while downloading segment {} on try {}", url.getFile(), i, e); + } + if(model != null && !isModelOnline()) { + break; } } } - throw new IOException("Unable to download segment " + url.getFile() + " after " + maxTries + " tries"); + throw new MissingSegmentException("Unable to download segment " + url.getFile() + " after " + maxTries + " tries"); + } + } + + public boolean isModelOnline() { + try { + return model.isOnline(IGNORE_CACHE); + } catch (IOException | ExecutionException | InterruptedException e) { + return false; } } } diff --git a/common/src/main/java/ctbrec/recorder/download/MissingSegmentException.java b/common/src/main/java/ctbrec/recorder/download/MissingSegmentException.java new file mode 100644 index 00000000..d6971aab --- /dev/null +++ b/common/src/main/java/ctbrec/recorder/download/MissingSegmentException.java @@ -0,0 +1,11 @@ +package ctbrec.recorder.download; + +import java.io.IOException; + +public class MissingSegmentException extends IOException { + + public MissingSegmentException(String msg) { + super(msg); + } + +} diff --git a/common/src/main/java/org/taktik/mpegts/Streamer.java b/common/src/main/java/org/taktik/mpegts/Streamer.java index d844da92..3ae5312d 100644 --- a/common/src/main/java/org/taktik/mpegts/Streamer.java +++ b/common/src/main/java/org/taktik/mpegts/Streamer.java @@ -30,12 +30,14 @@ public class Streamer { private Thread streamingThread; private boolean sleepingEnabled; + private String name; - private Streamer(MTSSource source, MTSSink sink, int bufferSize, boolean sleepingEnabled) { + private Streamer(MTSSource source, MTSSink sink, int bufferSize, boolean sleepingEnabled, String name) { this.source = source; this.sink = sink; this.bufferSize = bufferSize; this.sleepingEnabled = sleepingEnabled; + this.name = name; } public void stream() throws InterruptedException { @@ -48,15 +50,15 @@ public class Streamer { try { preBuffer(); } catch (Exception e) { - throw new IllegalStateException("Error while bufering", e); + throw new IllegalStateException("Error while buffering", e); } log.info("Done PreBuffering"); - bufferingThread = new Thread(this::fillBuffer, "buffering"); + bufferingThread = new Thread(this::fillBuffer, "Buffering ["+name+"]"); bufferingThread.setDaemon(true); bufferingThread.start(); - streamingThread = new Thread(this::internalStream, "streaming"); + streamingThread = new Thread(this::internalStream, "Streaming ["+name+"]"); streamingThread.setDaemon(true); streamingThread.start(); @@ -123,7 +125,7 @@ public class Streamer { } } } catch (InterruptedException e1) { - if(!endOfSourceReached) { + if(!endOfSourceReached && !streamingShouldStop) { log.error("Interrupted while waiting for packet"); continue; } else { @@ -240,7 +242,7 @@ public class Streamer { // Stream packet // System.out.println("Streaming packet #" + packetCount + ", PID=" + mtsPacket.getPid() + ", pcrCount=" + pcrCount + ", continuityCounter=" + mtsPacket.getContinuityCounter()); - if(!streamingShouldStop) { + if(!streamingShouldStop && !Thread.interrupted()) { try { sink.send(packet); } catch (Exception e) { @@ -275,7 +277,7 @@ public class Streamer { buffer.put(packet); put = true; } catch (InterruptedException ignored) { - + log.error("Error adding packet to buffer", ignored); } } } @@ -287,7 +289,11 @@ public class Streamer { log.error("Error reading from source", e); } finally { endOfSourceReached = true; - streamingThread.interrupt(); + try { + streamingThread.interrupt(); + } catch(Exception e) { + log.error("Couldn't interrupt streaming thread", e); + } } } @@ -308,6 +314,7 @@ public class Streamer { private MTSSource source; private int bufferSize = 1000; private boolean sleepingEnabled = false; + private String name; public StreamerBuilder setSink(MTSSink sink) { this.sink = sink; @@ -329,10 +336,16 @@ public class Streamer { return this; } + public StreamerBuilder setName(String name) { + this.name = name; + return this; + } + public Streamer build() { Preconditions.checkNotNull(sink); Preconditions.checkNotNull(source); - return new Streamer(source, sink, bufferSize, sleepingEnabled); + return new Streamer(source, sink, bufferSize, sleepingEnabled, name); } + } } \ No newline at end of file From e9909fe11aba3e2f9048781d080ec000785e0852 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 15:23:56 +0100 Subject: [PATCH 057/231] Add debug styling Display size cell red, if the size didn't change. This is only done when run in DEV mode. This makes it easier to debug freezing / hanging downloads --- client/src/main/java/ctbrec/ui/JavaFxRecording.java | 6 ++++++ client/src/main/java/ctbrec/ui/RecordingsTab.java | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/JavaFxRecording.java b/client/src/main/java/ctbrec/ui/JavaFxRecording.java index af9f0241..e44f36c0 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxRecording.java +++ b/client/src/main/java/ctbrec/ui/JavaFxRecording.java @@ -16,6 +16,7 @@ public class JavaFxRecording extends Recording { private transient LongProperty sizeProperty = new SimpleLongProperty(); private Recording delegate; + private long lastValue = 0; public JavaFxRecording(Recording recording) { this.delegate = recording; @@ -154,4 +155,9 @@ public class JavaFxRecording extends Recording { return sizeProperty; } + public boolean valueChanged() { + boolean changed = getSizeInByte() != lastValue; + lastValue = getSizeInByte(); + return changed; + } } diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index b60d439b..f8fef9dd 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -159,8 +159,19 @@ public class RecordingsTab extends Tab implements TabSelectionListener { protected void updateItem(Number sizeInByte, boolean empty) { if(empty || sizeInByte == null) { setText(null); + setStyle(null); } else { setText(StringUtil.formatSize(sizeInByte)); + if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) { + int row = this.getTableRow().getIndex(); + JavaFxRecording rec = tableViewProperty().get().getItems().get(row); + if(!rec.valueChanged() && rec.getStatus() == STATUS.RECORDING) { + setStyle("-fx-alignment: CENTER-RIGHT; -fx-background-color: red"); + } else { + setStyle("-fx-alignment: CENTER-RIGHT;"); + //setStyle(null); + } + } } } }; From 3a7f2ceca630a8a3c7561938db771475aeb5d5af Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 15:37:35 +0100 Subject: [PATCH 058/231] Add convenience method Config.isDevMode() Also made isServerMode() static --- client/src/main/java/ctbrec/ui/SettingsTab.java | 5 +++++ common/src/main/java/ctbrec/Config.java | 6 +++++- .../main/java/ctbrec/recorder/LocalRecorder.java | 14 +++++++------- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java index 0d2722e3..d1f816cc 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/SettingsTab.java @@ -319,6 +319,11 @@ public class SettingsTab extends Tab implements TabSelectionListener { layout.add(l, 0, row); List splitOptions = new ArrayList<>(); splitOptions.add(new SplitAfterOption("disabled", 0)); + if(Config.isDevMode()) { + splitOptions.add(new SplitAfterOption( "1 min", 1 * 60)); + splitOptions.add(new SplitAfterOption( "3 min", 3 * 60)); + splitOptions.add(new SplitAfterOption( "5 min", 5 * 60)); + } splitOptions.add(new SplitAfterOption("10 min", 10 * 60)); splitOptions.add(new SplitAfterOption("15 min", 15 * 60)); splitOptions.add(new SplitAfterOption("20 min", 20 * 60)); diff --git a/common/src/main/java/ctbrec/Config.java b/common/src/main/java/ctbrec/Config.java index 865f6bc1..20821ea8 100644 --- a/common/src/main/java/ctbrec/Config.java +++ b/common/src/main/java/ctbrec/Config.java @@ -100,10 +100,14 @@ public class Config { Files.write(configFile.toPath(), json.getBytes("utf-8"), CREATE, WRITE, TRUNCATE_EXISTING); } - public boolean isServerMode() { + public static boolean isServerMode() { return Objects.equals(System.getProperty("ctbrec.server.mode"), "1"); } + public static boolean isDevMode() { + return Objects.equals(System.getenv("CTBREC_DEV"), "1"); + } + public File getConfigDir() { return configDir; } diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 14aca3a7..82c5219c 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -83,7 +83,7 @@ public class LocalRecorder implements Recorder { onlineMonitor.start(); postProcessingTrigger = new PostProcessingTrigger(); - if(Config.getInstance().isServerMode()) { + if(Config.isServerMode()) { postProcessingTrigger.start(); } @@ -161,7 +161,7 @@ public class LocalRecorder implements Recorder { LOG.debug("Starting recording for model {}", model.getName()); Download download; - if (Config.getInstance().isServerMode()) { + if (Config.isServerMode()) { download = new HlsDownload(client); } else { download = new MergedHlsDownload(client); @@ -184,7 +184,7 @@ public class LocalRecorder implements Recorder { Download download = recordingProcesses.get(model); download.stop(); recordingProcesses.remove(model); - if(!Config.getInstance().isServerMode()) { + if(!Config.isServerMode()) { postprocess(download); } } @@ -358,7 +358,7 @@ public class LocalRecorder implements Recorder { LOG.debug("Recording terminated for model {}", m.getName()); iterator.remove(); restart.add(m); - if(config.isServerMode()) { + if(Config.isServerMode()) { try { finishRecording(d.getTarget()); } catch(Exception e) { @@ -385,7 +385,7 @@ public class LocalRecorder implements Recorder { } private void finishRecording(File directory) { - if(Config.getInstance().isServerMode()) { + if(Config.isServerMode()) { Thread t = new Thread() { @Override public void run() { @@ -513,7 +513,7 @@ public class LocalRecorder implements Recorder { @Override public List getRecordings() { - if(Config.getInstance().isServerMode()) { + if(Config.isServerMode()) { return listSegmentedRecordings(); } else { return listMergedRecordings(); @@ -558,7 +558,7 @@ public class LocalRecorder implements Recorder { return GENERATING_PLAYLIST; } - if (config.isServerMode()) { + if (Config.isServerMode()) { if (recording.hasPlaylist()) { return FINISHED; } else { From 403c1ed2d020b731c6b3ec7794242e18fdd01a27 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 16:03:21 +0100 Subject: [PATCH 059/231] Fix split recordings Split recordings didn't work, because splitRecStartTime had been removed by accident. Also the splitting now does not start a new recording, but switches the output file in Streamer. This is a much cleaner and smoother approach, because it is much faster and no segments are missed --- common/src/main/java/ctbrec/Config.java | 4 -- .../recorder/download/MergedHlsDownload.java | 41 +++++++++++-------- .../main/java/org/taktik/mpegts/Streamer.java | 16 ++++++++ 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/common/src/main/java/ctbrec/Config.java b/common/src/main/java/ctbrec/Config.java index 20821ea8..871c36ff 100644 --- a/common/src/main/java/ctbrec/Config.java +++ b/common/src/main/java/ctbrec/Config.java @@ -117,10 +117,6 @@ public class Config { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm"); String startTime = sdf.format(new Date()); File targetFile = new File(dirForRecording, model.getName() + '_' + startTime + ".ts"); - if(getSettings().splitRecordings > 0) { - LOG.debug("Splitting recordings every {} seconds", getSettings().splitRecordings); - targetFile = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-00000.ts")); - } return targetFile; } diff --git a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java index d4a935e3..c9cd0010 100644 --- a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java @@ -15,7 +15,6 @@ import java.nio.file.LinkOption; import java.nio.file.Path; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.text.DecimalFormat; import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; @@ -57,13 +56,12 @@ public class MergedHlsDownload extends AbstractHlsDownload { private BlockingMultiMTSSource multiSource; private Thread mergeThread; private Streamer streamer; - private ZonedDateTime startTime; + private ZonedDateTime splitRecStartTime; private Config config; private File targetFile; - private DecimalFormat df = new DecimalFormat("00000"); - private int splitCounter = 0; private BlockingQueue downloadQueue = new LinkedBlockingQueue<>(50); private ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue); + private FileChannel fileChannel = null; public MergedHlsDownload(HttpClient client) { super(client); @@ -78,6 +76,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { try { running = true; super.startTime = Instant.now(); + splitRecStartTime = ZonedDateTime.now(); mergeThread = createMergeThread(targetFile, progressListener, false); LOG.debug("Merge thread started"); mergeThread.start(); @@ -123,6 +122,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { running = true; super.startTime = Instant.now(); + splitRecStartTime = ZonedDateTime.now(); super.model = model; targetFile = Config.getInstance().getFileForRecording(model); String segments = getSegmentPlaylistUrl(model); @@ -130,6 +130,9 @@ public class MergedHlsDownload extends AbstractHlsDownload { mergeThread.start(); if(segments != null) { downloadSegments(segments, true); + if(config.getSettings().splitRecordings > 0) { + LOG.debug("Splitting recordings every {} seconds", config.getSettings().splitRecordings); + } } else { throw new IOException("Couldn't determine segments uri"); } @@ -194,7 +197,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { break; } } catch(Exception e) { - LOG.info("Unexpected error while downloading ", model.getName()); + LOG.info("Unexpected error while downloading {}", model.getName(), e); running = false; } } @@ -226,6 +229,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { } // get completed downloads and write them to the file + // TODO it might be a good idea to do this in a separate thread, so that the main download loop isn't blocked writeFinishedSegments(downloads); } @@ -276,14 +280,20 @@ public class MergedHlsDownload extends AbstractHlsDownload { private void splitRecording() { if(config.getSettings().splitRecordings > 0) { - Duration recordingDuration = Duration.between(startTime, ZonedDateTime.now()); + Duration recordingDuration = Duration.between(splitRecStartTime, ZonedDateTime.now()); long seconds = recordingDuration.getSeconds(); if(seconds >= config.getSettings().splitRecordings) { - streamer.stop(); - File target = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-"+df.format(++splitCounter)+".ts")); - mergeThread = createMergeThread(target, null, true); - mergeThread.start(); - startTime = ZonedDateTime.now(); + try { + targetFile = Config.getInstance().getFileForRecording(model); + LOG.debug("Switching to file {}", targetFile.getAbsolutePath()); + fileChannel = FileChannel.open(targetFile.toPath(), CREATE, WRITE); + MTSSink sink = ByteChannelSink.builder().setByteChannel(fileChannel).build(); + streamer.switchSink(sink); + splitRecStartTime = ZonedDateTime.now(); + } catch (IOException e) { + LOG.error("Error while splitting recording", e); + running = false; + } } } } @@ -331,14 +341,13 @@ public class MergedHlsDownload extends AbstractHlsDownload { .setProgressListener(listener) .build(); - FileChannel channel = null; try { Path downloadDir = targetFile.getParentFile().toPath(); if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) { Files.createDirectories(downloadDir); } - channel = FileChannel.open(targetFile.toPath(), CREATE, WRITE); - MTSSink sink = ByteChannelSink.builder().setByteChannel(channel).build(); + fileChannel = FileChannel.open(targetFile.toPath(), CREATE, WRITE); + MTSSink sink = ByteChannelSink.builder().setByteChannel(fileChannel).build(); streamer = Streamer.builder() .setSource(multiSource) @@ -358,9 +367,9 @@ public class MergedHlsDownload extends AbstractHlsDownload { } catch(Exception e) { LOG.error("Error while saving stream to file", e); } finally { - closeFile(channel); deleteEmptyRecording(targetFile); running = false; + closeFile(fileChannel); } }); t.setName("Segment Merger Thread [" + model.getName() + "]"); @@ -381,7 +390,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { private void closeFile(FileChannel channel) { try { - if (channel != null) { + if (channel != null && channel.isOpen()) { channel.close(); } } catch (Exception e) { diff --git a/common/src/main/java/org/taktik/mpegts/Streamer.java b/common/src/main/java/org/taktik/mpegts/Streamer.java index 3ae5312d..560bbfc8 100644 --- a/common/src/main/java/org/taktik/mpegts/Streamer.java +++ b/common/src/main/java/org/taktik/mpegts/Streamer.java @@ -64,6 +64,12 @@ public class Streamer { bufferingThread.join(); streamingThread.join(); + + try { + sink.close(); + } catch(Exception e) { + log.error("Couldn't close sink", e); + } } public void stop() { @@ -87,6 +93,16 @@ public class Streamer { } } + public void switchSink(MTSSink sink) { + MTSSink old = this.sink; + this.sink = sink; + try { + old.close(); + } catch (Exception e) { + log.error("Couldn't close old sink while switching sinks", e); + } + } + private void internalStream() { boolean resetState = false; MTSPacket packet = null; From ef9566999a5894863bcd8fb5945e1dd4e3507164 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 16:43:30 +0100 Subject: [PATCH 060/231] Fixed possible NPE in update method --- common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index f0776843..45f13c08 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -221,7 +221,7 @@ public class MyFreeCamsModel extends AbstractModel { setName(state.getNm()); setState(State.of(state.getVs())); setStreamUrl(streamUrl); - Optional camScore = Optional.of(state.getM()).map(m -> m.getCamscore()); + Optional camScore = Optional.ofNullable(state.getM()).map(m -> m.getCamscore()); setCamScore(camScore.orElse(0.0)); // preview From cbb6f3f45a9709ee9a0dd4b7816185eff0ceb922 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 16:45:42 +0100 Subject: [PATCH 061/231] Add failFast version of getStreamInfo for faster startup With many chaturbate models, the loading of the recording tab took a long time, because for each model the online state was loaded by the loading cache. The failFast version just returns null and makes the inital loading of recorder.getOnlineModels() much faster. --- .../main/java/ctbrec/sites/chaturbate/Chaturbate.java | 10 +++++++++- .../java/ctbrec/sites/chaturbate/ChaturbateModel.java | 11 +++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java index 36125f85..534a39bb 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java @@ -219,7 +219,15 @@ public class Chaturbate extends AbstractSite { } StreamInfo getStreamInfo(String modelName) throws IOException, ExecutionException { - return streamInfoCache.get(modelName); + return getStreamInfo(modelName, false); + } + + StreamInfo getStreamInfo(String modelName, boolean failFast) throws IOException, ExecutionException { + if(failFast) { + return streamInfoCache.getIfPresent(modelName); + } else { + return streamInfoCache.get(modelName); + } } StreamInfo loadStreamInfo(String modelName) throws HttpException, IOException, InterruptedException { diff --git a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java index 3095c4be..bd17cd23 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ExecutionException; import org.slf4j.Logger; @@ -39,14 +40,16 @@ public class ChaturbateModel extends AbstractModel { @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { - StreamInfo info; + String roomStatus; if(ignoreCache) { - info = getChaturbate().loadStreamInfo(getName()); + StreamInfo info = getChaturbate().loadStreamInfo(getName()); + roomStatus = Optional.ofNullable(info).map(i -> i.room_status).orElse(""); LOG.trace("Model {} room status: {}", getName(), info.room_status); } else { - info = getChaturbate().getStreamInfo(getName()); + StreamInfo info = getChaturbate().getStreamInfo(getName(), true); + roomStatus = Optional.ofNullable(info).map(i -> i.room_status).orElse(""); } - return Objects.equals("public", info.room_status); + return Objects.equals("public", roomStatus); } @Override From ab49ac414ab0e220ba057aa9a0f26fd5af2c0ec9 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 17:37:23 +0100 Subject: [PATCH 062/231] Update changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff02c63e..c956f655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +1.12.0 +======================== +* Added threshold setting to keep free space on the recording device. + This is useful, if you don't want to use up all of your storage. + The free space is also shown on the recordings tab +* Tweaked the download internals a lot. Downloads should not hang + in RECORDING state without actually recording. Downloads should + be more robust in general. +* Fixed and improved split recordings +* Improved detection of online state for Cam4 models +* Accelerated the initial loading of the "Recording" tab for many + Chaturbate models +* Recordings tab now shows smaller size units (Bytes, KiB, MiB, GiB) + 1.11.0 ======================== * Added model search function From b7711456125578224f11635dfa6c5de97ed73c70 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 17:52:30 +0100 Subject: [PATCH 063/231] Bump version to 1.12.0 --- client/pom.xml | 2 +- common/pom.xml | 2 +- master/pom.xml | 2 +- server/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index c0e4bb57..021c748b 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.11.0 + 1.12.0 ../master diff --git a/common/pom.xml b/common/pom.xml index 54db7db8..cb1e5152 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.11.0 + 1.12.0 ../master diff --git a/master/pom.xml b/master/pom.xml index 8d905ac5..3c43cdae 100644 --- a/master/pom.xml +++ b/master/pom.xml @@ -6,7 +6,7 @@ ctbrec master pom - 1.11.0 + 1.12.0 ../common diff --git a/server/pom.xml b/server/pom.xml index 873084e8..69969b61 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.11.0 + 1.12.0 ../master From 52fe184cea7ff5d71061e02b50bcea1663a92566 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 17:57:43 +0100 Subject: [PATCH 064/231] Update download links to 1.12.0 --- docs/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.html b/docs/index.html index 705ac05b..951aae21 100644 --- a/docs/index.html +++ b/docs/index.html @@ -118,19 +118,19 @@
    - + Download for Linux! From ede6dd73d20213c9a57d60258da2c0cb1977ca60 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 20:30:45 +0100 Subject: [PATCH 065/231] Don't print stacktrace on timeout in OnlineMonitor --- common/src/main/java/ctbrec/recorder/LocalRecorder.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 82c5219c..e6df866b 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -5,6 +5,7 @@ import static ctbrec.Recording.STATUS.*; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; +import java.net.SocketTimeoutException; import java.nio.file.FileStore; import java.nio.file.Files; import java.security.InvalidKeyException; @@ -441,6 +442,8 @@ public class LocalRecorder implements Recorder { } catch (HttpException e) { LOG.error("Couldn't check if model {} is online. HTTP Response: {} - {}", model.getName(), e.getResponseCode(), e.getResponseMessage()); + } catch (SocketTimeoutException e) { + LOG.error("Couldn't check if model {} is online. Request timed out", model.getName()); } catch (Exception e) { LOG.error("Couldn't check if model {} is online", model.getName(), e); } From c0bd89b2289703cc059d21d32ff97c2feb600325 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 20:35:23 +0100 Subject: [PATCH 066/231] Fix log message --- common/src/main/java/ctbrec/recorder/download/HlsDownload.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java index c9f4f174..c2720b16 100644 --- a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java @@ -171,7 +171,7 @@ public class HlsDownload extends AbstractHlsDownload { break; } catch(Exception e) { if (i == maxTries) { - LOG.warn("Error while downloading segment. Segment finally {} failed", file.toFile().getName()); + LOG.warn("Error while downloading segment. Segment {} finally failed", file.toFile().getName()); } else { LOG.warn("Error while downloading segment on try {}", i); } From 1e51298f417253e159e11c3990fb3fe6f670aefc Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 23:06:41 +0100 Subject: [PATCH 067/231] Fix recording download from server --- .../recorder/download/MergedHlsDownload.java | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java index c9cd0010..e876af62 100644 --- a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java @@ -19,6 +19,7 @@ import java.time.Duration; import java.time.Instant; 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; @@ -175,7 +176,11 @@ public class MergedHlsDownload extends AbstractHlsDownload { // download new segments long downloadStart = System.currentTimeMillis(); - downloadNewSegments(lsp, nextSegment); + if(livestreamDownload) { + downloadNewSegments(lsp, nextSegment); + } else { + downloadRecording(lsp); + } long downloadTookMillis = System.currentTimeMillis() - downloadStart; // download segments, which might have been skipped @@ -197,12 +202,25 @@ public class MergedHlsDownload extends AbstractHlsDownload { break; } } catch(Exception e) { - LOG.info("Unexpected error while downloading {}", model.getName(), e); + if(model != null) { + LOG.info("Unexpected error while downloading {}", model.getName(), e); + } else { + LOG.info("Unexpected error while downloading", e); + } running = false; } } } + private void downloadRecording(SegmentPlaylist lsp) throws IOException, InterruptedException { + for (String segment : lsp.segments) { + URL segmentUrl = new URL(segment); + SegmentDownload segmentDownload = new SegmentDownload(segmentUrl, client); + byte[] segmentData = segmentDownload.call(); + writeSegment(segmentData); + } + } + private void downloadNewSegments(SegmentPlaylist lsp, int nextSegment) throws MalformedURLException, MissingSegmentException, ExecutionException, HttpException { int skip = nextSegment - lsp.seq; if(lsp.segments.isEmpty()) { @@ -354,7 +372,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { .setSink(sink) .setSleepingEnabled(liveStream) .setBufferSize(10) - .setName(model.getName()) + .setName(Optional.ofNullable(model).map(m -> m.getName()).orElse("")) .build(); // Start streaming @@ -372,7 +390,11 @@ public class MergedHlsDownload extends AbstractHlsDownload { closeFile(fileChannel); } }); - t.setName("Segment Merger Thread [" + model.getName() + "]"); + if(model != null) { + t.setName("Segment Merger Thread [" + model.getName() + "]"); + } else { + t.setName("Segment Merger Thread"); + } t.setDaemon(true); return t; } From 4150a2911bb8a7b7652b1ee8b26729426a001575 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 23:24:06 +0100 Subject: [PATCH 068/231] Playing around with notifications --- .../java/ctbrec/ui/CamrecApplication.java | 50 ++- .../main/java/ctbrec/ui/ThumbOverviewTab.java | 3 +- .../src/main/java/ctbrec/ui/TokenLabel.java | 3 +- .../enzo/notification/Notification.java | 369 ++++++++++++++++++ .../eu/hansolo/enzo/notification/README.md | 4 + .../eu/hansolo/enzo/notification/error.png | Bin 0 -> 704 bytes .../eu/hansolo/enzo/notification/info.png | Bin 0 -> 810 bytes .../eu/hansolo/enzo/notification/license.txt | 202 ++++++++++ .../eu/hansolo/enzo/notification/notifier.css | 40 ++ .../eu/hansolo/enzo/notification/success.png | Bin 0 -> 839 bytes .../eu/hansolo/enzo/notification/warning.png | Bin 0 -> 623 bytes client/src/test/java/AudioTest.java | 19 + client/src/test/java/HlsTest.java | 44 +++ client/src/test/java/MediaControl.java | 355 +++++++++++++++++ client/src/test/java/pausebutton.png | Bin 0 -> 107 bytes client/src/test/java/playbutton.png | Bin 0 -> 131 bytes .../src/main/java/ctbrec/EventBusHolder.java | 10 + .../java/ctbrec/recorder/RemoteRecorder.java | 24 ++ 18 files changed, 1116 insertions(+), 7 deletions(-) create mode 100644 client/src/main/java/eu/hansolo/enzo/notification/Notification.java create mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/README.md create mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/error.png create mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/info.png create mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/license.txt create mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/notifier.css create mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/success.png create mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/warning.png create mode 100644 client/src/test/java/AudioTest.java create mode 100644 client/src/test/java/HlsTest.java create mode 100644 client/src/test/java/MediaControl.java create mode 100644 client/src/test/java/pausebutton.png create mode 100644 client/src/test/java/playbutton.png create mode 100644 common/src/main/java/ctbrec/EventBusHolder.java diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 72c0259d..08f07836 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -10,19 +10,21 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Objects; -import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.eventbus.AsyncEventBus; -import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import ctbrec.Config; +import ctbrec.EventBusHolder; +import ctbrec.Model; import ctbrec.StringUtil; import ctbrec.Version; import ctbrec.io.HttpClient; @@ -35,6 +37,7 @@ import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.mfc.MyFreeCams; +import eu.hansolo.enzo.notification.Notification; import javafx.application.Application; import javafx.application.HostServices; import javafx.application.Platform; @@ -45,6 +48,7 @@ import javafx.scene.control.Alert; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.image.Image; +import javafx.scene.media.AudioClip; import javafx.scene.paint.Color; import javafx.stage.Stage; import okhttp3.Request; @@ -54,17 +58,19 @@ public class CamrecApplication extends Application { static final transient Logger LOG = LoggerFactory.getLogger(CamrecApplication.class); + private Stage primaryStage; private Config config; private Recorder recorder; static HostServices hostServices; private SettingsTab settingsTab; private TabPane rootPane = new TabPane(); - static EventBus bus; private List sites = new ArrayList<>(); public static HttpClient httpClient; + private Notification.Notifier notifier; @Override public void start(Stage primaryStage) throws Exception { + this.primaryStage = primaryStage; logEnvironment(); sites.add(new BongaCams()); sites.add(new Cam4()); @@ -73,7 +79,6 @@ public class CamrecApplication extends Application { sites.add(new MyFreeCams()); loadConfig(); createHttpClient(); - bus = new AsyncEventBus(Executors.newSingleThreadExecutor()); hostServices = getHostServices(); createRecorder(); for (Site site : sites) { @@ -88,6 +93,14 @@ public class CamrecApplication extends Application { } createGui(primaryStage); checkForUpdates(); + new Thread(() -> { + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(10)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + Platform.runLater(() -> registerAlertSystem()); + }).start(); } private void logEnvironment() { @@ -197,6 +210,33 @@ public class CamrecApplication extends Application { }); } + private void registerAlertSystem() { + Notification.Notifier.setNotificationOwner(primaryStage); + notifier = Notification.Notifier.INSTANCE; + EventBusHolder.BUS.register(new Object() { + @Subscribe + public void modelEvent(Map e) { + try { + if (Objects.equals("model.status", e.get("event"))) { + String status = (String) e.get("status"); + Model model = (Model) e.get("model"); + LOG.debug("Alert: {} is {}", model.getName(), status); + if (Objects.equals("online", status)) { + Platform.runLater(() -> { + AudioClip clip = new AudioClip("file:///tmp/Oxygen-Im-Highlight-Msg.mp3"); + clip.play(); + Notification notification = new Notification("Model Online", model.getName() + " is now online"); + notifier.notify(notification); + }); + } + } + } catch (Exception e1) { + e1.printStackTrace(); + } + } + }); + } + private void writeColorSchemeStyleSheet(Stage primaryStage) { File colorCss = new File(Config.getInstance().getConfigDir(), "color.css"); try(FileOutputStream fos = new FileOutputStream(colorCss)) { diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index 618b0bcd..4c6fb8f9 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -25,6 +25,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Config; +import ctbrec.EventBusHolder; import ctbrec.Model; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; @@ -468,7 +469,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { Map event = new HashMap<>(); event.put("event", "tokens.sent"); event.put("amount", tokens); - CamrecApplication.bus.post(event); + EventBusHolder.BUS.post(event); } catch (Exception e1) { showError("Couldn't send tip", "An error occured while sending tip:", e1); } diff --git a/client/src/main/java/ctbrec/ui/TokenLabel.java b/client/src/main/java/ctbrec/ui/TokenLabel.java index d19b16d0..c24c40a9 100644 --- a/client/src/main/java/ctbrec/ui/TokenLabel.java +++ b/client/src/main/java/ctbrec/ui/TokenLabel.java @@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory; import com.google.common.eventbus.Subscribe; +import ctbrec.EventBusHolder; import ctbrec.sites.Site; import javafx.application.Platform; import javafx.concurrent.Task; @@ -24,7 +25,7 @@ public class TokenLabel extends Label { public TokenLabel(Site site) { this.site = site; setText("Tokens: loading…"); - CamrecApplication.bus.register(new Object() { + EventBusHolder.BUS.register(new Object() { @Subscribe public void tokensUpdates(Map e) { if (Objects.equals("tokens", e.get("event"))) { diff --git a/client/src/main/java/eu/hansolo/enzo/notification/Notification.java b/client/src/main/java/eu/hansolo/enzo/notification/Notification.java new file mode 100644 index 00000000..074fc295 --- /dev/null +++ b/client/src/main/java/eu/hansolo/enzo/notification/Notification.java @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2013 by Gerrit Grunwald + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.hansolo.enzo.notification; + +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.Popup; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.util.Duration; + + +/** + * Created by + * User: hansolo + * Date: 01.07.13 + * Time: 07:10 + */ +public class Notification { + public static final Image INFO_ICON = new Image(Notifier.class.getResourceAsStream("info.png")); + public static final Image WARNING_ICON = new Image(Notifier.class.getResourceAsStream("warning.png")); + public static final Image SUCCESS_ICON = new Image(Notifier.class.getResourceAsStream("success.png")); + public static final Image ERROR_ICON = new Image(Notifier.class.getResourceAsStream("error.png")); + public final String TITLE; + public final String MESSAGE; + public final Image IMAGE; + + + // ******************** Constructors ************************************** + public Notification(final String TITLE, final String MESSAGE) { + this(TITLE, MESSAGE, null); + } + public Notification(final String MESSAGE, final Image IMAGE) { + this("", MESSAGE, IMAGE); + } + public Notification(final String TITLE, final String MESSAGE, final Image IMAGE) { + this.TITLE = TITLE; + this.MESSAGE = MESSAGE; + this.IMAGE = IMAGE; + } + + + // ******************** Inner Classes ************************************* + public enum Notifier { + INSTANCE; + + private static final double ICON_WIDTH = 24; + private static final double ICON_HEIGHT = 24; + private static double width = 300; + private static double height = 80; + private static double offsetX = 0; + private static double offsetY = 25; + private static double spacingY = 5; + private static Pos popupLocation = Pos.TOP_RIGHT; + private static Stage stageRef = null; + private Duration popupLifetime; + private Stage stage; + private Scene scene; + private ObservableList popups; + + + // ******************** Constructor *************************************** + private Notifier() { + init(); + initGraphics(); + } + + + // ******************** Initialization ************************************ + private void init() { + popupLifetime = Duration.millis(5000); + popups = FXCollections.observableArrayList(); + } + + private void initGraphics() { + scene = new Scene(new Region()); + scene.setFill(null); + scene.getStylesheets().add(getClass().getResource("notifier.css").toExternalForm()); + + stage = new Stage(); + stage.initStyle(StageStyle.TRANSPARENT); + stage.setScene(scene); + } + + + // ******************** Methods ******************************************* + /** + * @param STAGE_REF The Notification will be positioned relative to the given Stage.
    + * If null then the Notification will be positioned relative to the primary Screen. + * @param POPUP_LOCATION The default is TOP_RIGHT of primary Screen. + */ + public static void setPopupLocation(final Stage STAGE_REF, final Pos POPUP_LOCATION) { + if (null != STAGE_REF) { + INSTANCE.stage.initOwner(STAGE_REF); + Notifier.stageRef = STAGE_REF; + } + Notifier.popupLocation = POPUP_LOCATION; + } + + /** + * Sets the Notification's owner stage so that when the owner + * stage is closed Notifications will be shut down as well.
    + * This is only needed if setPopupLocation is called + * without a stage reference. + * @param OWNER + */ + public static void setNotificationOwner(final Stage OWNER) { + INSTANCE.stage.initOwner(OWNER); + } + + /** + * @param OFFSET_X The horizontal shift required. + *
    The default is 0 px. + */ + public static void setOffsetX(final double OFFSET_X) { + Notifier.offsetX = OFFSET_X; + } + + /** + * @param OFFSET_Y The vertical shift required. + *
    The default is 25 px. + */ + public static void setOffsetY(final double OFFSET_Y) { + Notifier.offsetY = OFFSET_Y; + } + + /** + * @param WIDTH The default is 300 px. + */ + public static void setWidth(final double WIDTH) { + Notifier.width = WIDTH; + } + + /** + * @param HEIGHT The default is 80 px. + */ + public static void setHeight(final double HEIGHT) { + Notifier.height = HEIGHT; + } + + /** + * @param SPACING_Y The spacing between multiple Notifications. + *
    The default is 5 px. + */ + public static void setSpacingY(final double SPACING_Y) { + Notifier.spacingY = SPACING_Y; + } + + public void stop() { + popups.clear(); + stage.close(); + } + + /** + * Returns the Duration that the notification will stay on screen before it + * will fade out. + * @return the Duration the popup notification will stay on screen + */ + public Duration getPopupLifetime() { + return popupLifetime; + } + + /** + * Defines the Duration that the popup notification will stay on screen before it + * will fade out. The parameter is limited to values between 2 and 20 seconds. + * @param POPUP_LIFETIME + */ + public void setPopupLifetime(final Duration POPUP_LIFETIME) { + popupLifetime = Duration.millis(clamp(2000, 20000, POPUP_LIFETIME.toMillis())); + } + + /** + * Show the given Notification on the screen + * @param NOTIFICATION + */ + public void notify(final Notification NOTIFICATION) { + preOrder(); + showPopup(NOTIFICATION); + } + + /** + * Show a Notification with the given parameters on the screen + * @param TITLE + * @param MESSAGE + * @param IMAGE + */ + public void notify(final String TITLE, final String MESSAGE, final Image IMAGE) { + notify(new Notification(TITLE, MESSAGE, IMAGE)); + } + + /** + * Show a Notification with the given title and message and an Info icon + * @param TITLE + * @param MESSAGE + */ + public void notifyInfo(final String TITLE, final String MESSAGE) { + notify(new Notification(TITLE, MESSAGE, Notification.INFO_ICON)); + } + + /** + * Show a Notification with the given title and message and a Warning icon + * @param TITLE + * @param MESSAGE + */ + public void notifyWarning(final String TITLE, final String MESSAGE) { + notify(new Notification(TITLE, MESSAGE, Notification.WARNING_ICON)); + } + + /** + * Show a Notification with the given title and message and a Checkmark icon + * @param TITLE + * @param MESSAGE + */ + public void notifySuccess(final String TITLE, final String MESSAGE) { + notify(new Notification(TITLE, MESSAGE, Notification.SUCCESS_ICON)); + } + + /** + * Show a Notification with the given title and message and an Error icon + * @param TITLE + * @param MESSAGE + */ + public void notifyError(final String TITLE, final String MESSAGE) { + notify(new Notification(TITLE, MESSAGE, Notification.ERROR_ICON)); + } + + /** + * Makes sure that the given VALUE is within the range of MIN to MAX + * @param MIN + * @param MAX + * @param VALUE + * @return + */ + private double clamp(final double MIN, final double MAX, final double VALUE) { + if (VALUE < MIN) return MIN; + if (VALUE > MAX) return MAX; + return VALUE; + } + + /** + * Reorder the popup Notifications on screen so that the latest Notification will stay on top + */ + private void preOrder() { + if (popups.isEmpty()) return; + for (int i = 0 ; i < popups.size() ; i++) { + switch (popupLocation) { + case TOP_LEFT: case TOP_CENTER: case TOP_RIGHT: popups.get(i).setY(popups.get(i).getY() + height + spacingY); break; + default: popups.get( i ).setY( popups.get( i ).getY() - height - spacingY); + } + } + } + + /** + * Creates and shows a popup with the data from the given Notification object + * @param NOTIFICATION + */ + private void showPopup(final Notification NOTIFICATION) { + Label title = new Label(NOTIFICATION.TITLE); + title.getStyleClass().add("title"); + + ImageView icon = new ImageView(NOTIFICATION.IMAGE); + icon.setFitWidth(ICON_WIDTH); + icon.setFitHeight(ICON_HEIGHT); + + Label message = new Label(NOTIFICATION.MESSAGE, icon); + message.getStyleClass().add("message"); + + VBox popupLayout = new VBox(); + popupLayout.setSpacing(10); + popupLayout.setPadding(new Insets(10, 10, 10, 10)); + popupLayout.getChildren().addAll(title, message); + + StackPane popupContent = new StackPane(); + popupContent.setPrefSize(width, height); + popupContent.getStyleClass().add("notification"); + popupContent.getChildren().addAll(popupLayout); + + final Popup POPUP = new Popup(); + POPUP.setX( getX() ); + POPUP.setY( getY() ); + System.out.println(POPUP.getX() + "," + POPUP.getY()); + POPUP.getContent().add(popupContent); + + popups.add(POPUP); + + // Add a timeline for popup fade out + KeyValue fadeOutBegin = new KeyValue(POPUP.opacityProperty(), 1.0); + KeyValue fadeOutEnd = new KeyValue(POPUP.opacityProperty(), 0.0); + + KeyFrame kfBegin = new KeyFrame(Duration.ZERO, fadeOutBegin); + KeyFrame kfEnd = new KeyFrame(Duration.millis(500), fadeOutEnd); + + Timeline timeline = new Timeline(kfBegin, kfEnd); + timeline.setDelay(popupLifetime); + timeline.setOnFinished(actionEvent -> Platform.runLater(() -> { + POPUP.hide(); + popups.remove(POPUP); + })); + + // Move popup to the right during fade out + //POPUP.opacityProperty().addListener((observableValue, oldOpacity, opacity) -> popup.setX(popup.getX() + (1.0 - opacity.doubleValue()) * popup.getWidth()) ); + + if (stage.isShowing()) { + stage.toFront(); + } else { + stage.show(); + } + + POPUP.show(stage); + timeline.play(); + } + + private double getX() { + if (null == stageRef) return calcX( 0.0, Screen.getPrimary().getBounds().getWidth() ); + + return calcX(stageRef.getX(), stageRef.getWidth()); + } + private double getY() { + if (null == stageRef) return calcY( 0.0, Screen.getPrimary().getBounds().getHeight() ); + + return calcY(stageRef.getY(), stageRef.getHeight()); + } + + private double calcX(final double LEFT, final double TOTAL_WIDTH) { + switch (popupLocation) { + case TOP_LEFT : case CENTER_LEFT : case BOTTOM_LEFT : return LEFT + offsetX; + case TOP_CENTER: case CENTER : case BOTTOM_CENTER: return LEFT + (TOTAL_WIDTH - width) * 0.5 - offsetX; + case TOP_RIGHT : case CENTER_RIGHT: case BOTTOM_RIGHT : return LEFT + TOTAL_WIDTH - width - offsetX; + default: return 0.0; + } + } + private double calcY(final double TOP, final double TOTAL_HEIGHT ) { + switch (popupLocation) { + case TOP_LEFT : case TOP_CENTER : case TOP_RIGHT : return TOP + offsetY; + case CENTER_LEFT: case CENTER : case CENTER_RIGHT: return TOP + (TOTAL_HEIGHT- height)/2 - offsetY; + case BOTTOM_LEFT: case BOTTOM_CENTER: case BOTTOM_RIGHT: return TOP + TOTAL_HEIGHT - height - offsetY; + default: return 0.0; + } + } + } +} diff --git a/client/src/main/resources/eu/hansolo/enzo/notification/README.md b/client/src/main/resources/eu/hansolo/enzo/notification/README.md new file mode 100644 index 00000000..849c6e92 --- /dev/null +++ b/client/src/main/resources/eu/hansolo/enzo/notification/README.md @@ -0,0 +1,4 @@ +Enzo +==== + +A repo that contains custom controls for JavaFX 8 (current version is hosted on bitbucket) diff --git a/client/src/main/resources/eu/hansolo/enzo/notification/error.png b/client/src/main/resources/eu/hansolo/enzo/notification/error.png new file mode 100644 index 0000000000000000000000000000000000000000..f0651ec351db0a270c295195802c436869650cb2 GIT binary patch literal 704 zcmV;x0zdtUP)P000;W1^@s654Bdt00004b3#c}2nYxW zdP}K-Mc%gai#jxU)FM;%3j;$J^wJoDM9Ho%zrIKX2dc>;?`o6&k<| z;5u*#xB%3E9`G4>2fP5Dhn#MbLzPfLUIXp|_kpvA6=L=scmO;OIlUuAR5%B$16Q2; z7w`($27UskfEnN_aMIyk1B)T&n-_@%nkuwaP`h#!?x;|&*m2$Bxyxy*&`gS`FrmVZ z1Fu$*SBR~;+#MArQX#~i;n1`3cD8DEeK8EOYC=fD;Ddh zFqQ#xK*Qosq9@RGRhQjV}NzlCjCd{ ztvI8ZM9w5IZnr-&;Dd`_P6%vKK7(77&*){`-Vx8o40sEJF?~A%H()SYiZggdxf$xf zi70Q;)-FX8sIcbxdMet@&@ZF;r&Q<~-`db)rE1KiaH+B4D;Vv7G(bG+SB`zu8+ari mt$*_Q{Xahvk=U{P68!-uhjovBUk&#F0000P000;W1^@s654Bdt00004b3#c}2nYxW zd4ig;WsWT#P=-jJl|dS-K&M`Y0k`G` z%sDgDqfWBC58JT~>oI^{yucm&h9B^qkWO}kXti2;D!_Yj0AFEQZ6byIi(@z=q%Yn~ zlI8uljEyDh6a0kRc!)(J+k~C9F91run&?LIEcLw=89%B)a3momSZVCowCwwj^Ik1?TxsRghNq3$3aKd zwQ5N6s2?lJw_8%D74{N8N{^Sy)?Iyk0?YSkV{l#$1ZkxLZL|##x(`GI}PYkGc@-K)<42 z7Dwy<_`Px;1FQim)wM5yy|bUb@qzjB_i&uZmKG&nkk3OXEvOWcltIlATdc;(8*a z6BvueU9x=T6!+py*;=xU;cHBEg)icFwS8M7gA?t8KN)o&PCGhYzMVh9+|v9a#)Ncq z8bJ|v8gt6H1xZM6pb;0gN|LM;dQZ_0llUH+g|wlYB*}4|T{j|b?Nxsc2j0b1|DGSM oPT&kbza63({zyzEdnaF_mll`s5^Y~IWdHyG07*qoM6N<$f{XfRrvLx| literal 0 HcmV?d00001 diff --git a/client/src/main/resources/eu/hansolo/enzo/notification/license.txt b/client/src/main/resources/eu/hansolo/enzo/notification/license.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/client/src/main/resources/eu/hansolo/enzo/notification/license.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/client/src/main/resources/eu/hansolo/enzo/notification/notifier.css b/client/src/main/resources/eu/hansolo/enzo/notification/notifier.css new file mode 100644 index 00000000..723117bc --- /dev/null +++ b/client/src/main/resources/eu/hansolo/enzo/notification/notifier.css @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2013 by Gerrit Grunwald + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.root { + -fx-background-color: transparent; + -fx-fill : transparent; +} +.notification { + -fx-background-color : -fx-background; + -fx-background-radius: 5; + -fx-effect : innershadow(two-pass-box, rgba(255, 255, 255, 0.6), 5.0, 0.25, 0, 0); + -foreground-color : -fx-base; + -icon-color : -fx-base; +} + +.notification .title { + -fx-font-size : 1.083333em; + -fx-font-weight: bold; + -fx-text-fill : -foreground-color; +} +.notification .message { + -fx-font-size : 1.0em; + -fx-content-display : left; + -fx-graphic-text-gap: 10; + -fx-text-fill : -foreground-color; +} + diff --git a/client/src/main/resources/eu/hansolo/enzo/notification/success.png b/client/src/main/resources/eu/hansolo/enzo/notification/success.png new file mode 100644 index 0000000000000000000000000000000000000000..472804af55afc0b647b833e12992f5efd7cd3f0a GIT binary patch literal 839 zcmV-N1GxN&P)P000;W1^@s654Bdt00004b3#c}2nYxW zdJNR7i=v*3YY!MHC0{&pS8rk`$Jk2%_8sl0FawDG`W~iFdMvBwRPCoAMv1u$Gz7 z&W#!bx3*?rIz|vgn=qk}CJPC}lFFdeexO_l-?zncUU|6P_q}(Kp3RwaX3qC}<~(y| zo}Mw1OEZaQ@eCfr3QXWS&fz4E;e(Rv?0gu5!9Z^W_yk_WYq)n@A|b!xbsQ|YUSE(T zm*zek!s8wHU-%f`;4<#U!*~jJb=G}`ttHoAMv~;xJcv*6K)BAuZr(4sUYjSOCD;*r+L$J5xx_cq; zp+q9vb7?N%mB87Dosc4vN3bJej`#chR~Jjrm43g!8^^=k5x6Z0UJ88;yFT1}daxMK zT$|I6Pb@^R>FBB+-p00YeTyecu78feS)H_v1y@S0f6U2v zb1u!{D2+`=SM^d#`?aNj@9}iW^|x+Z;9Lpwo@9jN(mbK9`4+sBOEZyD+N-^8;AgBa zx&AbM@9pWj9OI?AG|M`3XS7WP)A$-MhkZfY)#pbfxC~3fyp$q%C~JpP000;W1^@s654Bdt00004b3#c}2nYxW zd-V>?93NPk=`)k%dJvbS%{5D9M>W@VH302Swo&~&OGP*&tYcnZFM>wyR9~~TCFT| zis1+*@wTk$B|W%}MS%Ns8eFK4BOOc#kn$!f#Y%Re#CFbilL3!eqA=2Nihr!48au6W=6VzuM7PVTUazwbNipofM~rTh{1xwT-M^oiZY|?X zS=B$;cqWhf0AReV>M4x(@r~l)+M<9I!*M+BlQ^iRQw+z}A$WM>vZ~u_g(s|DyW!scf%OZEG?VPMvj6cr(O=Ovc^M;Tyk`Ia002ov JPDHLkV1h)#3WNXv literal 0 HcmV?d00001 diff --git a/client/src/test/java/AudioTest.java b/client/src/test/java/AudioTest.java new file mode 100644 index 00000000..78a9ab5e --- /dev/null +++ b/client/src/test/java/AudioTest.java @@ -0,0 +1,19 @@ +import javafx.application.Application; +import javafx.application.Platform; +import javafx.scene.media.AudioClip; +import javafx.stage.Stage; + +public class AudioTest extends Application { + + @Override + public void start(Stage primaryStage) throws Exception { + AudioClip clip = new AudioClip("file:///tmp/Oxygen-Im-Highlight-Msg.mp3"); + clip.cycleCountProperty().set(3); + clip.play(); + Platform.exit(); + } + + public static void main(String[] args) { + launch(args); + } +} diff --git a/client/src/test/java/HlsTest.java b/client/src/test/java/HlsTest.java new file mode 100644 index 00000000..d9c06e87 --- /dev/null +++ b/client/src/test/java/HlsTest.java @@ -0,0 +1,44 @@ +import javafx.application.Application; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; +import javafx.stage.Stage; + +public class HlsTest extends Application { + // media = new Media("http://localhost:3202/hls/sun_shine_baby/2018-11-28_20-43/playlist.m3u8"); + + private static final String MEDIA_URL = "http://localhost:3202/hls/sun_shine_baby/2018-11-28_20-43/playlist.m3u8"; + + private Media media; + private MediaPlayer mediaPlayer; + private MediaControl mediaControl; + + public Parent createContent() { + media = new Media(MEDIA_URL); + mediaPlayer = new MediaPlayer(media); + mediaPlayer.setOnError(()-> { + mediaPlayer.getError().printStackTrace(System.err); + }); + mediaControl = new MediaControl(mediaPlayer); + mediaControl.setMinSize(480, 280); + mediaControl.setPrefSize(480, 280); + mediaControl.setMaxSize(480, 280); + return mediaControl; + } + + @Override + public void start(Stage primaryStage) throws Exception { + primaryStage.setScene(new Scene(createContent())); + primaryStage.show(); + } + + @Override + public void stop() { + mediaPlayer.stop(); + } + + public static void main(String[] args) { + launch(args); + } +} diff --git a/client/src/test/java/MediaControl.java b/client/src/test/java/MediaControl.java new file mode 100644 index 00000000..55c0e0bb --- /dev/null +++ b/client/src/test/java/MediaControl.java @@ -0,0 +1,355 @@ +import javafx.application.Platform; +import javafx.beans.Observable; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.Slider; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.media.MediaPlayer; +import javafx.scene.media.MediaView; +import javafx.stage.Stage; +import javafx.util.Duration; + +public class MediaControl extends BorderPane { + + private MediaPlayer mp; + private MediaView mediaView; + private final boolean repeat = false; + private boolean stopRequested = false; + private boolean atEndOfMedia = false; + private Duration duration; + private Slider timeSlider; + private Label playTime; + private Slider volumeSlider; + private HBox mediaBar; + private Pane mvPane; + private Stage newStage; + private boolean fullScreen = false; + + @Override + protected void layoutChildren() { + if (mediaView != null && getBottom() != null) { + mediaView.setFitWidth(getWidth()); + mediaView.setFitHeight(getHeight() - getBottom().prefHeight(-1)); + } + super.layoutChildren(); + if (mediaView != null && getCenter() != null) { + mediaView.setTranslateX((((Pane) getCenter()).getWidth() - mediaView.prefWidth(-1)) / 2); + mediaView.setTranslateY((((Pane) getCenter()).getHeight() - mediaView.prefHeight(-1)) / 2); + } + } + + @Override + protected double computeMinWidth(double height) { + return mediaBar.prefWidth(-1); + } + + @Override + protected double computeMinHeight(double width) { + return 200; + } + + @Override + protected double computePrefWidth(double height) { + return Math.max(mp.getMedia().getWidth(), mediaBar.prefWidth(height)); + } + + @Override + protected double computePrefHeight(double width) { + return mp.getMedia().getHeight() + mediaBar.prefHeight(width); + } + + @Override + protected double computeMaxWidth(double height) { + return Double.MAX_VALUE; + } + + @Override + protected double computeMaxHeight(double width) { + return Double.MAX_VALUE; + } + + public MediaControl(final MediaPlayer mp) { + this.mp = mp; + setStyle("-fx-background-color: #bfc2c7;"); // TODO: Use css file + mediaView = new MediaView(mp); + mvPane = new Pane(); + mvPane.getChildren().add(mediaView); + mvPane.setStyle("-fx-background-color: black;"); // TODO: Use css file + setCenter(mvPane); + mediaBar = new HBox(5.0); + mediaBar.setPadding(new Insets(5, 10, 5, 10)); + mediaBar.setAlignment(Pos.CENTER_LEFT); + BorderPane.setAlignment(mediaBar, Pos.CENTER); + + final Button playButton = new Button(); + playButton.setMinWidth(Control.USE_PREF_SIZE); + + String PLAY = "playbutton.png"; + String PAUSE = "pausebutton.png"; + Image PlayButton = new Image(getClass().getResourceAsStream(PLAY)); + Image PauseButton = new Image(getClass().getResourceAsStream(PAUSE)); + ImageView imageViewPlay = new ImageView(PlayButton); + ImageView imageViewPause = new ImageView(PauseButton); + playButton.setGraphic(imageViewPlay); + playButton.setOnAction((ActionEvent e) -> { + updateValues(); + MediaPlayer.Status status = mp.getStatus(); + if (status == MediaPlayer.Status.UNKNOWN || status == MediaPlayer.Status.HALTED) { + // don't do anything in these states + return; + } + + if (status == MediaPlayer.Status.PAUSED || status == MediaPlayer.Status.READY || status == MediaPlayer.Status.STOPPED) { + // rewind the movie if we're sitting at the end + if (atEndOfMedia) { + mp.seek(mp.getStartTime()); + atEndOfMedia = false; + playButton.setGraphic(imageViewPlay); + // playButton.setText(">"); + updateValues(); + } + mp.play(); + playButton.setGraphic(imageViewPause); + // playButton.setText("||"); + } else { + mp.pause(); + } + }); + ReadOnlyObjectProperty time = mp.currentTimeProperty(); + time.addListener((ObservableValue observable, Duration oldValue, Duration newValue) -> { + //updateValues(); + }); + mp.setOnPlaying(() -> { + if (stopRequested) { + mp.pause(); + stopRequested = false; + } else { + playButton.setGraphic(imageViewPause); + // playButton.setText("||"); + } + }); + mp.setOnPaused(() -> { + playButton.setGraphic(imageViewPlay); + // playButton.setText("||"); + }); + mp.setOnReady(() -> { + duration = mp.getMedia().getDuration(); + updateValues(); + }); + + mp.setCycleCount(repeat ? MediaPlayer.INDEFINITE : 1); + mp.setOnEndOfMedia(() -> { + if (!repeat) { + playButton.setGraphic(imageViewPlay); + // playButton.setText(">"); + stopRequested = true; + atEndOfMedia = true; + } + }); + mediaBar.getChildren().add(playButton); + + // Time label + Label timeLabel = new Label("Time"); + timeLabel.setMinWidth(Control.USE_PREF_SIZE); + mediaBar.getChildren().add(timeLabel); + + // Time slider + timeSlider = new Slider(); + timeSlider.setMinWidth(30); + timeSlider.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(timeSlider, Priority.ALWAYS); + + DoubleProperty timeValue = timeSlider.valueProperty(); + timeValue.addListener((ObservableValue observable, Number old, Number now) -> { + if (timeSlider.isValueChanging()) { + // multiply duration by percentage calculated by slider position + if (duration != null) { + System.out.println(timeSlider.getValue() + "%"); + mp.seek(duration.multiply(timeSlider.getValue() / 100.0)); + } + updateValues(); + } else if (Math.abs(now.doubleValue() - old.doubleValue()) > 1.5) { + // multiply duration by percentage calculated by slider position + System.out.println(timeSlider.getValue() + "%"); + if (duration != null) { + mp.seek(duration.multiply(timeSlider.getValue() / 100.0)); + } + } + }); + mediaBar.getChildren().add(timeSlider); + + // Play label + playTime = new Label(); + playTime.setMinWidth(Control.USE_PREF_SIZE); + + mediaBar.getChildren().add(playTime); + + // Fullscreen button + Button buttonFullScreen = new Button("Full Screen"); + buttonFullScreen.setMinWidth(Control.USE_PREF_SIZE); + + buttonFullScreen.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent event) { + if (!fullScreen) { + newStage = new Stage(); + ReadOnlyBooleanProperty full = newStage.fullScreenProperty(); + full.addListener((ObservableValue ov, Boolean old, Boolean now) -> { + onFullScreen(); + }); + final BorderPane borderPane = new BorderPane() { + @Override + protected void layoutChildren() { + if (mediaView != null && getBottom() != null) { + mediaView.setFitWidth(getWidth()); + double height = getHeight() - getBottom().prefHeight(-1); + mediaView.setFitHeight(height); + } + super.layoutChildren(); + if (mediaView != null) { + final Pane center = (Pane) getCenter(); + if (center != null) { // if smaller pane has content + double width = center.getWidth() - mediaView.prefWidth(-1); + double height = center.getHeight() - mediaView.prefHeight(-1); + double xval = width / 2.0; + double yval = height / 2.0; + + mediaView.setTranslateX(xval); + mediaView.setTranslateY(yval); + } + } + } + }; + + setCenter(null); + setBottom(null); + borderPane.setCenter(mvPane); + borderPane.setBottom(mediaBar); + + Scene newScene = new Scene(borderPane); + newStage.setScene(newScene); + // Workaround for disposing stage when exit fullscreen + newStage.setX(-100000); + newStage.setY(-100000); + + newStage.setFullScreen(true); + fullScreen = true; + newStage.show(); + + } else { + // toggle FullScreen + fullScreen = false; + newStage.setFullScreen(false); + + } + } + }); + mediaBar.getChildren().add(buttonFullScreen); + + // Volume label + Label volumeLabel = new Label("Vol"); + volumeLabel.setMinWidth(Control.USE_PREF_SIZE); + mediaBar.getChildren().add(volumeLabel); + + // Volume slider + volumeSlider = new Slider(); + volumeSlider.setPrefWidth(70); + volumeSlider.setMinWidth(30); + volumeSlider.setMaxWidth(Region.USE_PREF_SIZE); + volumeSlider.valueProperty().addListener((Observable ov) -> { + }); + + final DoubleProperty volume = volumeSlider.valueProperty(); + volume.addListener((ObservableValue observable, Number old, Number now) -> { + mp.setVolume(volumeSlider.getValue() / 100.0); + }); + mediaBar.getChildren().add(volumeSlider); + + setBottom(mediaBar); + + } + + protected void onFullScreen() { + if (!newStage.isFullScreen()) { + + fullScreen = false; + BorderPane smallBP = (BorderPane) newStage.getScene().getRoot(); + smallBP.setCenter(null); + setCenter(mvPane); + + smallBP.setBottom(null); + setBottom(mediaBar); + Platform.runLater(() -> { + newStage.close(); + }); + + } + } + + protected void updateValues() { + if (playTime != null && timeSlider != null && volumeSlider != null && duration != null) { + Platform.runLater(() -> { + Duration now = mp.getCurrentTime(); + playTime.setText(formatTime(now, duration)); + timeSlider.setDisable(duration.isUnknown()); + if (!timeSlider.isDisabled() && duration.greaterThan(Duration.ZERO) && !timeSlider.isValueChanging()) { + final double value = now.divide(duration).toMillis() * 100.0; + timeSlider.setValue(value); + } + if (!volumeSlider.isValueChanging()) { + final int value = (int) Math.round(mp.getVolume() * 100); + volumeSlider.setValue(value); + } + }); + } + } + + private String formatTime(Duration elapsed, Duration duration) { + int intElapsed = (int) Math.floor(elapsed.toSeconds()); + int elapsedHours = intElapsed / (60 * 60); + if (elapsedHours > 0) { + intElapsed -= elapsedHours * 60 * 60; + } + int elapsedMinutes = intElapsed / 60; + int elapsedSeconds = intElapsed - elapsedHours * 60 * 60 - elapsedMinutes * 60; + + if (duration.greaterThan(Duration.ZERO)) { + int intDuration = (int) Math.floor(duration.toSeconds()); + int durationHours = intDuration / (60 * 60); + if (durationHours > 0) { + intDuration -= durationHours * 60 * 60; + } + int durationMinutes = intDuration / 60; + int durationSeconds = intDuration - durationHours * 60 * 60 - durationMinutes * 60; + + if (durationHours > 0) { + return String.format("%d:%02d:%02d/%d:%02d:%02d", elapsedHours, elapsedMinutes, elapsedSeconds, durationHours, durationMinutes, + durationSeconds); + } else { + return String.format("%02d:%02d/%02d:%02d", elapsedMinutes, elapsedSeconds, durationMinutes, durationSeconds); + } + } else { + if (elapsedHours > 0) { + return String.format("%d:%02d:%02d", elapsedHours, elapsedMinutes, elapsedSeconds); + } else { + return String.format("%02d:%02d", elapsedMinutes, elapsedSeconds); + } + } + } +} \ No newline at end of file diff --git a/client/src/test/java/pausebutton.png b/client/src/test/java/pausebutton.png new file mode 100644 index 0000000000000000000000000000000000000000..4e429238f487ef59389dfcb4253db0dea852eee6 GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^;y^6G!3HG%>OYYHQbwLGjv*Ddk`pBQ9@q=1x%OEc z=9jfOYYHQeK`ejv*Ddk`pBQ9@vXqKK8-6 zo$v4Rsj3Nw6KC9-wm`x!*L#DMpRV@@slGJMmu=UtUKKoIVmRkf!ot>t5xmQ*f|j%{ gEIIkFS(<^NtY>|`;_-dAfu=Hey85}Sb4q9e0H2015&!@I literal 0 HcmV?d00001 diff --git a/common/src/main/java/ctbrec/EventBusHolder.java b/common/src/main/java/ctbrec/EventBusHolder.java new file mode 100644 index 00000000..aefe499b --- /dev/null +++ b/common/src/main/java/ctbrec/EventBusHolder.java @@ -0,0 +1,10 @@ +package ctbrec; + +import java.util.concurrent.Executors; + +import com.google.common.eventbus.AsyncEventBus; +import com.google.common.eventbus.EventBus; + +public class EventBusHolder { + public static final EventBus BUS = new AsyncEventBus(Executors.newSingleThreadExecutor()); +} diff --git a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java index 7c3ced0a..8844d5c8 100644 --- a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -6,7 +6,9 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.json.JSONObject; import org.slf4j.Logger; @@ -16,6 +18,7 @@ import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import ctbrec.Config; +import ctbrec.EventBusHolder; import ctbrec.Hmac; import ctbrec.Model; import ctbrec.Recording; @@ -95,6 +98,7 @@ public class RemoteRecorder implements Recorder { models.add(model); } else if ("stop".equals(action)) { models.remove(model); + onlineModels.remove(model); } } else { throw new HttpException(response.code(), response.message()); @@ -231,6 +235,7 @@ public class RemoteRecorder implements Recorder { if (response.isSuccessful()) { ModelListResponse resp = modelListResponseAdapter.fromJson(json); if (resp.status.equals("success")) { + List previouslyOnline = onlineModels; onlineModels = resp.models; for (Model model : models) { for (Site site : sites) { @@ -239,6 +244,25 @@ public class RemoteRecorder implements Recorder { } } } + + for (Model prev : previouslyOnline) { + if(!onlineModels.contains(prev)) { + Map evt = new HashMap<>(); + evt.put("event", "model.status"); + evt.put("status", "offline"); + evt.put("model", prev); + EventBusHolder.BUS.post(evt); + } + } + for (Model model : onlineModels) { + if(!previouslyOnline.contains(model)) { + Map evt = new HashMap<>(); + evt.put("event", "model.status"); + evt.put("status", "online"); + evt.put("model", model); + EventBusHolder.BUS.post(evt); + } + } } else { LOG.error("Server returned error: {} - {}", resp.status, resp.msg); } From 539db89bdb25d0c36317b8e7863e83134428f4a8 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 23:28:32 +0100 Subject: [PATCH 069/231] Bump version to 1.12.1 --- CHANGELOG.md | 6 +++++- client/pom.xml | 2 +- common/pom.xml | 2 +- master/pom.xml | 2 +- server/pom.xml | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c956f655..b791eb9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +1.12.1 +======================== +* Fixed downloads in client / server mode + 1.12.0 ======================== * Added threshold setting to keep free space on the recording device. @@ -138,4 +142,4 @@ * Added proxy settings * Made playlist generator more robust * Fixed some issues with the file merging -* Fixed memory leak caused by the model filter function \ No newline at end of file +* Fixed memory leak caused by the model filter function diff --git a/client/pom.xml b/client/pom.xml index 021c748b..343c11d1 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.12.0 + 1.12.1 ../master diff --git a/common/pom.xml b/common/pom.xml index cb1e5152..3206ef56 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.12.0 + 1.12.1 ../master diff --git a/master/pom.xml b/master/pom.xml index 3c43cdae..869bcf9e 100644 --- a/master/pom.xml +++ b/master/pom.xml @@ -6,7 +6,7 @@ ctbrec master pom - 1.12.0 + 1.12.1 ../common diff --git a/server/pom.xml b/server/pom.xml index 69969b61..dcd1739e 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.12.0 + 1.12.1 ../master From c73bdda35dada95abbc46172edf68782449d0152 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 28 Nov 2018 23:34:25 +0100 Subject: [PATCH 070/231] Update download links to 1.12.1 --- docs/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.html b/docs/index.html index 951aae21..ca67aab2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -118,19 +118,19 @@
    - + Download for Linux! From 09a65c0a969bda811167fdee08e3c6912b4d2750 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 29 Nov 2018 01:05:03 +0100 Subject: [PATCH 071/231] Load stylesheets from parent stage Also, move the notification to the bottom right --- .../src/main/java/ctbrec/ui/CamrecApplication.java | 12 +++++------- .../eu/hansolo/enzo/notification/Notification.java | 10 +++++++--- .../eu/hansolo/enzo/notification/notifier.css | 8 ++++---- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 08f07836..7e59df7e 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -48,7 +48,6 @@ import javafx.scene.control.Alert; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.image.Image; -import javafx.scene.media.AudioClip; import javafx.scene.paint.Color; import javafx.stage.Stage; import okhttp3.Request; @@ -66,7 +65,7 @@ public class CamrecApplication extends Application { private TabPane rootPane = new TabPane(); private List sites = new ArrayList<>(); public static HttpClient httpClient; - private Notification.Notifier notifier; + private Notification.Notifier notifier = Notification.Notifier.INSTANCE; @Override public void start(Stage primaryStage) throws Exception { @@ -93,6 +92,7 @@ public class CamrecApplication extends Application { } createGui(primaryStage); checkForUpdates(); + new Thread(() -> { try { Thread.sleep(TimeUnit.SECONDS.toMillis(10)); @@ -212,7 +212,6 @@ public class CamrecApplication extends Application { private void registerAlertSystem() { Notification.Notifier.setNotificationOwner(primaryStage); - notifier = Notification.Notifier.INSTANCE; EventBusHolder.BUS.register(new Object() { @Subscribe public void modelEvent(Map e) { @@ -223,10 +222,9 @@ public class CamrecApplication extends Application { LOG.debug("Alert: {} is {}", model.getName(), status); if (Objects.equals("online", status)) { Platform.runLater(() -> { - AudioClip clip = new AudioClip("file:///tmp/Oxygen-Im-Highlight-Msg.mp3"); - clip.play(); - Notification notification = new Notification("Model Online", model.getName() + " is now online"); - notifier.notify(notification); + notifier.notifyInfo("Model Online", model.getName() + " is now online"); + //AudioClip clip = new AudioClip("file:///tmp/Oxygen-Im-Highlight-Msg.mp3"); + //clip.play(); }); } } diff --git a/client/src/main/java/eu/hansolo/enzo/notification/Notification.java b/client/src/main/java/eu/hansolo/enzo/notification/Notification.java index 074fc295..567beaaa 100644 --- a/client/src/main/java/eu/hansolo/enzo/notification/Notification.java +++ b/client/src/main/java/eu/hansolo/enzo/notification/Notification.java @@ -79,7 +79,7 @@ public class Notification { private static double offsetX = 0; private static double offsetY = 25; private static double spacingY = 5; - private static Pos popupLocation = Pos.TOP_RIGHT; + private static Pos popupLocation = Pos.BOTTOM_RIGHT; private static Stage stageRef = null; private Duration popupLifetime; private Stage stage; @@ -101,7 +101,10 @@ public class Notification { } private void initGraphics() { - scene = new Scene(new Region()); + Region region = new Region(); + region.resize(0, 0); + region.setVisible(false); + scene = new Scene(region); scene.setFill(null); scene.getStylesheets().add(getClass().getResource("notifier.css").toExternalForm()); @@ -134,6 +137,7 @@ public class Notification { */ public static void setNotificationOwner(final Stage OWNER) { INSTANCE.stage.initOwner(OWNER); + INSTANCE.stage.getScene().getStylesheets().addAll(OWNER.getScene().getStylesheets()); } /** @@ -306,7 +310,6 @@ public class Notification { final Popup POPUP = new Popup(); POPUP.setX( getX() ); POPUP.setY( getY() ); - System.out.println(POPUP.getX() + "," + POPUP.getY()); POPUP.getContent().add(popupContent); popups.add(POPUP); @@ -323,6 +326,7 @@ public class Notification { timeline.setOnFinished(actionEvent -> Platform.runLater(() -> { POPUP.hide(); popups.remove(POPUP); + stage.hide(); })); // Move popup to the right during fade out diff --git a/client/src/main/resources/eu/hansolo/enzo/notification/notifier.css b/client/src/main/resources/eu/hansolo/enzo/notification/notifier.css index 723117bc..ded966ed 100644 --- a/client/src/main/resources/eu/hansolo/enzo/notification/notifier.css +++ b/client/src/main/resources/eu/hansolo/enzo/notification/notifier.css @@ -19,11 +19,11 @@ -fx-fill : transparent; } .notification { - -fx-background-color : -fx-background; + -fx-background-color : -fx-base; -fx-background-radius: 5; - -fx-effect : innershadow(two-pass-box, rgba(255, 255, 255, 0.6), 5.0, 0.25, 0, 0); - -foreground-color : -fx-base; - -icon-color : -fx-base; + -fx-effect : dropshadow(gaussian, rgba(0, 0, 0, 0.8), 20, 0, 0, 0); + -foreground-color : -fx-text-background-color; + -icon-color : -fx-text-background-color; } .notification .title { From 6a2a1aaba2be7e0efd868646c05c4b448e099f77 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 29 Nov 2018 13:36:23 +0100 Subject: [PATCH 072/231] Remove HLS player stuff --- client/src/test/java/AudioTest.java | 19 -- client/src/test/java/HlsTest.java | 44 --- client/src/test/java/MediaControl.java | 355 ------------------------- client/src/test/java/pausebutton.png | Bin 107 -> 0 bytes client/src/test/java/playbutton.png | Bin 131 -> 0 bytes 5 files changed, 418 deletions(-) delete mode 100644 client/src/test/java/AudioTest.java delete mode 100644 client/src/test/java/HlsTest.java delete mode 100644 client/src/test/java/MediaControl.java delete mode 100644 client/src/test/java/pausebutton.png delete mode 100644 client/src/test/java/playbutton.png diff --git a/client/src/test/java/AudioTest.java b/client/src/test/java/AudioTest.java deleted file mode 100644 index 78a9ab5e..00000000 --- a/client/src/test/java/AudioTest.java +++ /dev/null @@ -1,19 +0,0 @@ -import javafx.application.Application; -import javafx.application.Platform; -import javafx.scene.media.AudioClip; -import javafx.stage.Stage; - -public class AudioTest extends Application { - - @Override - public void start(Stage primaryStage) throws Exception { - AudioClip clip = new AudioClip("file:///tmp/Oxygen-Im-Highlight-Msg.mp3"); - clip.cycleCountProperty().set(3); - clip.play(); - Platform.exit(); - } - - public static void main(String[] args) { - launch(args); - } -} diff --git a/client/src/test/java/HlsTest.java b/client/src/test/java/HlsTest.java deleted file mode 100644 index d9c06e87..00000000 --- a/client/src/test/java/HlsTest.java +++ /dev/null @@ -1,44 +0,0 @@ -import javafx.application.Application; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.scene.media.Media; -import javafx.scene.media.MediaPlayer; -import javafx.stage.Stage; - -public class HlsTest extends Application { - // media = new Media("http://localhost:3202/hls/sun_shine_baby/2018-11-28_20-43/playlist.m3u8"); - - private static final String MEDIA_URL = "http://localhost:3202/hls/sun_shine_baby/2018-11-28_20-43/playlist.m3u8"; - - private Media media; - private MediaPlayer mediaPlayer; - private MediaControl mediaControl; - - public Parent createContent() { - media = new Media(MEDIA_URL); - mediaPlayer = new MediaPlayer(media); - mediaPlayer.setOnError(()-> { - mediaPlayer.getError().printStackTrace(System.err); - }); - mediaControl = new MediaControl(mediaPlayer); - mediaControl.setMinSize(480, 280); - mediaControl.setPrefSize(480, 280); - mediaControl.setMaxSize(480, 280); - return mediaControl; - } - - @Override - public void start(Stage primaryStage) throws Exception { - primaryStage.setScene(new Scene(createContent())); - primaryStage.show(); - } - - @Override - public void stop() { - mediaPlayer.stop(); - } - - public static void main(String[] args) { - launch(args); - } -} diff --git a/client/src/test/java/MediaControl.java b/client/src/test/java/MediaControl.java deleted file mode 100644 index 55c0e0bb..00000000 --- a/client/src/test/java/MediaControl.java +++ /dev/null @@ -1,355 +0,0 @@ -import javafx.application.Platform; -import javafx.beans.Observable; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.value.ObservableValue; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.control.Control; -import javafx.scene.control.Label; -import javafx.scene.control.Slider; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.media.MediaPlayer; -import javafx.scene.media.MediaView; -import javafx.stage.Stage; -import javafx.util.Duration; - -public class MediaControl extends BorderPane { - - private MediaPlayer mp; - private MediaView mediaView; - private final boolean repeat = false; - private boolean stopRequested = false; - private boolean atEndOfMedia = false; - private Duration duration; - private Slider timeSlider; - private Label playTime; - private Slider volumeSlider; - private HBox mediaBar; - private Pane mvPane; - private Stage newStage; - private boolean fullScreen = false; - - @Override - protected void layoutChildren() { - if (mediaView != null && getBottom() != null) { - mediaView.setFitWidth(getWidth()); - mediaView.setFitHeight(getHeight() - getBottom().prefHeight(-1)); - } - super.layoutChildren(); - if (mediaView != null && getCenter() != null) { - mediaView.setTranslateX((((Pane) getCenter()).getWidth() - mediaView.prefWidth(-1)) / 2); - mediaView.setTranslateY((((Pane) getCenter()).getHeight() - mediaView.prefHeight(-1)) / 2); - } - } - - @Override - protected double computeMinWidth(double height) { - return mediaBar.prefWidth(-1); - } - - @Override - protected double computeMinHeight(double width) { - return 200; - } - - @Override - protected double computePrefWidth(double height) { - return Math.max(mp.getMedia().getWidth(), mediaBar.prefWidth(height)); - } - - @Override - protected double computePrefHeight(double width) { - return mp.getMedia().getHeight() + mediaBar.prefHeight(width); - } - - @Override - protected double computeMaxWidth(double height) { - return Double.MAX_VALUE; - } - - @Override - protected double computeMaxHeight(double width) { - return Double.MAX_VALUE; - } - - public MediaControl(final MediaPlayer mp) { - this.mp = mp; - setStyle("-fx-background-color: #bfc2c7;"); // TODO: Use css file - mediaView = new MediaView(mp); - mvPane = new Pane(); - mvPane.getChildren().add(mediaView); - mvPane.setStyle("-fx-background-color: black;"); // TODO: Use css file - setCenter(mvPane); - mediaBar = new HBox(5.0); - mediaBar.setPadding(new Insets(5, 10, 5, 10)); - mediaBar.setAlignment(Pos.CENTER_LEFT); - BorderPane.setAlignment(mediaBar, Pos.CENTER); - - final Button playButton = new Button(); - playButton.setMinWidth(Control.USE_PREF_SIZE); - - String PLAY = "playbutton.png"; - String PAUSE = "pausebutton.png"; - Image PlayButton = new Image(getClass().getResourceAsStream(PLAY)); - Image PauseButton = new Image(getClass().getResourceAsStream(PAUSE)); - ImageView imageViewPlay = new ImageView(PlayButton); - ImageView imageViewPause = new ImageView(PauseButton); - playButton.setGraphic(imageViewPlay); - playButton.setOnAction((ActionEvent e) -> { - updateValues(); - MediaPlayer.Status status = mp.getStatus(); - if (status == MediaPlayer.Status.UNKNOWN || status == MediaPlayer.Status.HALTED) { - // don't do anything in these states - return; - } - - if (status == MediaPlayer.Status.PAUSED || status == MediaPlayer.Status.READY || status == MediaPlayer.Status.STOPPED) { - // rewind the movie if we're sitting at the end - if (atEndOfMedia) { - mp.seek(mp.getStartTime()); - atEndOfMedia = false; - playButton.setGraphic(imageViewPlay); - // playButton.setText(">"); - updateValues(); - } - mp.play(); - playButton.setGraphic(imageViewPause); - // playButton.setText("||"); - } else { - mp.pause(); - } - }); - ReadOnlyObjectProperty time = mp.currentTimeProperty(); - time.addListener((ObservableValue observable, Duration oldValue, Duration newValue) -> { - //updateValues(); - }); - mp.setOnPlaying(() -> { - if (stopRequested) { - mp.pause(); - stopRequested = false; - } else { - playButton.setGraphic(imageViewPause); - // playButton.setText("||"); - } - }); - mp.setOnPaused(() -> { - playButton.setGraphic(imageViewPlay); - // playButton.setText("||"); - }); - mp.setOnReady(() -> { - duration = mp.getMedia().getDuration(); - updateValues(); - }); - - mp.setCycleCount(repeat ? MediaPlayer.INDEFINITE : 1); - mp.setOnEndOfMedia(() -> { - if (!repeat) { - playButton.setGraphic(imageViewPlay); - // playButton.setText(">"); - stopRequested = true; - atEndOfMedia = true; - } - }); - mediaBar.getChildren().add(playButton); - - // Time label - Label timeLabel = new Label("Time"); - timeLabel.setMinWidth(Control.USE_PREF_SIZE); - mediaBar.getChildren().add(timeLabel); - - // Time slider - timeSlider = new Slider(); - timeSlider.setMinWidth(30); - timeSlider.setMaxWidth(Double.MAX_VALUE); - HBox.setHgrow(timeSlider, Priority.ALWAYS); - - DoubleProperty timeValue = timeSlider.valueProperty(); - timeValue.addListener((ObservableValue observable, Number old, Number now) -> { - if (timeSlider.isValueChanging()) { - // multiply duration by percentage calculated by slider position - if (duration != null) { - System.out.println(timeSlider.getValue() + "%"); - mp.seek(duration.multiply(timeSlider.getValue() / 100.0)); - } - updateValues(); - } else if (Math.abs(now.doubleValue() - old.doubleValue()) > 1.5) { - // multiply duration by percentage calculated by slider position - System.out.println(timeSlider.getValue() + "%"); - if (duration != null) { - mp.seek(duration.multiply(timeSlider.getValue() / 100.0)); - } - } - }); - mediaBar.getChildren().add(timeSlider); - - // Play label - playTime = new Label(); - playTime.setMinWidth(Control.USE_PREF_SIZE); - - mediaBar.getChildren().add(playTime); - - // Fullscreen button - Button buttonFullScreen = new Button("Full Screen"); - buttonFullScreen.setMinWidth(Control.USE_PREF_SIZE); - - buttonFullScreen.setOnAction(new EventHandler() { - @Override - public void handle(ActionEvent event) { - if (!fullScreen) { - newStage = new Stage(); - ReadOnlyBooleanProperty full = newStage.fullScreenProperty(); - full.addListener((ObservableValue ov, Boolean old, Boolean now) -> { - onFullScreen(); - }); - final BorderPane borderPane = new BorderPane() { - @Override - protected void layoutChildren() { - if (mediaView != null && getBottom() != null) { - mediaView.setFitWidth(getWidth()); - double height = getHeight() - getBottom().prefHeight(-1); - mediaView.setFitHeight(height); - } - super.layoutChildren(); - if (mediaView != null) { - final Pane center = (Pane) getCenter(); - if (center != null) { // if smaller pane has content - double width = center.getWidth() - mediaView.prefWidth(-1); - double height = center.getHeight() - mediaView.prefHeight(-1); - double xval = width / 2.0; - double yval = height / 2.0; - - mediaView.setTranslateX(xval); - mediaView.setTranslateY(yval); - } - } - } - }; - - setCenter(null); - setBottom(null); - borderPane.setCenter(mvPane); - borderPane.setBottom(mediaBar); - - Scene newScene = new Scene(borderPane); - newStage.setScene(newScene); - // Workaround for disposing stage when exit fullscreen - newStage.setX(-100000); - newStage.setY(-100000); - - newStage.setFullScreen(true); - fullScreen = true; - newStage.show(); - - } else { - // toggle FullScreen - fullScreen = false; - newStage.setFullScreen(false); - - } - } - }); - mediaBar.getChildren().add(buttonFullScreen); - - // Volume label - Label volumeLabel = new Label("Vol"); - volumeLabel.setMinWidth(Control.USE_PREF_SIZE); - mediaBar.getChildren().add(volumeLabel); - - // Volume slider - volumeSlider = new Slider(); - volumeSlider.setPrefWidth(70); - volumeSlider.setMinWidth(30); - volumeSlider.setMaxWidth(Region.USE_PREF_SIZE); - volumeSlider.valueProperty().addListener((Observable ov) -> { - }); - - final DoubleProperty volume = volumeSlider.valueProperty(); - volume.addListener((ObservableValue observable, Number old, Number now) -> { - mp.setVolume(volumeSlider.getValue() / 100.0); - }); - mediaBar.getChildren().add(volumeSlider); - - setBottom(mediaBar); - - } - - protected void onFullScreen() { - if (!newStage.isFullScreen()) { - - fullScreen = false; - BorderPane smallBP = (BorderPane) newStage.getScene().getRoot(); - smallBP.setCenter(null); - setCenter(mvPane); - - smallBP.setBottom(null); - setBottom(mediaBar); - Platform.runLater(() -> { - newStage.close(); - }); - - } - } - - protected void updateValues() { - if (playTime != null && timeSlider != null && volumeSlider != null && duration != null) { - Platform.runLater(() -> { - Duration now = mp.getCurrentTime(); - playTime.setText(formatTime(now, duration)); - timeSlider.setDisable(duration.isUnknown()); - if (!timeSlider.isDisabled() && duration.greaterThan(Duration.ZERO) && !timeSlider.isValueChanging()) { - final double value = now.divide(duration).toMillis() * 100.0; - timeSlider.setValue(value); - } - if (!volumeSlider.isValueChanging()) { - final int value = (int) Math.round(mp.getVolume() * 100); - volumeSlider.setValue(value); - } - }); - } - } - - private String formatTime(Duration elapsed, Duration duration) { - int intElapsed = (int) Math.floor(elapsed.toSeconds()); - int elapsedHours = intElapsed / (60 * 60); - if (elapsedHours > 0) { - intElapsed -= elapsedHours * 60 * 60; - } - int elapsedMinutes = intElapsed / 60; - int elapsedSeconds = intElapsed - elapsedHours * 60 * 60 - elapsedMinutes * 60; - - if (duration.greaterThan(Duration.ZERO)) { - int intDuration = (int) Math.floor(duration.toSeconds()); - int durationHours = intDuration / (60 * 60); - if (durationHours > 0) { - intDuration -= durationHours * 60 * 60; - } - int durationMinutes = intDuration / 60; - int durationSeconds = intDuration - durationHours * 60 * 60 - durationMinutes * 60; - - if (durationHours > 0) { - return String.format("%d:%02d:%02d/%d:%02d:%02d", elapsedHours, elapsedMinutes, elapsedSeconds, durationHours, durationMinutes, - durationSeconds); - } else { - return String.format("%02d:%02d/%02d:%02d", elapsedMinutes, elapsedSeconds, durationMinutes, durationSeconds); - } - } else { - if (elapsedHours > 0) { - return String.format("%d:%02d:%02d", elapsedHours, elapsedMinutes, elapsedSeconds); - } else { - return String.format("%02d:%02d", elapsedMinutes, elapsedSeconds); - } - } - } -} \ No newline at end of file diff --git a/client/src/test/java/pausebutton.png b/client/src/test/java/pausebutton.png deleted file mode 100644 index 4e429238f487ef59389dfcb4253db0dea852eee6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^;y^6G!3HG%>OYYHQbwLGjv*Ddk`pBQ9@q=1x%OEc z=9jfOYYHQeK`ejv*Ddk`pBQ9@vXqKK8-6 zo$v4Rsj3Nw6KC9-wm`x!*L#DMpRV@@slGJMmu=UtUKKoIVmRkf!ot>t5xmQ*f|j%{ gEIIkFS(<^NtY>|`;_-dAfu=Hey85}Sb4q9e0H2015&!@I From fbb1c284d26a1259c4c86a3a0a9beadd7aba281d Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 29 Nov 2018 13:41:45 +0100 Subject: [PATCH 073/231] Remove old code --- .../main/java/ctbrec/ui/RecordingsTab.java | 74 ------------------- 1 file changed, 74 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index f8fef9dd..08b8a411 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -477,80 +477,6 @@ public class RecordingsTab extends Tab implements TabSelectionListener { } } - // private void download(Recording recording) throws IOException, ParseException, PlaylistException { - // String filename = recording.getPath().replaceAll("/", "-") + ".ts"; - // FileChooser chooser = new FileChooser(); - // chooser.setInitialFileName(filename); - // if(config.getSettings().lastDownloadDir != null && !config.getSettings().lastDownloadDir.equals("")) { - // File dir = new File(config.getSettings().lastDownloadDir); - // while(!dir.exists()) { - // dir = dir.getParentFile(); - // } - // chooser.setInitialDirectory(dir); - // } - // File target = chooser.showSaveDialog(null); - // if(target != null) { - // config.getSettings().lastDownloadDir = target.getParent(); - // String hlsBase = "http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/hls"; - // URL url = new URL(hlsBase + "/" + recording.getPath() + "/playlist.m3u8"); - // LOG.info("Downloading {}", recording.getPath()); - // - // PlaylistParser parser = new PlaylistParser(url.openStream(), Format.EXT_M3U, Encoding.UTF_8); - // Playlist playlist = parser.parse(); - // MediaPlaylist mediaPlaylist = playlist.getMediaPlaylist(); - // List tracks = mediaPlaylist.getTracks(); - // List segmentUris = new ArrayList<>(); - // for (TrackData trackData : tracks) { - // String segmentUri = hlsBase + "/" + recording.getPath() + "/" + trackData.getUri(); - // segmentUris.add(segmentUri); - // } - // - // Thread t = new Thread() { - // @Override - // public void run() { - // try(FileOutputStream fos = new FileOutputStream(target)) { - // for (int i = 0; i < segmentUris.size(); i++) { - // URL segment = new URL(segmentUris.get(i)); - // InputStream in = segment.openStream(); - // byte[] b = new byte[1024]; - // int length = -1; - // while( (length = in.read(b)) >= 0 ) { - // fos.write(b, 0, length); - // } - // in.close(); - // int progress = (int) (i * 100.0 / segmentUris.size()); - // Platform.runLater(new Runnable() { - // @Override - // public void run() { - // recording.setStatus(STATUS.DOWNLOADING); - // recording.setProgress(progress); - // } - // }); - // } - // - // } catch (FileNotFoundException e) { - // showErrorDialog("Error while downloading recording", "The target file couldn't be created", e); - // LOG.error("Error while downloading recording", e); - // } catch (IOException e) { - // showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e); - // LOG.error("Error while downloading recording", e); - // } finally { - // Platform.runLater(new Runnable() { - // @Override - // public void run() { - // recording.setStatus(STATUS.FINISHED); - // recording.setProgress(-1); - // } - // }); - // } - // } - // }; - // t.setDaemon(true); - // t.setName("Download Thread " + recording.getPath()); - // t.start(); - // } - // } - private void showErrorDialog(final String title, final String msg, final Exception e) { Platform.runLater(new Runnable() { @Override From 2c716d3c88a2c2221e76867f8050e9fb05e7f6db Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 29 Nov 2018 13:42:23 +0100 Subject: [PATCH 074/231] Set all occurrences of PlaylistParser to lenient mode --- common/src/main/java/ctbrec/recorder/PlaylistGenerator.java | 3 ++- common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java | 3 ++- common/src/main/java/ctbrec/sites/cam4/Cam4Model.java | 3 ++- common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java | 3 ++- common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java | 3 ++- common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java b/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java index e615f63f..2fec113c 100644 --- a/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java +++ b/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java @@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory; import com.iheartradio.m3u8.Encoding; import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.ParsingMode; import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistParser; import com.iheartradio.m3u8.PlaylistWriter; @@ -190,7 +191,7 @@ public class PlaylistGenerator { public void validate(File recDir) throws IOException, ParseException, PlaylistException { File playlist = new File(recDir, "playlist.m3u8"); if(playlist.exists()) { - PlaylistParser playlistParser = new PlaylistParser(new FileInputStream(playlist), Format.EXT_M3U, Encoding.UTF_8); + PlaylistParser playlistParser = new PlaylistParser(new FileInputStream(playlist), Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); Playlist m3u = playlistParser.parse(); MediaPlaylist mediaPlaylist = m3u.getMediaPlaylist(); int playlistSize = mediaPlaylist.getTracks().size(); diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java b/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java index a6fd377f..eaad5853 100644 --- a/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java +++ b/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java @@ -14,6 +14,7 @@ import org.slf4j.LoggerFactory; import com.iheartradio.m3u8.Encoding; import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.ParsingMode; import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistParser; import com.iheartradio.m3u8.data.MasterPlaylist; @@ -101,7 +102,7 @@ public class BongaCamsModel extends AbstractModel { try(Response response = site.getHttpClient().execute(req)) { if(response.isSuccessful()) { InputStream inputStream = response.body().byteStream(); - PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); Playlist playlist = parser.parse(); MasterPlaylist master = playlist.getMasterPlaylist(); streamSources.clear(); diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java index 68b24354..95686b19 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java @@ -17,6 +17,7 @@ import org.slf4j.LoggerFactory; import com.iheartradio.m3u8.Encoding; import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.ParsingMode; import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistParser; import com.iheartradio.m3u8.data.MasterPlaylist; @@ -129,7 +130,7 @@ public class Cam4Model extends AbstractModel { Response response = site.getHttpClient().execute(req); try { InputStream inputStream = response.body().byteStream(); - PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); Playlist playlist = parser.parse(); MasterPlaylist master = playlist.getMasterPlaylist(); return master; diff --git a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index 53af0dad..b3d03d94 100644 --- a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -17,6 +17,7 @@ import com.google.common.cache.CacheBuilder; import com.iheartradio.m3u8.Encoding; import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.ParsingMode; import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistParser; import com.iheartradio.m3u8.data.MasterPlaylist; @@ -112,7 +113,7 @@ public class CamsodaModel extends AbstractModel { Response response = site.getHttpClient().execute(req); try { InputStream inputStream = response.body().byteStream(); - PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); Playlist playlist = parser.parse(); MasterPlaylist master = playlist.getMasterPlaylist(); PlaylistData playlistData = master.getPlaylists().get(0); diff --git a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java index 534a39bb..0c1af470 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java @@ -20,6 +20,7 @@ import com.google.common.cache.LoadingCache; import com.iheartradio.m3u8.Encoding; import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.ParsingMode; import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistParser; import com.iheartradio.m3u8.data.MasterPlaylist; @@ -324,7 +325,7 @@ public class Chaturbate extends AbstractSite { try (Response response = getHttpClient().execute(req)) { if(response.isSuccessful()) { InputStream inputStream = response.body().byteStream(); - PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); Playlist playlist = parser.parse(); MasterPlaylist master = playlist.getMasterPlaylist(); return master; diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index 45f13c08..6ca46f26 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory; import com.iheartradio.m3u8.Encoding; import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.ParsingMode; import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistParser; import com.iheartradio.m3u8.data.MasterPlaylist; @@ -106,7 +107,7 @@ public class MyFreeCamsModel extends AbstractModel { try(Response response = site.getHttpClient().execute(req)) { if(response.isSuccessful()) { InputStream inputStream = response.body().byteStream(); - PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); Playlist playlist = parser.parse(); MasterPlaylist master = playlist.getMasterPlaylist(); return master; From c17dcc42163b252c88e3a84e273e8e27498df5b1 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 29 Nov 2018 17:33:10 +0100 Subject: [PATCH 075/231] Add setting to toggle Player Starting message --- .../src/main/java/ctbrec/ui/RecordedModelsTab.java | 2 +- client/src/main/java/ctbrec/ui/RecordingsTab.java | 4 ++-- client/src/main/java/ctbrec/ui/SettingsTab.java | 13 +++++++++++++ client/src/main/java/ctbrec/ui/ThumbCell.java | 2 +- .../ctbrec/ui/controls/SearchPopoverTreeList.java | 3 ++- common/src/main/java/ctbrec/Settings.java | 1 + 6 files changed, 20 insertions(+), 5 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index 2fc735fd..86a3c35b 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -348,7 +348,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { new Thread(() -> { boolean started = Player.play(selectedModel); Platform.runLater(() -> { - if(started) { + if(started && Config.getInstance().getSettings().showPlayerStarting) { Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500); } table.setCursor(Cursor.DEFAULT); diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index 08b8a411..32b8e3c0 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -497,7 +497,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { @Override public void run() { boolean started = Player.play(recording); - if(started) { + if(started && Config.getInstance().getSettings().showPlayerStarting) { Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500)); } } @@ -509,7 +509,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { @Override public void run() { boolean started = Player.play(url); - if(started) { + if(started && Config.getInstance().getSettings().showPlayerStarting) { Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500)); } } diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java index d1f816cc..a5182481 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/SettingsTab.java @@ -72,6 +72,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private CheckBox chooseStreamQuality = new CheckBox(); private CheckBox multiplePlayers = new CheckBox(); private CheckBox updateThumbnails = new CheckBox(); + private CheckBox showPlayerStarting = new CheckBox(); private RadioButton recordLocal; private RadioButton recordRemote; private ToggleGroup recordLocation; @@ -409,6 +410,18 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(multiplePlayers, 1, row++); + l = new Label("Show \"Player Starting\" Message"); + layout.add(l, 0, row); + showPlayerStarting.setSelected(Config.getInstance().getSettings().showPlayerStarting); + showPlayerStarting.setOnAction((e) -> { + Config.getInstance().getSettings().showPlayerStarting = showPlayerStarting.isSelected(); + saveConfig(); + }); + GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); + GridPane.setMargin(showPlayerStarting, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + layout.add(showPlayerStarting, 1, row++); + + l = new Label("Display stream resolution in overview"); layout.add(l, 0, row); loadResolution = new CheckBox(); diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index eb316656..54449c0c 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -308,7 +308,7 @@ public class ThumbCell extends StackPane { boolean started = Player.play(model); Platform.runLater(() -> { setCursor(Cursor.DEFAULT); - if (started) { + if (started && Config.getInstance().getSettings().showPlayerStarting) { Toast.makeText(getScene(), "Starting Player", 2000, 500, 500); } }); diff --git a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java index 8b949ece..474e91c6 100644 --- a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java +++ b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java @@ -38,6 +38,7 @@ import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ctbrec.Config; import ctbrec.Model; import ctbrec.recorder.Recorder; import ctbrec.ui.Player; @@ -86,7 +87,7 @@ public class SearchPopoverTreeList extends PopoverTreeList implements Pop new Thread(() -> { Platform.runLater(() -> { boolean started = Player.play(model); - if(started) { + if(started && Config.getInstance().getSettings().showPlayerStarting) { Toast.makeText(getScene(), "Starting Player", 2000, 500, 500); } setCursor(Cursor.DEFAULT); diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 3b613845..da3d8854 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -30,6 +30,7 @@ public class Settings { } public boolean singlePlayer = true; + public boolean showPlayerStarting = true; public boolean localRecording = true; public int httpPort = 8080; public int httpTimeout = 10000; From cbe466e7b99396d7dd5ea7dcafb75f1741e5ed8e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 29 Nov 2018 17:50:09 +0100 Subject: [PATCH 076/231] Set default for showPlayerStarting to false --- common/src/main/java/ctbrec/Settings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index da3d8854..18172a94 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -30,7 +30,7 @@ public class Settings { } public boolean singlePlayer = true; - public boolean showPlayerStarting = true; + public boolean showPlayerStarting = false; public boolean localRecording = true; public int httpPort = 8080; public int httpTimeout = 10000; From 88bddcb1880bd4691a7236cfd11359c5d2113481 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 29 Nov 2018 17:53:26 +0100 Subject: [PATCH 077/231] Fix: Player not starting when path contains spaces --- client/src/main/java/ctbrec/ui/Player.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/Player.java b/client/src/main/java/ctbrec/ui/Player.java index f9c0d9fd..bace7e78 100644 --- a/client/src/main/java/ctbrec/ui/Player.java +++ b/client/src/main/java/ctbrec/ui/Player.java @@ -120,7 +120,11 @@ public class Player { try { if (Config.getInstance().getSettings().localRecording && rec != null) { File file = new File(Config.getInstance().getSettings().recordingsDir, rec.getPath()); - playerProcess = rt.exec(Config.getInstance().getSettings().mediaPlayer + " " + file, OS.getEnvironment(), file.getParentFile()); + String[] args = new String[] { + Config.getInstance().getSettings().mediaPlayer, + file.getName() + }; + playerProcess = rt.exec(args, OS.getEnvironment(), file.getParentFile()); } else { if(Config.getInstance().getSettings().requireAuthentication) { URL u = new URL(url); @@ -136,10 +140,12 @@ public class Player { // create threads, which read stdout and stderr of the player process. these are needed, // because otherwise the internal buffer for these streams fill up and block the process Thread std = new Thread(new StreamRedirectThread(playerProcess.getInputStream(), new DevNull())); + //Thread std = new Thread(new StreamRedirectThread(playerProcess.getInputStream(), System.out)); std.setName("Player stdout pipe"); std.setDaemon(true); std.start(); Thread err = new Thread(new StreamRedirectThread(playerProcess.getErrorStream(), new DevNull())); + //Thread err = new Thread(new StreamRedirectThread(playerProcess.getErrorStream(), System.err)); err.setName("Player stderr pipe"); err.setDaemon(true); err.start(); From afd5d3caa3d7a77c79a5a22e508abc12449fa844 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 29 Nov 2018 19:08:57 +0100 Subject: [PATCH 078/231] Extend manual add function to allow to add models by their URL --- CHANGELOG.md | 6 +++ .../java/ctbrec/ui/RecordedModelsTab.java | 44 +++++++++++++++++-- .../main/java/ctbrec/sites/AbstractSite.java | 5 +++ common/src/main/java/ctbrec/sites/Site.java | 1 + .../java/ctbrec/sites/bonga/BongaCams.java | 12 +++++ .../src/main/java/ctbrec/sites/cam4/Cam4.java | 13 ++++++ .../java/ctbrec/sites/camsoda/Camsoda.java | 13 ++++++ .../ctbrec/sites/chaturbate/Chaturbate.java | 13 ++++++ .../java/ctbrec/sites/mfc/MyFreeCams.java | 18 ++++++++ 9 files changed, 121 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b791eb9b..51b8e6a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +1.12.2 +======================== +* Fix: Player not starting when path contains spaces +* Added setting to toggle "Player Starting" message +* Added possibility to add models by their URL + 1.12.1 ======================== * Fixed downloads in client / server mode diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index 86a3c35b..523ef502 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -155,8 +155,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { ObservableList suggestions = FXCollections.observableArrayList(); sites.forEach(site -> suggestions.add(site.getName())); model = new AutoFillTextField(suggestions); - model.setPrefWidth(300); - model.setPromptText("e.g. MyFreeCams:ModelName"); + model.setPrefWidth(600); + model.setPromptText("e.g. MyFreeCams:ModelName or an URL like https://chaturbate.com/modelname/"); model.onActionHandler(e -> addModel(e)); model.setTooltip(new Tooltip("To add a model enter SiteName:ModelName\n" + "press ENTER to confirm a suggested site name")); @@ -174,6 +174,43 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } private void addModel(ActionEvent e) { + String input = model.getText(); + if(StringUtil.isBlank(input)) { + return; + } + + if(input.startsWith("http")) { + addModelByUrl(input); + } else { + addModelByName(input); + } + }; + + private void addModelByUrl(String url) { + for (Site site : sites) { + Model model = site.createModelFromUrl(url); + if(model != null) { + try { + recorder.startRecording(model); + } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText("Couldn't add model"); + alert.setContentText("The model " + model.getName() + " could not be added: " + e1.getLocalizedMessage()); + alert.showAndWait(); + } + return; + } + } + + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Unknown URL format"); + alert.setHeaderText("Couldn't add model"); + alert.setContentText("The URL you entered has an unknown format or the function does not support this site, yet"); + alert.showAndWait(); + } + + private void addModelByName(String siteModelCombo) { String[] parts = model.getText().trim().split(":"); if (parts.length != 2) { Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); @@ -207,8 +244,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { alert.setHeaderText("Couldn't add model"); alert.setContentText("The site you entered is unknown"); alert.showAndWait(); - }; - + } void initializeUpdateService() { updateService = createUpdateService(); diff --git a/common/src/main/java/ctbrec/sites/AbstractSite.java b/common/src/main/java/ctbrec/sites/AbstractSite.java index 96d67005..4ecd6465 100644 --- a/common/src/main/java/ctbrec/sites/AbstractSite.java +++ b/common/src/main/java/ctbrec/sites/AbstractSite.java @@ -46,4 +46,9 @@ public abstract class AbstractSite implements Site { public boolean searchRequiresLogin() { return false; } + + @Override + public Model createModelFromUrl(String url) { + return null; + } } diff --git a/common/src/main/java/ctbrec/sites/Site.java b/common/src/main/java/ctbrec/sites/Site.java index cf6f3119..9225b52c 100644 --- a/common/src/main/java/ctbrec/sites/Site.java +++ b/common/src/main/java/ctbrec/sites/Site.java @@ -29,4 +29,5 @@ public interface Site { public boolean isEnabled(); public List search(String q) throws IOException, InterruptedException; public boolean searchRequiresLogin(); + public Model createModelFromUrl(String url); } diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java index 6b2670d8..d74288e0 100644 --- a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java +++ b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java @@ -5,6 +5,8 @@ import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.json.JSONArray; import org.json.JSONObject; @@ -184,4 +186,14 @@ public class BongaCams extends AbstractSite { return username != null && !username.trim().isEmpty(); } + @Override + public Model createModelFromUrl(String url) { + Matcher m = Pattern.compile("https?://.*?bongacams.com(?:/profile)?/([^/]*?)/?").matcher(url); + if(m.matches()) { + String modelName = m.group(1); + return createModel(modelName); + } else { + return super.createModelFromUrl(url); + } + } } diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4.java b/common/src/main/java/ctbrec/sites/cam4/Cam4.java index 04b032f4..8c3907a0 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4.java @@ -4,6 +4,8 @@ import java.io.IOException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.json.JSONArray; import org.json.JSONObject; @@ -154,4 +156,15 @@ public class Cam4 extends AbstractSite { String username = Config.getInstance().getSettings().cam4Username; return username != null && !username.trim().isEmpty(); } + + @Override + public Model createModelFromUrl(String url) { + Matcher m = Pattern.compile("https?://(?:www\\.)?cam4(?:.*?).com/([^/]*?)/?").matcher(url); + if(m.matches()) { + String modelName = m.group(1); + return createModel(modelName); + } else { + return super.createModelFromUrl(url); + } + } } diff --git a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java index e79688fa..346c9315 100644 --- a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java +++ b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java @@ -5,6 +5,8 @@ import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.json.JSONArray; import org.json.JSONObject; @@ -161,4 +163,15 @@ public class Camsoda extends AbstractSite { String username = Config.getInstance().getSettings().camsodaUsername; return username != null && !username.trim().isEmpty(); } + + @Override + public Model createModelFromUrl(String url) { + Matcher m = Pattern.compile("https?://(?:www\\.)?camsoda.com/([^/]*?)/?").matcher(url); + if(m.matches()) { + String modelName = m.group(1); + return createModel(modelName); + } else { + return super.createModelFromUrl(url); + } + } } diff --git a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java index 0c1af470..251e3187 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java @@ -10,6 +10,8 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -340,4 +342,15 @@ public class Chaturbate extends AbstractSite { String username = Config.getInstance().getSettings().username; return username != null && !username.trim().isEmpty(); } + + @Override + public Model createModelFromUrl(String url) { + Matcher m = Pattern.compile("https?://.*?chaturbate.com(?:/p)?/([^/]*?)/?").matcher(url); + if(m.matches()) { + String modelName = m.group(1); + return createModel(modelName); + } else { + return super.createModelFromUrl(url); + } + } } diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java index 146c834a..a72191e2 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java @@ -2,6 +2,8 @@ package ctbrec.sites.mfc; import java.io.IOException; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.jsoup.select.Elements; @@ -122,4 +124,20 @@ public class MyFreeCams extends AbstractSite { String username = Config.getInstance().getSettings().mfcUsername; return username != null && !username.trim().isEmpty(); } + + @Override + public Model createModelFromUrl(String url) { + String[] patterns = new String[] { + "https?://profiles.myfreecams.com/([^/]*?)", + "https?://(?:www.)?myfreecams.com/#(.*)" + }; + for (String pattern : patterns) { + Matcher m = Pattern.compile(pattern).matcher(url); + if(m.matches()) { + String modelName = m.group(1); + return createModel(modelName); + } + } + return super.createModelFromUrl(url); + } } From 0bd655bf95feacbf5d5451fdbdec434ba5e66adb Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 29 Nov 2018 20:10:06 +0100 Subject: [PATCH 079/231] Add buttons to pause/resume all recordings --- .../java/ctbrec/ui/RecordedModelsTab.java | 65 ++++++++++++++++++- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index 523ef502..af825f69 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -79,6 +79,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { Label modelLabel = new Label("Model"); AutoFillTextField model; Button addModelButton = new Button("Record"); + Button pauseAll = new Button("Pause All"); + Button resumeAll = new Button("Resume All"); public RecordedModelsTab(String title, Recorder recorder, List sites) { super(title); @@ -157,12 +159,15 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { model = new AutoFillTextField(suggestions); model.setPrefWidth(600); model.setPromptText("e.g. MyFreeCams:ModelName or an URL like https://chaturbate.com/modelname/"); - model.onActionHandler(e -> addModel(e)); + model.onActionHandler(this::addModel); model.setTooltip(new Tooltip("To add a model enter SiteName:ModelName\n" + "press ENTER to confirm a suggested site name")); BorderPane.setMargin(addModelBox, new Insets(5)); - addModelButton.setOnAction((e) -> addModel(e)); - addModelBox.getChildren().addAll(modelLabel, model, addModelButton); + addModelButton.setOnAction(this::addModel); + addModelBox.getChildren().addAll(modelLabel, model, addModelButton, pauseAll, resumeAll); + HBox.setMargin(pauseAll, new Insets(0, 0, 0, 20)); + pauseAll.setOnAction(this::pauseAll); + resumeAll.setOnAction(this::resumeAll); BorderPane root = new BorderPane(); root.setPadding(new Insets(5)); @@ -246,6 +251,60 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { alert.showAndWait(); } + private void pauseAll(ActionEvent evt) { + getTabPane().setCursor(Cursor.WAIT); + threadPool.submit(() -> { + List models = recorder.getModelsRecording(); + Exception ex = null; + for (Model model : models) { + try { + recorder.suspendRecording(model); + } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) { + LOG.error("Couldn't suspend model {}: {}", model, e.getMessage()); + ex = e; + } + } + final Exception exc = ex; // stupid compiler + Platform.runLater(() -> { + getTabPane().setCursor(Cursor.DEFAULT); + if(exc != null) { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Pause Model"); + alert.setHeaderText("Couldn't pause recording"); + alert.setContentText("At least one recording couldn't be paused: " + exc.getMessage()); + alert.showAndWait(); + } + }); + }); + } + + private void resumeAll(ActionEvent evt) { + getTabPane().setCursor(Cursor.WAIT); + threadPool.submit(() -> { + List models = recorder.getModelsRecording(); + Exception ex = null; + for (Model model : models) { + try { + recorder.resumeRecording(model); + } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) { + LOG.error("Couldn't resume model {}: {}", model, e.getMessage()); + ex = e; + } + } + final Exception exc = ex; // stupid compiler + Platform.runLater(() -> { + getTabPane().setCursor(Cursor.DEFAULT); + if(exc != null) { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Resume Model"); + alert.setHeaderText("Couldn't resume recording"); + alert.setContentText("At least one recording couldn't be resumed: " + exc.getMessage()); + alert.showAndWait(); + } + }); + }); + } + void initializeUpdateService() { updateService = createUpdateService(); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2))); From 0c825237b28fd62b245bdd5361b10343c04098f6 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 29 Nov 2018 20:56:42 +0100 Subject: [PATCH 080/231] Implement multi-selection for RecordedModelsTab --- .../java/ctbrec/ui/RecordedModelsTab.java | 232 +++++++----------- 1 file changed, 90 insertions(+), 142 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index af825f69..acc72ce1 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -14,6 +14,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -42,6 +43,7 @@ import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; +import javafx.scene.control.SelectionMode; import javafx.scene.control.Tab; import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn.SortType; @@ -103,6 +105,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { BorderPane.setMargin(scrollPane, new Insets(5)); table.setEditable(false); + table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); TableColumn name = new TableColumn<>("Model"); name.setPrefWidth(200); name.setCellValueFactory(new PropertyValueFactory("name")); @@ -137,19 +140,9 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { }); table.addEventFilter(KeyEvent.KEY_PRESSED, event -> { if(event.getCode() == KeyCode.DELETE) { - stopAction(); + stopAction(table.getSelectionModel().getSelectedItems()); } }); - table.addEventFilter(KeyEvent.KEY_PRESSED, event -> { - if(event.getCode() == KeyCode.S) { - for (TableColumn col : table.getSortOrder()) { - System.out.println(col.getText()); - System.out.println(col.getSortType()); - System.out.println(col.getComparator()); - } - } - }); - scrollPane.setContent(table); HBox addModelBox = new HBox(5); @@ -252,56 +245,38 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } private void pauseAll(ActionEvent evt) { - getTabPane().setCursor(Cursor.WAIT); - threadPool.submit(() -> { - List models = recorder.getModelsRecording(); - Exception ex = null; - for (Model model : models) { - try { - recorder.suspendRecording(model); - } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) { - LOG.error("Couldn't suspend model {}: {}", model, e.getMessage()); - ex = e; - } + List models = recorder.getModelsRecording(); + Consumer action = (m) -> { + try { + recorder.suspendRecording(m); + } catch(Exception e) { + Platform.runLater(() -> + showErrorDialog(e, "Couldn't suspend recording of model", "Suspending recording of " + m.getName() + " failed")); } - final Exception exc = ex; // stupid compiler - Platform.runLater(() -> { - getTabPane().setCursor(Cursor.DEFAULT); - if(exc != null) { - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Pause Model"); - alert.setHeaderText("Couldn't pause recording"); - alert.setContentText("At least one recording couldn't be paused: " + exc.getMessage()); - alert.showAndWait(); - } - }); - }); + }; + massEdit(models, action); } private void resumeAll(ActionEvent evt) { + List models = recorder.getModelsRecording(); + Consumer action = (m) -> { + try { + recorder.resumeRecording(m); + } catch(Exception e) { + Platform.runLater(() -> + showErrorDialog(e, "Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed")); + } + }; + massEdit(models, action); + } + + private void massEdit(List models, Consumer action) { getTabPane().setCursor(Cursor.WAIT); threadPool.submit(() -> { - List models = recorder.getModelsRecording(); - Exception ex = null; for (Model model : models) { - try { - recorder.resumeRecording(model); - } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) { - LOG.error("Couldn't resume model {}: {}", model, e.getMessage()); - ex = e; - } + action.accept(model); } - final Exception exc = ex; // stupid compiler - Platform.runLater(() -> { - getTabPane().setCursor(Cursor.DEFAULT); - if(exc != null) { - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Resume Model"); - alert.setHeaderText("Couldn't resume recording"); - alert.setContentText("At least one recording couldn't be resumed: " + exc.getMessage()); - alert.showAndWait(); - } - }); + Platform.runLater(() -> getTabPane().setCursor(Cursor.DEFAULT)); }); } @@ -405,16 +380,16 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } private ContextMenu createContextMenu() { - JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem(); - if(selectedModel == null) { + ObservableList selectedModels = table.getSelectionModel().getSelectedItems(); + if(selectedModels.isEmpty()) { return null; } MenuItem stop = new MenuItem("Remove Model"); - stop.setOnAction((e) -> stopAction()); + stop.setOnAction((e) -> stopAction(selectedModels)); MenuItem copyUrl = new MenuItem("Copy URL"); copyUrl.setOnAction((e) -> { - Model selected = selectedModel; + Model selected = selectedModels.get(0); final Clipboard clipboard = Clipboard.getSystemClipboard(); final ClipboardContent content = new ClipboardContent(); content.putString(selected.getUrl()); @@ -422,19 +397,31 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { }); MenuItem pauseRecording = new MenuItem("Pause Recording"); - pauseRecording.setOnAction((e) -> pauseRecording()); + pauseRecording.setOnAction((e) -> pauseRecording(selectedModels)); MenuItem resumeRecording = new MenuItem("Resume Recording"); - resumeRecording.setOnAction((e) -> resumeRecording()); + resumeRecording.setOnAction((e) -> resumeRecording(selectedModels)); MenuItem openInBrowser = new MenuItem("Open in Browser"); - openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModel.getUrl())); + openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModels.get(0).getUrl())); MenuItem openInPlayer = new MenuItem("Open in Player"); - openInPlayer.setOnAction((e) -> openInPlayer(selectedModel)); + openInPlayer.setOnAction((e) -> openInPlayer(selectedModels.get(0))); MenuItem switchStreamSource = new MenuItem("Switch resolution"); - switchStreamSource.setOnAction((e) -> switchStreamSource(selectedModel)); + switchStreamSource.setOnAction((e) -> switchStreamSource(selectedModels.get(0))); ContextMenu menu = new ContextMenu(stop); - menu.getItems().add(selectedModel.isSuspended() ? resumeRecording : pauseRecording); + if(selectedModels.size() == 1) { + menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording); + } else { + menu.getItems().addAll(resumeRecording, pauseRecording); + } menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, switchStreamSource); + + if(selectedModels.size() > 1) { + copyUrl.setDisable(true); + openInPlayer.setDisable(true); + openInBrowser.setDisable(true); + switchStreamSource.setDisable(true); + } + return menu; } @@ -488,94 +475,55 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } private void showStreamSwitchErrorDialog(Throwable throwable) { + showErrorDialog(throwable, "Couldn't switch stream resolution", "Error while switching stream resolution"); + } + + private void showErrorDialog(Throwable throwable, String header, String msg) { Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); alert.setTitle("Error"); - alert.setHeaderText("Couldn't switch stream resolution"); - alert.setContentText("Error while switching stream resolution: " + throwable.getLocalizedMessage()); + alert.setHeaderText(header); + alert.setContentText(msg + ": " + throwable.getLocalizedMessage()); alert.showAndWait(); } - private void stopAction() { - Model selected = table.getSelectionModel().getSelectedItem().getDelegate(); - if (selected != null) { - table.setCursor(Cursor.WAIT); - new Thread() { - @Override - public void run() { - try { - recorder.stopRecording(selected); - observableModels.remove(selected); - } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { - LOG.error("Couldn't stop recording", e1); - Platform.runLater(() -> { - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Error"); - alert.setHeaderText("Couldn't stop recording"); - alert.setContentText("Error while stopping the recording: " + e1.getLocalizedMessage()); - alert.showAndWait(); - }); - } finally { - table.setCursor(Cursor.DEFAULT); - } - } - }.start(); - } + private void stopAction(ObservableList selectedModels) { + Consumer action = (m) -> { + try { + recorder.stopRecording(m); + observableModels.remove(m); + } catch(Exception e) { + Platform.runLater(() -> + showErrorDialog(e, "Couldn't stop recording", "Stopping recording of " + m.getName() + " failed")); + } + }; + List models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList()); + massEdit(models, action); }; - private void pauseRecording() { - JavaFxModel model = table.getSelectionModel().getSelectedItem(); - Model delegate = table.getSelectionModel().getSelectedItem().getDelegate(); - if (delegate != null) { - table.setCursor(Cursor.WAIT); - new Thread() { - @Override - public void run() { - try { - recorder.suspendRecording(delegate); - Platform.runLater(() -> model.setSuspended(true)); - } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { - LOG.error("Couldn't pause recording", e1); - Platform.runLater(() -> { - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Error"); - alert.setHeaderText("Couldn't pause recording"); - alert.setContentText("Error while pausing the recording: " + e1.getLocalizedMessage()); - alert.showAndWait(); - }); - } finally { - table.setCursor(Cursor.DEFAULT); - } - } - }.start(); - } + private void pauseRecording(ObservableList selectedModels) { + Consumer action = (m) -> { + try { + recorder.suspendRecording(m); + } catch(Exception e) { + Platform.runLater(() -> + showErrorDialog(e, "Couldn't pause recording of model", "Pausing recording of " + m.getName() + " failed")); + } + }; + List models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList()); + massEdit(models, action); }; - private void resumeRecording() { - JavaFxModel model = table.getSelectionModel().getSelectedItem(); - Model delegate = table.getSelectionModel().getSelectedItem().getDelegate(); - if (delegate != null) { - table.setCursor(Cursor.WAIT); - new Thread() { - @Override - public void run() { - try { - recorder.resumeRecording(delegate); - Platform.runLater(() -> model.setSuspended(false)); - } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { - LOG.error("Couldn't resume recording", e1); - Platform.runLater(() -> { - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Error"); - alert.setHeaderText("Couldn't resume recording"); - alert.setContentText("Error while resuming the recording: " + e1.getLocalizedMessage()); - alert.showAndWait(); - }); - } finally { - table.setCursor(Cursor.DEFAULT); - } - } - }.start(); - } + private void resumeRecording(ObservableList selectedModels) { + Consumer action = (m) -> { + try { + recorder.resumeRecording(m); + } catch(Exception e) { + Platform.runLater(() -> + showErrorDialog(e, "Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed")); + } + }; + List models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList()); + massEdit(models, action); } public void saveState() { From 308a40210fbef32486dbe28132df67d054796898 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 29 Nov 2018 15:03:57 +0100 Subject: [PATCH 081/231] Load images with OkHttp instead of the built-in loader The built-in loader does not allow control over http headers etc. That is why we use OkHttp now. --- client/src/main/java/ctbrec/ui/ThumbCell.java | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index 54449c0c..fb8611f6 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -5,6 +5,8 @@ import java.io.IOException; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.function.Function; import org.slf4j.Logger; @@ -14,6 +16,7 @@ import com.iheartradio.m3u8.ParseException; import ctbrec.Config; import ctbrec.Model; +import ctbrec.io.HttpException; import ctbrec.recorder.Recorder; import ctbrec.ui.controls.Toast; import javafx.animation.FadeTransition; @@ -43,6 +46,8 @@ import javafx.scene.text.Font; import javafx.scene.text.Text; import javafx.scene.text.TextAlignment; import javafx.util.Duration; +import okhttp3.Request; +import okhttp3.Response; public class ThumbCell extends StackPane { @@ -74,6 +79,7 @@ public class ThumbCell extends StackPane { private ObservableList thumbCellList; private boolean mouseHovering = false; private boolean recording = false; + private static ExecutorService imageLoadingThreadPool = Executors.newFixedThreadPool(10); public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder) { this.thumbCellList = parent.grid.getChildren(); @@ -267,18 +273,35 @@ public class ThumbCell extends StackPane { if(!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { boolean updateThumbs = Config.getInstance().getSettings().updateThumbnails; if(updateThumbs || iv.getImage() == null) { - Image img = new Image(url, true); - - // wait for the image to load, otherwise the ImageView replaces the current image with an "empty" image, - // which causes to show the grey background until the image is loaded - img.progressProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, Number oldValue, Number newValue) { - if(newValue.doubleValue() == 1.0) { - //imgAspectRatio = img.getHeight() / img.getWidth(); - iv.setImage(img); - setThumbWidth(Config.getInstance().getSettings().thumbWidth); + imageLoadingThreadPool.submit(() -> { + Request req = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .build(); + try(Response resp = CamrecApplication.httpClient.execute(req)) { + if(resp.isSuccessful()) { + Image img = new Image(resp.body().byteStream()); + if(img.progressProperty().get() == 1.0) { + Platform.runLater(() -> { + iv.setImage(img); + setThumbWidth(Config.getInstance().getSettings().thumbWidth); + }); + } else { + img.progressProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, Number oldValue, Number newValue) { + if(newValue.doubleValue() == 1.0) { + iv.setImage(img); + setThumbWidth(Config.getInstance().getSettings().thumbWidth); + } + } + }); + } + } else { + throw new HttpException(resp.code(), resp.message()); } + } catch (IOException e) { + LOG.error("Error loading image", e); } }); } From da614caaa03cf1765b8355f4aea47443a2947cb8 Mon Sep 17 00:00:00 2001 From: 0xboobface <40739250+0xboobface@users.noreply.github.com> Date: Fri, 30 Nov 2018 13:19:48 +0100 Subject: [PATCH 082/231] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..5978a115 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,23 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Desktop (please complete the following information):** + - OS: [e.g. Windows, Mac, Linux] + - Ctbrec Version [e.g. 1.12.1 JRE] + - Standalone or Client / Server mode + +**Log** +If there are any errors in the ctbrec.log, please add them here. You can find the ctbrec.log next to the ctbrec.exe. From 7c98510eae98520ecfb0a626c13364dedc94b9b2 Mon Sep 17 00:00:00 2001 From: 0xboobface <40739250+0xboobface@users.noreply.github.com> Date: Fri, 30 Nov 2018 13:21:38 +0100 Subject: [PATCH 083/231] Update issue templates --- .github/ISSUE_TEMPLATE/feature_request.md | 17 +++++++++++++++++ .github/ISSUE_TEMPLATE/other-issue.md | 7 +++++++ .github/ISSUE_TEMPLATE/other.md | 7 +++++++ 3 files changed, 31 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/other-issue.md create mode 100644 .github/ISSUE_TEMPLATE/other.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..066b2d92 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/other-issue.md b/.github/ISSUE_TEMPLATE/other-issue.md new file mode 100644 index 00000000..09b2fc23 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/other-issue.md @@ -0,0 +1,7 @@ +--- +name: Other issue +about: Anything else + +--- + + diff --git a/.github/ISSUE_TEMPLATE/other.md b/.github/ISSUE_TEMPLATE/other.md new file mode 100644 index 00000000..3204c4f9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/other.md @@ -0,0 +1,7 @@ +--- +name: Other +about: Anything else + +--- + + From fca5a4ea1abf9c3455604a2314e25b26fbc38196 Mon Sep 17 00:00:00 2001 From: 0xboobface <40739250+0xboobface@users.noreply.github.com> Date: Fri, 30 Nov 2018 13:22:11 +0100 Subject: [PATCH 084/231] Update issue templates --- .github/ISSUE_TEMPLATE/other-issue.md | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/other-issue.md diff --git a/.github/ISSUE_TEMPLATE/other-issue.md b/.github/ISSUE_TEMPLATE/other-issue.md deleted file mode 100644 index 09b2fc23..00000000 --- a/.github/ISSUE_TEMPLATE/other-issue.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: Other issue -about: Anything else - ---- - - From 918f63b1f5db597667f594828a5f423a8f17c30f Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 30 Nov 2018 13:42:51 +0100 Subject: [PATCH 085/231] Use defaults, if settings cannot be loaded If the settings cannot be loaded, make a backup of the settings file and use the defaults, so that the application at least starts. --- .../java/ctbrec/ui/CamrecApplication.java | 3 +-- common/src/main/java/ctbrec/Config.java | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 72c0259d..b55622ff 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -250,9 +250,8 @@ public class CamrecApplication extends Application { LOG.error("Couldn't load settings", e); Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); alert.setTitle("Whoopsie"); - alert.setContentText("Couldn't load settings."); + alert.setContentText("Couldn't load settings. Falling back to defaults. A backup of your settings has been created."); alert.showAndWait(); - System.exit(1); } config = Config.getInstance(); } diff --git a/common/src/main/java/ctbrec/Config.java b/common/src/main/java/ctbrec/Config.java index 871c36ff..9170a5f1 100644 --- a/common/src/main/java/ctbrec/Config.java +++ b/common/src/main/java/ctbrec/Config.java @@ -7,6 +7,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; @@ -46,7 +47,6 @@ public class Config { } else { filename = "settings.json"; } - load(); } private void load() throws FileNotFoundException, IOException { @@ -61,6 +61,13 @@ public class Config { BufferedSource source = buffer.readFrom(fin); settings = adapter.fromJson(source); settings.httpTimeout = Math.max(settings.httpTimeout, 10_000); + } catch(Throwable e) { + settings = OS.getDefaultSettings(); + for (Site site : sites) { + site.setEnabled(!settings.disabledSites.contains(site.getName())); + } + makeBackup(configFile); + throw e; } } else { LOG.error("Config file does not exist. Falling back to default values."); @@ -71,9 +78,22 @@ public class Config { } } + private void makeBackup(File source) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); + String timestamp = sdf.format(new Date()); + String backup = source.getName() + '.' + timestamp; + File target = new File(source.getParentFile(), backup); + Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch(Throwable e) { + LOG.error("Couldn't create backup of settings file", e); + } + } + public static synchronized void init(List sites) throws FileNotFoundException, IOException { if(instance == null) { instance = new Config(sites); + instance.load(); } } From 76657e2b920a9487fc9fc01150c991fa867853cc Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 30 Nov 2018 13:44:13 +0100 Subject: [PATCH 086/231] Increase thumb loading thread pool size from 10 to 30 --- client/src/main/java/ctbrec/ui/ThumbCell.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index fb8611f6..cd46672d 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -79,7 +79,7 @@ public class ThumbCell extends StackPane { private ObservableList thumbCellList; private boolean mouseHovering = false; private boolean recording = false; - private static ExecutorService imageLoadingThreadPool = Executors.newFixedThreadPool(10); + private static ExecutorService imageLoadingThreadPool = Executors.newFixedThreadPool(30); public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder) { this.thumbCellList = parent.grid.getChildren(); From ad1f841167e436c6c27e0d2ff1e280c379ce0968 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 30 Nov 2018 14:51:17 +0100 Subject: [PATCH 087/231] Don't do space check, if minimum is set to 0 --- common/src/main/java/ctbrec/recorder/LocalRecorder.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index e6df866b..c46ced4d 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -787,6 +787,10 @@ public class LocalRecorder implements Recorder { private boolean enoughSpaceForRecording() throws IOException { long minimum = config.getSettings().minimumSpaceLeftInBytes; - return getFreeSpaceBytes() > minimum; + if(minimum == 0) { // 0 means don't check + return true; + } else { + return getFreeSpaceBytes() > minimum; + } } } From fedf38004d389b48d1157e2eac6f4d98891f9459 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 30 Nov 2018 15:04:02 +0100 Subject: [PATCH 088/231] Don't log error, if recordings dir does not exist If the recordings dir does not exist, don't log an error, but instead set the tooltip to show the problem --- client/src/main/java/ctbrec/ui/RecordingsTab.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index 32b8e3c0..439cd5e4 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -6,6 +6,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.net.URL; +import java.nio.file.NoSuchFileException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.DecimalFormat; @@ -316,6 +317,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener { try { spaceTotal = recorder.getTotalSpaceBytes(); spaceFree = recorder.getFreeSpaceBytes(); + Platform.runLater(() -> spaceLeft.setTooltip(new Tooltip())); + } catch (NoSuchFileException e) { + // recordings dir does not exist + Platform.runLater(() -> spaceLeft.setTooltip(new Tooltip("Recordings directory does not exist"))); } catch (IOException e) { LOG.error("Couldn't update free space", e); } From 40b3b78e5289a20741fa45e2341ea09ae61a869b Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 30 Nov 2018 15:21:01 +0100 Subject: [PATCH 089/231] Implement multi-selection for Recordings table --- .../main/java/ctbrec/ui/RecordingsTab.java | 75 ++++++++++++------- 1 file changed, 48 insertions(+), 27 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index 439cd5e4..44a43e9d 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -53,6 +53,7 @@ import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.ProgressBar; import javafx.scene.control.ScrollPane; +import javafx.scene.control.SelectionMode; import javafx.scene.control.Tab; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; @@ -115,6 +116,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { BorderPane.setMargin(scrollPane, new Insets(5)); table.setEditable(false); + table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); TableColumn name = new TableColumn<>("Model"); name.setPrefWidth(200); name.setCellValueFactory(new PropertyValueFactory("modelName")); @@ -183,9 +185,9 @@ public class RecordingsTab extends Tab implements TabSelectionListener { table.getColumns().addAll(name, date, status, progress, size); table.setItems(observableRecordings); table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { - Recording recording = table.getSelectionModel().getSelectedItem(); - if(recording != null) { - popup = createContextMenu(recording); + List recordings = table.getSelectionModel().getSelectedItems(); + if(recordings != null && !recordings.isEmpty()) { + popup = createContextMenu(recordings); if(!popup.getItems().isEmpty()) { popup.show(table, event.getScreenX(), event.getScreenY()); } @@ -206,13 +208,15 @@ public class RecordingsTab extends Tab implements TabSelectionListener { } }); table.addEventFilter(KeyEvent.KEY_PRESSED, event -> { - JavaFxRecording recording = table.getSelectionModel().getSelectedItem(); - if (recording != null) { + List recordings = table.getSelectionModel().getSelectedItems(); + if (recordings != null && !recordings.isEmpty()) { if (event.getCode() == KeyCode.DELETE) { - delete(recording); + if(recordings.size() > 1 || recordings.get(0).getStatus() == STATUS.FINISHED) { + delete(recordings); + } } else if (event.getCode() == KeyCode.ENTER) { - if(recording.getStatus() == STATUS.FINISHED) { - play(recording); + if(recordings.get(0).getStatus() == STATUS.FINISHED) { + play(recordings.get(0)); } } } @@ -356,7 +360,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { } } - private ContextMenu createContextMenu(Recording recording) { + private ContextMenu createContextMenu(List recordings) { ContextMenu contextMenu = new ContextMenu(); contextMenu.setHideOnEscape(true); contextMenu.setAutoHide(true); @@ -364,9 +368,9 @@ public class RecordingsTab extends Tab implements TabSelectionListener { MenuItem openInPlayer = new MenuItem("Open in Player"); openInPlayer.setOnAction((e) -> { - play(recording); + play(recordings.get(0)); }); - if(recording.getStatus() == STATUS.FINISHED || Config.getInstance().getSettings().localRecording) { + if(recordings.get(0).getStatus() == STATUS.FINISHED || Config.getInstance().getSettings().localRecording) { contextMenu.getItems().add(openInPlayer); } @@ -386,16 +390,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener { MenuItem deleteRecording = new MenuItem("Delete"); deleteRecording.setOnAction((e) -> { - delete(recording); + delete(recordings); }); - if(recording.getStatus() == STATUS.FINISHED) { + if(recordings.get(0).getStatus() == STATUS.FINISHED || recordings.size() > 1) { contextMenu.getItems().add(deleteRecording); } MenuItem openDir = new MenuItem("Open directory"); openDir.setOnAction((e) -> { String recordingsDir = Config.getInstance().getSettings().recordingsDir; - String path = recording.getPath(); + String path = recordings.get(0).getPath(); File tsFile = new File(recordingsDir, path); new Thread(() -> { DesktopIntegration.open(tsFile.getParent()); @@ -408,16 +412,22 @@ public class RecordingsTab extends Tab implements TabSelectionListener { MenuItem downloadRecording = new MenuItem("Download"); downloadRecording.setOnAction((e) -> { try { - download(recording); + download(recordings.get(0)); } catch (IOException | ParseException | PlaylistException e1) { showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e1); LOG.error("Error while downloading recording", e1); } }); - if (!Config.getInstance().getSettings().localRecording && recording.getStatus() == STATUS.FINISHED) { + if (!Config.getInstance().getSettings().localRecording && recordings.get(0).getStatus() == STATUS.FINISHED) { contextMenu.getItems().add(downloadRecording); } + if(recordings.size() > 1) { + openInPlayer.setDisable(true); + openDir.setDisable(true); + downloadRecording.setDisable(true); + } + return contextMenu; } @@ -523,12 +533,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener { } - private void delete(Recording r) { - if(r.getStatus() != STATUS.FINISHED) { - return; - } + private void delete(List recordings) { table.setCursor(Cursor.WAIT); - String msg = "Delete " + r.getModelName() + "/" + r.getStartDate() + " for good?"; + + String msg; + if(recordings.size() > 1) { + msg = "Delete " + recordings.size() + " recordings for good?"; + } else { + Recording r = recordings.get(0); + msg = "Delete " + r.getModelName() + "/" + r.getStartDate() + " for good?"; + } AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, msg, YES, NO); confirm.setTitle("Delete recording?"); confirm.setHeaderText(msg); @@ -539,13 +553,20 @@ public class RecordingsTab extends Tab implements TabSelectionListener { @Override public void run() { try { - recorder.delete(r); - Platform.runLater(() -> observableRecordings.remove(r)); - } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { - LOG.error("Error while deleting recording", e1); - showErrorDialog("Error while deleting recording", "Recording not deleted", e1); + for (JavaFxRecording r : recordings) { + if(r.getStatus() != STATUS.FINISHED) { + continue; + } + try { + recorder.delete(r); + Platform.runLater(() -> observableRecordings.remove(r)); + } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { + LOG.error("Error while deleting recording", e1); + showErrorDialog("Error while deleting recording", "Recording not deleted", e1); + } + } } finally { - table.setCursor(Cursor.DEFAULT); + Platform.runLater(() -> table.setCursor(Cursor.DEFAULT)); } } }; From b4f25c29ca928b1b480a616cb6fd12bd27c867bc Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 30 Nov 2018 15:30:30 +0100 Subject: [PATCH 090/231] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51b8e6a3..1b160520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ * Fix: Player not starting when path contains spaces * Added setting to toggle "Player Starting" message * Added possibility to add models by their URL +* Added pause / resume all buttons +* Implemented multi-selection for Recording and Recordings tab +* Fix: Don't do space check, if minimum is set to 0 1.12.1 ======================== From ecf9fc27464f0c56e826a794b4eb4aba9579e36c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 30 Nov 2018 15:38:52 +0100 Subject: [PATCH 091/231] Add key listener for P to pause and resume selected models --- .../src/main/java/ctbrec/ui/RecordedModelsTab.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index acc72ce1..2861882d 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -139,8 +139,14 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } }); table.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + List selectedModels = table.getSelectionModel().getSelectedItems(); if(event.getCode() == KeyCode.DELETE) { - stopAction(table.getSelectionModel().getSelectedItems()); + stopAction(selectedModels); + } else if(event.getCode() == KeyCode.P) { + List pausedModels = selectedModels.stream().filter(m -> m.isSuspended()).collect(Collectors.toList()); + List runningModels = selectedModels.stream().filter(m -> !m.isSuspended()).collect(Collectors.toList()); + resumeRecording(pausedModels); + pauseRecording(runningModels); } }); scrollPane.setContent(table); @@ -486,7 +492,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { alert.showAndWait(); } - private void stopAction(ObservableList selectedModels) { + private void stopAction(List selectedModels) { Consumer action = (m) -> { try { recorder.stopRecording(m); @@ -500,7 +506,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { massEdit(models, action); }; - private void pauseRecording(ObservableList selectedModels) { + private void pauseRecording(List selectedModels) { Consumer action = (m) -> { try { recorder.suspendRecording(m); @@ -513,7 +519,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { massEdit(models, action); }; - private void resumeRecording(ObservableList selectedModels) { + private void resumeRecording(List selectedModels) { Consumer action = (m) -> { try { recorder.resumeRecording(m); From 0e627aef12a8dd0fb5d3aca307e68f4d55497c43 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 30 Nov 2018 17:01:01 +0100 Subject: [PATCH 092/231] Make paused checkboxes clickable --- .../java/ctbrec/ui/RecordedModelsTab.java | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index 2861882d..86e99b6a 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -3,6 +3,7 @@ package ctbrec.ui; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; @@ -104,45 +105,50 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { scrollPane.setFitToWidth(true); BorderPane.setMargin(scrollPane, new Insets(5)); - table.setEditable(false); + table.setEditable(true); table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); TableColumn name = new TableColumn<>("Model"); name.setPrefWidth(200); name.setCellValueFactory(new PropertyValueFactory("name")); + name.setEditable(false); TableColumn url = new TableColumn<>("URL"); url.setCellValueFactory(new PropertyValueFactory("url")); url.setPrefWidth(400); + url.setEditable(false); TableColumn online = new TableColumn<>("Online"); online.setCellValueFactory((cdf) -> cdf.getValue().getOnlineProperty()); online.setCellFactory(CheckBoxTableCell.forTableColumn(online)); online.setPrefWidth(100); + online.setEditable(false); TableColumn recording = new TableColumn<>("Recording"); recording.setCellValueFactory((cdf) -> cdf.getValue().getRecordingProperty()); recording.setCellFactory(CheckBoxTableCell.forTableColumn(recording)); recording.setPrefWidth(100); + recording.setEditable(false); TableColumn paused = new TableColumn<>("Paused"); paused.setCellValueFactory((cdf) -> cdf.getValue().getPausedProperty()); paused.setCellFactory(CheckBoxTableCell.forTableColumn(paused)); paused.setPrefWidth(100); + paused.setEditable(true); table.getColumns().addAll(name, url, online, recording, paused); table.setItems(observableModels); table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { popup = createContextMenu(); - if(popup != null) { + if (popup != null) { popup.show(table, event.getScreenX(), event.getScreenY()); } event.consume(); }); table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { - if(popup != null) { + if (popup != null) { popup.hide(); } }); table.addEventFilter(KeyEvent.KEY_PRESSED, event -> { List selectedModels = table.getSelectionModel().getSelectedItems(); - if(event.getCode() == KeyCode.DELETE) { + if (event.getCode() == KeyCode.DELETE) { stopAction(selectedModels); - } else if(event.getCode() == KeyCode.P) { + } else if (event.getCode() == KeyCode.P) { List pausedModels = selectedModels.stream().filter(m -> m.isSuspended()).collect(Collectors.toList()); List runningModels = selectedModels.stream().filter(m -> !m.isSuspended()).collect(Collectors.toList()); resumeRecording(pausedModels); @@ -179,11 +185,11 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { private void addModel(ActionEvent e) { String input = model.getText(); - if(StringUtil.isBlank(input)) { + if (StringUtil.isBlank(input)) { return; } - if(input.startsWith("http")) { + if (input.startsWith("http")) { addModelByUrl(input); } else { addModelByName(input); @@ -193,7 +199,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { private void addModelByUrl(String url) { for (Site site : sites) { Model model = site.createModelFromUrl(url); - if(model != null) { + if (model != null) { try { recorder.startRecording(model); } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { @@ -291,7 +297,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2))); updateService.setOnSucceeded((event) -> { List models = updateService.getValue(); - if(models == null) { + if (models == null) { return; } @@ -299,6 +305,13 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { int index = observableModels.indexOf(updatedModel); if (index == -1) { observableModels.add(updatedModel); + updatedModel.getPausedProperty().addListener((obs, oldV, newV) -> { + if(newV) { + pauseRecording(Collections.singletonList(updatedModel)); + } else { + resumeRecording(Collections.singletonList(updatedModel)); + } + }); } else { // make sure to update the JavaFX online property, so that the table cell is updated JavaFxModel oldModel = observableModels.get(index); @@ -387,7 +400,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { private ContextMenu createContextMenu() { ObservableList selectedModels = table.getSelectionModel().getSelectedItems(); - if(selectedModels.isEmpty()) { + if (selectedModels.isEmpty()) { return null; } MenuItem stop = new MenuItem("Remove Model"); @@ -414,14 +427,14 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { switchStreamSource.setOnAction((e) -> switchStreamSource(selectedModels.get(0))); ContextMenu menu = new ContextMenu(stop); - if(selectedModels.size() == 1) { + if (selectedModels.size() == 1) { menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording); } else { menu.getItems().addAll(resumeRecording, pauseRecording); } menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, switchStreamSource); - if(selectedModels.size() > 1) { + if (selectedModels.size() > 1) { copyUrl.setDisable(true); openInPlayer.setDisable(true); openInBrowser.setDisable(true); @@ -436,7 +449,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { new Thread(() -> { boolean started = Player.play(selectedModel); Platform.runLater(() -> { - if(started && Config.getInstance().getSettings().showPlayerStarting) { + if (started && Config.getInstance().getSettings().showPlayerStarting) { Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500); } table.setCursor(Cursor.DEFAULT); @@ -446,7 +459,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { private void switchStreamSource(JavaFxModel fxModel) { try { - if(!fxModel.isOnline()) { + if (!fxModel.isOnline()) { Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION); alert.setTitle("Switch resolution"); alert.setHeaderText("Couldn't switch stream resolution"); @@ -533,7 +546,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } public void saveState() { - if(!table.getSortOrder().isEmpty()) { + if (!table.getSortOrder().isEmpty()) { TableColumn col = table.getSortOrder().get(0); Config.getInstance().getSettings().recordedModelsSortColumn = col.getText(); Config.getInstance().getSettings().recordedModelsSortType = col.getSortType().toString(); @@ -547,9 +560,9 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { private void restoreState() { String sortCol = Config.getInstance().getSettings().recordedModelsSortColumn; - if(StringUtil.isNotBlank(sortCol)) { + if (StringUtil.isNotBlank(sortCol)) { for (TableColumn col : table.getColumns()) { - if(Objects.equals(sortCol, col.getText())) { + if (Objects.equals(sortCol, col.getText())) { col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordedModelsSortType)); table.getSortOrder().clear(); table.getSortOrder().add(col); @@ -559,7 +572,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } double[] columnWidths = Config.getInstance().getSettings().recordedModelsColumnWidths; - if(columnWidths != null && columnWidths.length == table.getColumns().size()) { + if (columnWidths != null && columnWidths.length == table.getColumns().size()) { for (int i = 0; i < columnWidths.length; i++) { table.getColumns().get(i).setPrefWidth(columnWidths[i]); } From 52016c6a86eda1a3ce9d9e4b1285272fbd100f62 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 00:16:44 +0100 Subject: [PATCH 093/231] Make sure, the hlsUrl is available, when loading the master playlist --- .../main/java/ctbrec/sites/mfc/MyFreeCamsModel.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index 6ca46f26..e0937cb5 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -99,7 +99,7 @@ public class MyFreeCamsModel extends AbstractModel { } private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException { - if(hlsUrl == null) { + if(getHlsUrl() == null) { throw new IllegalStateException("Stream url unknown"); } LOG.trace("Loading master playlist {}", hlsUrl); @@ -117,6 +117,14 @@ public class MyFreeCamsModel extends AbstractModel { } } + private String getHlsUrl() { + if(hlsUrl == null) { + MyFreeCams mfc = (MyFreeCams) getSite(); + mfc.getClient().update(this); + } + return hlsUrl; + } + @Override public void invalidateCacheEntries() { resolution = null; From 754271c4661104ad20e3f6fa7bce66abf34667df Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 00:29:20 +0100 Subject: [PATCH 094/231] Add column which opens a preview popup, when hovered over Add a column to the recorded models table, which can be used to open a small preview popup. The popup opens, when the mouse hovers over the table cell for a certain amount of time or if the cell gets clicked. The preview plays the stream with the lowest quality without audio. --- .../java/ctbrec/ui/PreviewPopupHandler.java | 314 ++++++++++++++++++ .../java/ctbrec/ui/RecordedModelsTab.java | 45 ++- 2 files changed, 349 insertions(+), 10 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/PreviewPopupHandler.java diff --git a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java new file mode 100644 index 00000000..42d5d960 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java @@ -0,0 +1,314 @@ +package ctbrec.ui; + +import java.io.InterruptedIOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.recorder.download.StreamSource; +import javafx.application.Platform; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableRow; +import javafx.scene.control.TableView; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; +import javafx.scene.media.MediaView; +import javafx.stage.Popup; + +public class PreviewPopupHandler implements EventHandler { + private static final transient Logger LOG = LoggerFactory.getLogger(PreviewPopupHandler.class); + + private static final int offset = 10; + private long timeForPopupOpen = TimeUnit.SECONDS.toMillis(1); + private long timeForPopupClose = 400; + private Popup popup = new Popup(); + private Node parent; + private ImageView preview = new ImageView(); + private MediaView videoPreview; + private MediaPlayer videoPlayer; + private Media video; + private JavaFxModel model; + private volatile long openCountdown = -1; + private volatile long closeCountdown = -1; + private volatile long lastModelChange = -1; + private volatile boolean changeModel = false; + private ExecutorService executor = Executors.newSingleThreadExecutor(); + private Future future; + private ProgressIndicator progressIndicator; + private StackPane pane; + + public PreviewPopupHandler(Node parent) { + this.parent = parent; + + videoPreview = new MediaView(); + videoPreview.setFitWidth(Config.getInstance().getSettings().thumbWidth); + videoPreview.setFitHeight(videoPreview.getFitWidth() * 9 / 16); + videoPreview.setPreserveRatio(true); + StackPane.setMargin(videoPreview, new Insets(5)); + + preview.setFitWidth(Config.getInstance().getSettings().thumbWidth); + preview.setPreserveRatio(true); + preview.setSmooth(true); + preview.setStyle("-fx-background-radius: 10px, 10px, 10px, 10px;"); + preview.visibleProperty().bind(videoPreview.visibleProperty().not()); + StackPane.setMargin(preview, new Insets(5)); + + progressIndicator = new ProgressIndicator(); + progressIndicator.setVisible(false); + progressIndicator.prefWidthProperty().bind(videoPreview.fitWidthProperty()); + + Region veil = new Region(); + veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.8)"); + veil.visibleProperty().bind(progressIndicator.visibleProperty()); + StackPane.setMargin(veil, new Insets(5)); + + pane = new StackPane(); + pane.getChildren().addAll(preview, videoPreview, veil, progressIndicator); + pane.setStyle("-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-base;"+ + "-fx-background-insets: 0 0 -1 0, 0, 1, 2;" + + "-fx-background-radius: 10px, 10px, 10px, 10px;" + + "-fx-padding: 1;" + + "-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.8), 20, 0, 0, 0);"); + popup.getContent().add(pane); + + createTimerThread(); + } + + @Override + public void handle(MouseEvent event) { + if(!isInPreviewColumn(event)) { + closeCountdown = timeForPopupClose; + return; + } + + if(event.getEventType() == MouseEvent.MOUSE_CLICKED && event.getButton() == MouseButton.PRIMARY) { + model = getModel(event); + popup.setX(event.getScreenX()+ offset); + popup.setY(event.getScreenY()+ offset); + showPopup(); + openCountdown = -1; + } else if(event.getEventType() == MouseEvent.MOUSE_ENTERED) { + popup.setX(event.getScreenX()+ offset); + popup.setY(event.getScreenY()+ offset); + JavaFxModel model = getModel(event); + if(model != null) { + closeCountdown = -1; + boolean modelChanged = model != this.model; + this.model = model; + if(popup.isShowing()) { + openCountdown = -1; + if(modelChanged) { + lastModelChange = System.currentTimeMillis(); + changeModel = true; + future.cancel(true); + progressIndicator.setVisible(true); + } + } else { + openCountdown = timeForPopupOpen; + } + } + } else if(event.getEventType() == MouseEvent.MOUSE_EXITED) { + openCountdown = -1; + closeCountdown = timeForPopupClose; + model = null; + } else if(event.getEventType() == MouseEvent.MOUSE_MOVED) { + popup.setX(event.getScreenX() + offset); + popup.setY(event.getScreenY() + offset); + } + } + + private boolean isInPreviewColumn(MouseEvent event) { + @SuppressWarnings("unchecked") + TableRow row = (TableRow) event.getSource(); + TableView table = row.getTableView(); + double offset = 0; + double width = 0; + for (TableColumn col : table.getColumns()) { + offset += width; + width = col.getWidth(); + if(Objects.equals(col.getId(), "preview")) { + Point2D screenToLocal = table.screenToLocal(event.getScreenX(), event.getScreenY()); + double x = screenToLocal.getX(); + return x >= offset && x <= offset + width; + } + } + return false; + } + + private JavaFxModel getModel(MouseEvent event) { + @SuppressWarnings("unchecked") + TableRow row = (TableRow) event.getSource(); + TableView table = row.getTableView(); + int rowIndex = row.getIndex(); + if(rowIndex < table.getItems().size()) { + return table.getItems().get(rowIndex); + } else { + return null; + } + } + + private void showPopup() { + startStream(model); + } + + private void startStream(JavaFxModel model) { + if(future != null && !future.isDone()) { + future.cancel(true); + } + future = executor.submit(() -> { + try { + Platform.runLater(() -> { + progressIndicator.setVisible(true); + popup.show(parent.getScene().getWindow()); + }); + List sources = model.getStreamSources(); + Collections.sort(sources); + StreamSource best = sources.get(0); + checkInterrupt(); + video = new Media(best.getMediaPlaylistUrl()); + if(videoPlayer != null) { + videoPlayer.dispose(); + } + videoPlayer = new MediaPlayer(video); + videoPlayer.setMute(true); + checkInterrupt(); + videoPlayer.setOnReady(() -> { + if(!future.isCancelled()) { + Platform.runLater(() -> { + double aspect = (double)video.getWidth() / video.getHeight(); + double w = Config.getInstance().getSettings().thumbWidth; + double h = w / aspect; + videoPreview.setVisible(true); + videoPreview.setMediaPlayer(videoPlayer); + resize(w, h); + videoPlayer.play(); + progressIndicator.setVisible(false); + }); + } + }); + } catch (IllegalStateException e) { + if(e.getMessage().equals("Stream url unknown")) { + // fine hls url for mfc not known yet + } else { + LOG.error("Couldn't start preview video", e); + } + showTestImage(); + } catch (InterruptedException | InterruptedIOException e) { + // future has been canceled, that's fine + } catch (ExecutionException e) { + if(e.getCause() instanceof InterruptedException || e.getCause() instanceof InterruptedIOException) { + // future has been canceled, that's fine + } else { + LOG.error("Couldn't start preview video", e); + showTestImage(); + } + } catch (Exception e) { + LOG.error("Couldn't start preview video", e); + showTestImage(); + } + }); + } + + private void resize(double w, double h) { + preview.setFitWidth(w); + preview.setFitHeight(h); + videoPreview.setFitWidth(w); + videoPreview.setFitHeight(h); + pane.setPrefSize(w, h); + popup.setWidth(w); + popup.setHeight(h); + } + + private void checkInterrupt() throws InterruptedException { + if(Thread.interrupted()) { + throw new InterruptedException(); + } + } + + private void showTestImage() { + Platform.runLater(() -> { + videoPreview.setVisible(false); + Image img = new Image(getClass().getResource("/image_not_found.png").toString(), true); + preview.setImage(img); + double aspect = img.getWidth() / img.getHeight(); + double w = Config.getInstance().getSettings().thumbWidth; + double h = w / aspect; + resize(w, h); + progressIndicator.setVisible(false); + }); + } + + private void hidePopup() { + Platform.runLater(() -> { + popup.setX(-1000); + popup.setY(-1000); + popup.hide(); + if(videoPlayer != null) { + videoPlayer.dispose(); + } + }); + } + + private void createTimerThread() { + Thread timerThread = new Thread(() -> { + while(true) { + openCountdown--; + if(openCountdown == 0) { + openCountdown = -1; + if(model != null) { + showPopup(); + } + } + + closeCountdown--; + if(closeCountdown == 0) { + hidePopup(); + closeCountdown = -1; + } + + openCountdown = Math.max(openCountdown, -1); + closeCountdown = Math.max(closeCountdown, -1); + + long now = System.currentTimeMillis(); + long diff = (now - lastModelChange); + if(changeModel && diff > 400) { + changeModel = false; + if(model != null) { + startStream(model); + } + } + + try { + Thread.sleep(1); + } catch (InterruptedException e) { + LOG.error("PreviewPopupTimer interrupted"); + break; + } + } + }); + timerThread.setDaemon(true); + timerThread.setPriority(Thread.MIN_PRIORITY); + timerThread.setName("PreviewPopupTimer"); + timerThread.start(); + } +} diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index 86e99b6a..f14fb35e 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -31,6 +31,9 @@ import ctbrec.sites.Site; import ctbrec.ui.controls.AutoFillTextField; import ctbrec.ui.controls.Toast; import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.concurrent.ScheduledService; @@ -48,6 +51,7 @@ import javafx.scene.control.SelectionMode; import javafx.scene.control.Tab; import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn.SortType; +import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.control.cell.CheckBoxTableCell; @@ -105,8 +109,20 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { scrollPane.setFitToWidth(true); BorderPane.setMargin(scrollPane, new Insets(5)); + table.setEditable(true); table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + PreviewPopupHandler previewPopupHandler = new PreviewPopupHandler(table); + table.setRowFactory((tableview) -> { + TableRow row = new TableRow<>(); + row.addEventHandler(MouseEvent.ANY, previewPopupHandler); + return row; + }); + TableColumn preview = new TableColumn<>("🎥"); + preview.setPrefWidth(35); + preview.setCellValueFactory(cdf -> new SimpleStringProperty(" ▶ ")); + preview.setEditable(false); + preview.setId("preview"); TableColumn name = new TableColumn<>("Model"); name.setPrefWidth(200); name.setCellValueFactory(new PropertyValueFactory("name")); @@ -116,21 +132,21 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { url.setPrefWidth(400); url.setEditable(false); TableColumn online = new TableColumn<>("Online"); - online.setCellValueFactory((cdf) -> cdf.getValue().getOnlineProperty()); + online.setCellValueFactory(cdf -> cdf.getValue().getOnlineProperty()); online.setCellFactory(CheckBoxTableCell.forTableColumn(online)); online.setPrefWidth(100); online.setEditable(false); TableColumn recording = new TableColumn<>("Recording"); - recording.setCellValueFactory((cdf) -> cdf.getValue().getRecordingProperty()); + recording.setCellValueFactory(cdf -> cdf.getValue().getRecordingProperty()); recording.setCellFactory(CheckBoxTableCell.forTableColumn(recording)); recording.setPrefWidth(100); recording.setEditable(false); TableColumn paused = new TableColumn<>("Paused"); - paused.setCellValueFactory((cdf) -> cdf.getValue().getPausedProperty()); + paused.setCellValueFactory(cdf -> cdf.getValue().getPausedProperty()); paused.setCellFactory(CheckBoxTableCell.forTableColumn(paused)); paused.setPrefWidth(100); paused.setEditable(true); - table.getColumns().addAll(name, url, online, recording, paused); + table.getColumns().addAll(preview, name, url, online, recording, paused); table.setItems(observableModels); table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { popup = createContextMenu(); @@ -144,7 +160,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { popup.hide(); } }); - table.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + table.addEventHandler(KeyEvent.KEY_PRESSED, event -> { List selectedModels = table.getSelectionModel().getSelectedItems(); if (event.getCode() == KeyCode.DELETE) { stopAction(selectedModels); @@ -305,11 +321,20 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { int index = observableModels.indexOf(updatedModel); if (index == -1) { observableModels.add(updatedModel); - updatedModel.getPausedProperty().addListener((obs, oldV, newV) -> { - if(newV) { - pauseRecording(Collections.singletonList(updatedModel)); - } else { - resumeRecording(Collections.singletonList(updatedModel)); + updatedModel.getPausedProperty().addListener(new ChangeListener() { + boolean firstChange = true; + @Override + public void changed(ObservableValue obs, Boolean oldV, Boolean newV) { + if(firstChange) { + // don't react to the first change, because that is made by the recorder and not by the user + firstChange = false; + return; + } + if (newV) { + pauseRecording(Collections.singletonList(updatedModel)); + } else { + resumeRecording(Collections.singletonList(updatedModel)); + } } }); } else { From b44a1c24228f37eeba14955ada413331887b52fc Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 02:12:27 +0100 Subject: [PATCH 095/231] Fix stream source selection --- .../ui/StreamSourceSelectionDialog.java | 5 ++++- .../java/ctbrec/recorder/LocalRecorder.java | 19 +++++++++++++------ .../download/AbstractHlsDownload.java | 10 +++++++++- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java b/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java index 76a90059..2347493b 100644 --- a/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java +++ b/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java @@ -1,5 +1,6 @@ package ctbrec.ui; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -15,7 +16,9 @@ public class StreamSourceSelectionDialog { Task> selectStreamSource = new Task>() { @Override protected List call() throws Exception { - return model.getStreamSources(); + List sources = model.getStreamSources(); + Collections.sort(sources); + return sources; } }; selectStreamSource.setOnSucceeded((e) -> { diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index c46ced4d..c2017ab5 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -710,13 +710,20 @@ public class LocalRecorder implements Recorder { @Override public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { - LOG.debug("Switching stream source to index {} for model {}", model.getStreamUrlIndex(), model.getName()); - Download download = recordingProcesses.get(model); - if(download != null) { - stopRecordingProcess(model); + if (models.contains(model)) { + int index = models.indexOf(model); + models.get(index).setStreamUrlIndex(model.getStreamUrlIndex()); + config.save(); + LOG.debug("Switching stream source to index {} for model {}", model.getStreamUrlIndex(), model.getName()); + Download download = recordingProcesses.get(model); + if(download != null) { + stopRecordingProcess(model); + } + tryRestartRecording(model); + } else { + LOG.warn("Couldn't switch stream source for model {}. Not found in list", model.getName()); + return; } - tryRestartRecording(model); - config.save(); } @Override diff --git a/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java index 04b11402..1fb6333d 100644 --- a/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java @@ -83,12 +83,18 @@ public abstract class AbstractHlsDownload implements Download { String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException { + LOG.debug("{} stream idx: {}", model.getName(), model.getStreamUrlIndex()); List streamSources = model.getStreamSources(); + Collections.sort(streamSources); + for (StreamSource streamSource : streamSources) { + LOG.debug("{} src {}", model.getName(), streamSource); + } String url = null; if(model.getStreamUrlIndex() >= 0 && model.getStreamUrlIndex() < streamSources.size()) { + // TODO don't use the index, but the bandwidth. if the bandwidth does not match, take the closest one + LOG.debug("{} selected {}", model.getName(), streamSources.get(model.getStreamUrlIndex())); url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl(); } else { - Collections.sort(streamSources); // filter out stream resolutions, which are too high int maxRes = Config.getInstance().getSettings().maximumResolution; if(maxRes > 0) { @@ -103,9 +109,11 @@ public abstract class AbstractHlsDownload implements Download { if(streamSources.isEmpty()) { throw new ExecutionException(new RuntimeException("No stream left in playlist")); } else { + LOG.debug("{} selected {}", model.getName(), streamSources.get(streamSources.size()-1)); url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl(); } } + LOG.debug("Segment playlist url {}", url); return url; } From 53f77afb851a48fa536f24d5b6fecb35c8471935 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 14:01:29 +0100 Subject: [PATCH 096/231] Fix concurrent modification bug in delete method --- .../main/java/ctbrec/ui/RecordingsTab.java | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index 44a43e9d..da238bc3 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -23,6 +23,8 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -93,6 +95,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { ContextMenu popup; ProgressBar spaceLeft; Label spaceLabel; + Lock recordingsLock = new ReentrantLock(); public RecordingsTab(String title, Recorder recorder, Config config, List sites) { super(title); @@ -165,14 +168,12 @@ public class RecordingsTab extends Tab implements TabSelectionListener { setStyle(null); } else { setText(StringUtil.formatSize(sizeInByte)); + setStyle("-fx-alignment: CENTER-RIGHT;"); if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) { int row = this.getTableRow().getIndex(); JavaFxRecording rec = tableViewProperty().get().getItems().get(row); if(!rec.valueChanged() && rec.getStatus() == STATUS.RECORDING) { setStyle("-fx-alignment: CENTER-RIGHT; -fx-background-color: red"); - } else { - setStyle("-fx-alignment: CENTER-RIGHT;"); - //setStyle(null); } } } @@ -280,23 +281,28 @@ public class RecordingsTab extends Tab implements TabSelectionListener { return; } - for (Iterator iterator = observableRecordings.iterator(); iterator.hasNext();) { - JavaFxRecording old = iterator.next(); - if (!recordings.contains(old)) { - // remove deleted recordings - iterator.remove(); + recordingsLock.lock(); + try { + for (Iterator iterator = observableRecordings.iterator(); iterator.hasNext();) { + JavaFxRecording old = iterator.next(); + if (!recordings.contains(old)) { + // remove deleted recordings + iterator.remove(); + } } - } - for (JavaFxRecording recording : recordings) { - if (!observableRecordings.contains(recording)) { - // add new recordings - observableRecordings.add(recording); - } else { - // update existing ones - int index = observableRecordings.indexOf(recording); - JavaFxRecording old = observableRecordings.get(index); - old.update(recording); + for (JavaFxRecording recording : recordings) { + if (!observableRecordings.contains(recording)) { + // add new recordings + observableRecordings.add(recording); + } else { + // update existing ones + int index = observableRecordings.indexOf(recording); + JavaFxRecording old = observableRecordings.get(index); + old.update(recording); + } } + } finally { + recordingsLock.unlock(); } table.sort(); } @@ -552,20 +558,25 @@ public class RecordingsTab extends Tab implements TabSelectionListener { Thread deleteThread = new Thread() { @Override public void run() { + recordingsLock.lock(); try { - for (JavaFxRecording r : recordings) { + List deleted = new ArrayList<>(); + for (Iterator iterator = recordings.iterator(); iterator.hasNext();) { + JavaFxRecording r = iterator.next(); if(r.getStatus() != STATUS.FINISHED) { continue; } try { recorder.delete(r); - Platform.runLater(() -> observableRecordings.remove(r)); + deleted.add(r); } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { LOG.error("Error while deleting recording", e1); showErrorDialog("Error while deleting recording", "Recording not deleted", e1); } } + observableRecordings.removeAll(deleted); } finally { + recordingsLock.unlock(); Platform.runLater(() -> table.setCursor(Cursor.DEFAULT)); } } From f1eaa75a57147991e6021923b9d11cce2abf7724 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 14:01:50 +0100 Subject: [PATCH 097/231] Add error handler for the video player --- .../src/main/java/ctbrec/ui/PreviewPopupHandler.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java index 42d5d960..78eb1559 100644 --- a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java +++ b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java @@ -185,6 +185,7 @@ public class PreviewPopupHandler implements EventHandler { Collections.sort(sources); StreamSource best = sources.get(0); checkInterrupt(); + LOG.debug("Preview url for {} is {}", model.getName(), best.getMediaPlaylistUrl()); video = new Media(best.getMediaPlaylistUrl()); if(videoPlayer != null) { videoPlayer.dispose(); @@ -206,6 +207,7 @@ public class PreviewPopupHandler implements EventHandler { }); } }); + videoPlayer.setOnError(() -> onError(videoPlayer)); } catch (IllegalStateException e) { if(e.getMessage().equals("Stream url unknown")) { // fine hls url for mfc not known yet @@ -229,6 +231,16 @@ public class PreviewPopupHandler implements EventHandler { }); } + private void onError(MediaPlayer videoPlayer) { + LOG.error("Error while starting preview stream", videoPlayer.getError()); + if(videoPlayer.getError().getCause() != null) { + LOG.error("Error while starting preview stream root cause:", videoPlayer.getError().getCause()); + } + Platform.runLater(() -> { + showTestImage(); + }); + } + private void resize(double w, double h) { preview.setFitWidth(w); preview.setFitHeight(h); From 0edb17ae9f69f513351e9564e3fb06a0425dc606 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 14:02:22 +0100 Subject: [PATCH 098/231] Add trace output for the time the online check took --- common/src/main/java/ctbrec/recorder/LocalRecorder.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index c2017ab5..9ddb7eaa 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -431,7 +431,8 @@ public class LocalRecorder implements Recorder { running = true; while (running) { Instant begin = Instant.now(); - for (Model model : getModelsRecording()) { + List models = getModelsRecording(); + for (Model model : models) { try { boolean isOnline = model.isOnline(IGNORE_CACHE); LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline")); @@ -450,6 +451,7 @@ public class LocalRecorder implements Recorder { } Instant end = Instant.now(); Duration timeCheckTook = Duration.between(begin, end); + LOG.trace("Online check for {} models took {} seconds", models.size(), timeCheckTook.getSeconds()); long sleepTime = Config.getInstance().getSettings().onlineCheckIntervalInSecs; if(timeCheckTook.getSeconds() < sleepTime) { From 7192856c87681b9860180b6b37d4430ac687f66e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 14:17:03 +0100 Subject: [PATCH 099/231] Add setting for chaturbate base URL --- .../ui/sites/chaturbate/ChaturbateConfigUi.java | 17 ++++++++++++++++- .../sites/chaturbate/ChaturbateTabProvider.java | 14 ++++++-------- common/src/main/java/ctbrec/Settings.java | 1 + .../ctbrec/sites/chaturbate/Chaturbate.java | 14 +++++++------- .../sites/chaturbate/ChaturbateHttpClient.java | 6 +++--- .../sites/chaturbate/ChaturbateModel.java | 6 ++---- 6 files changed, 35 insertions(+), 23 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java index e48a7892..706ff33c 100644 --- a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java +++ b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java @@ -42,14 +42,29 @@ public class ChaturbateConfigUi extends AbstractConfigUI { GridPane.setColumnSpan(password, 2); layout.add(password, 1, 1); + layout.add(new Label("Chaturbate Base URL"), 0, 2); + TextField baseUrl = new TextField(); + baseUrl.setText(Config.getInstance().getSettings().chaturbateBaseUrl); + baseUrl.textProperty().addListener((ob, o, n) -> { + Config.getInstance().getSettings().chaturbateBaseUrl = baseUrl.getText(); + save(); + }); + GridPane.setFillWidth(baseUrl, true); + GridPane.setHgrow(baseUrl, Priority.ALWAYS); + GridPane.setColumnSpan(baseUrl, 2); + layout.add(baseUrl, 1, 2); + Button createAccount = new Button("Create new Account"); createAccount.setOnAction((e) -> DesktopIntegration.open(Chaturbate.REGISTRATION_LINK)); - layout.add(createAccount, 1, 2); + layout.add(createAccount, 1, 3); 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(baseUrl, 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/chaturbate/ChaturbateTabProvider.java b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateTabProvider.java index b9864907..f425a23b 100644 --- a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateTabProvider.java @@ -1,7 +1,5 @@ package ctbrec.ui.sites.chaturbate; -import static ctbrec.sites.chaturbate.Chaturbate.*; - import java.util.ArrayList; import java.util.List; @@ -21,17 +19,17 @@ public class ChaturbateTabProvider extends TabProvider { public ChaturbateTabProvider(Chaturbate chaturbate) { this.chaturbate = chaturbate; this.recorder = chaturbate.getRecorder(); - this.followedTab = new ChaturbateFollowedTab("Followed", BASE_URI + "/followed-cams/", chaturbate); + this.followedTab = new ChaturbateFollowedTab("Followed", chaturbate.getBaseUrl() + "/followed-cams/", chaturbate); } @Override public List getTabs(Scene scene) { List tabs = new ArrayList<>(); - tabs.add(createTab("Featured", BASE_URI + "/")); - tabs.add(createTab("Female", BASE_URI + "/female-cams/")); - tabs.add(createTab("Male", BASE_URI + "/male-cams/")); - tabs.add(createTab("Couples", BASE_URI + "/couple-cams/")); - tabs.add(createTab("Trans", BASE_URI + "/trans-cams/")); + tabs.add(createTab("Featured", chaturbate.getBaseUrl() + "/")); + tabs.add(createTab("Female", chaturbate.getBaseUrl() + "/female-cams/")); + tabs.add(createTab("Male", chaturbate.getBaseUrl() + "/male-cams/")); + tabs.add(createTab("Couples", chaturbate.getBaseUrl() + "/couple-cams/")); + tabs.add(createTab("Trans", chaturbate.getBaseUrl() + "/trans-cams/")); followedTab.setScene(scene); followedTab.setRecorder(recorder); tabs.add(followedTab); diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 18172a94..8a155a19 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -43,6 +43,7 @@ public class Settings { public String postProcessing = ""; public String username = ""; // chaturbate username TODO maybe rename this onetime public String password = ""; // chaturbate password TODO maybe rename this onetime + public String chaturbateBaseUrl = "https://chaturbate.com"; public String bongaUsername = ""; public String bongaPassword = ""; public String mfcUsername = ""; diff --git a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java index 251e3187..99128330 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java @@ -45,14 +45,14 @@ import okhttp3.Response; public class Chaturbate extends AbstractSite { private static final transient Logger LOG = LoggerFactory.getLogger(Chaturbate.class); - public static final String BASE_URI = "https://chaturbate.com"; - public static final String AFFILIATE_LINK = BASE_URI + "/in/?track=default&tour=grq0&campaign=55vTi"; - public static final String REGISTRATION_LINK = BASE_URI + "/in/?track=default&tour=g4pe&campaign=55vTi"; + static String baseUrl = "https://chaturbate.com"; + public static final String AFFILIATE_LINK = "https://chaturbate.com/in/?track=default&tour=grq0&campaign=55vTi"; + public static final String REGISTRATION_LINK = "https://chaturbate.com/in/?track=default&tour=g4pe&campaign=55vTi"; private ChaturbateHttpClient httpClient; @Override public void init() throws IOException { - + baseUrl = Config.getInstance().getSettings().chaturbateBaseUrl; } @Override @@ -62,7 +62,7 @@ public class Chaturbate extends AbstractSite { @Override public String getBaseUrl() { - return "https://chaturbate.com"; + return baseUrl; } @Override @@ -139,7 +139,7 @@ public class Chaturbate extends AbstractSite { @Override public List search(String q) throws IOException, InterruptedException { - String url = BASE_URI + "?keywords=" + URLEncoder.encode(q, "utf-8"); + String url = baseUrl + "?keywords=" + URLEncoder.encode(q, "utf-8"); List result = new ArrayList<>(); // search online models @@ -155,7 +155,7 @@ public class Chaturbate extends AbstractSite { // since chaturbate does not return offline models, we at least try, if the profile page // exists for the search string - url = BASE_URI + '/' + q; + url = baseUrl + '/' + q; req = new Request.Builder() .url(url) .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) diff --git a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java index 9950bccd..fbfffa70 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java @@ -54,7 +54,7 @@ public class ChaturbateHttpClient extends HttpClient { try { Request login = new Request.Builder() - .url(Chaturbate.BASE_URI + "/auth/login/") + .url(Chaturbate.baseUrl + "/auth/login/") .build(); Response response = client.newCall(login).execute(); String content = response.body().string(); @@ -68,8 +68,8 @@ public class ChaturbateHttpClient extends HttpClient { .add("csrfmiddlewaretoken", token) .build(); login = new Request.Builder() - .url(Chaturbate.BASE_URI + "/auth/login/") - .header("Referer", Chaturbate.BASE_URI + "/auth/login/") + .url(Chaturbate.baseUrl + "/auth/login/") + .header("Referer", Chaturbate.baseUrl + "/auth/login/") .post(body) .build(); diff --git a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java index bd17cd23..6328840f 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java @@ -1,7 +1,5 @@ package ctbrec.sites.chaturbate; -import static ctbrec.sites.chaturbate.Chaturbate.*; - import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -137,9 +135,9 @@ public class ChaturbateModel extends AbstractModel { String url = null; if(follow) { - url = BASE_URI + "/follow/follow/" + getName() + "/"; + url = getSite().getBaseUrl() + "/follow/follow/" + getName() + "/"; } else { - url = BASE_URI + "/follow/unfollow/" + getName() + "/"; + url = getSite().getBaseUrl() + "/follow/unfollow/" + getName() + "/"; } RequestBody body = RequestBody.create(null, new byte[0]); From 431d2f60c49c8f9dfd3d5707e4a26e0fcffa921a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 15:06:52 +0100 Subject: [PATCH 100/231] Add support for display names Add display name to model and use it in GUI. The actual sites don't set it yet. If the display name is not set, getDisplayName returns getName instead --- client/src/main/java/ctbrec/ui/JavaFxModel.java | 10 ++++++++++ .../main/java/ctbrec/ui/RecordedModelsTab.java | 2 +- client/src/main/java/ctbrec/ui/ThumbCell.java | 2 +- .../src/main/java/ctbrec/ui/ThumbOverviewTab.java | 2 ++ .../ctbrec/ui/controls/SearchPopoverTreeList.java | 2 +- common/src/main/java/ctbrec/AbstractModel.java | 15 +++++++++++++++ common/src/main/java/ctbrec/Model.java | 2 ++ 7 files changed, 32 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java index fccdd9e3..99c4fcb6 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java @@ -197,4 +197,14 @@ public class JavaFxModel implements Model { delegate.setSuspended(suspended); pausedProperty.set(suspended); } + + @Override + public String getDisplayName() { + return delegate.getDisplayName(); + } + + @Override + public void setDisplayName(String name) { + delegate.setDisplayName(name); + } } diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index f14fb35e..e7a397a8 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -125,7 +125,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { preview.setId("preview"); TableColumn name = new TableColumn<>("Model"); name.setPrefWidth(200); - name.setCellValueFactory(new PropertyValueFactory("name")); + name.setCellValueFactory(new PropertyValueFactory("displayName")); name.setEditable(false); TableColumn url = new TableColumn<>("URL"); url.setCellValueFactory(new PropertyValueFactory("url")); diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index cd46672d..ece9efce 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -116,7 +116,7 @@ public class ThumbCell extends StackPane { StackPane.setMargin(resolutionBackground, new Insets(2)); getChildren().add(resolutionBackground); - name = new Text(model.getName()); + name = new Text(model.getDisplayName()); name.setFill(Color.WHITE); name.setFont(new Font("Sansserif", 16)); name.setTextAlignment(TextAlignment.CENTER); diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index 618b0bcd..fce0d327 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -752,6 +752,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { String[] tokens = filter.split(" "); StringBuilder searchTextBuilder = new StringBuilder(m.getName()); searchTextBuilder.append(' '); + searchTextBuilder.append(m.getDisplayName()); + searchTextBuilder.append(' '); for (String tag : m.getTags()) { searchTextBuilder.append(tag).append(' '); } diff --git a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java index 474e91c6..5b58e3a4 100644 --- a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java +++ b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java @@ -231,7 +231,7 @@ public class SearchPopoverTreeList extends PopoverTreeList implements Pop } else { follow.setVisible(model.getSite().supportsFollow()); title.setVisible(true); - title.setText(model.getName()); + title.setText(model.getDisplayName()); this.model = model; URL anonymousPng = getClass().getResource("/anonymous.png"); String previewUrl = Optional.ofNullable(model.getPreview()).orElse(anonymousPng.toString()); diff --git a/common/src/main/java/ctbrec/AbstractModel.java b/common/src/main/java/ctbrec/AbstractModel.java index ce8b7ef1..61238759 100644 --- a/common/src/main/java/ctbrec/AbstractModel.java +++ b/common/src/main/java/ctbrec/AbstractModel.java @@ -14,6 +14,7 @@ public abstract class AbstractModel implements Model { private String url; private String name; + private String displayName; private String preview; private String description; private List tags = new ArrayList<>(); @@ -46,6 +47,20 @@ public abstract class AbstractModel implements Model { this.name = name; } + @Override + public String getDisplayName() { + if(displayName != null) { + return displayName; + } else { + return getName(); + } + } + + @Override + public void setDisplayName(String name) { + this.displayName = name; + } + @Override public String getPreview() { return preview; diff --git a/common/src/main/java/ctbrec/Model.java b/common/src/main/java/ctbrec/Model.java index 3144f777..e13f2fcd 100644 --- a/common/src/main/java/ctbrec/Model.java +++ b/common/src/main/java/ctbrec/Model.java @@ -15,6 +15,8 @@ import ctbrec.sites.Site; public interface Model { public String getUrl(); public void setUrl(String url); + public String getDisplayName(); + public void setDisplayName(String name); public String getName(); public void setName(String name); public String getPreview(); From 56291cb97f98256fad76453f192705334f333417 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 15:29:56 +0100 Subject: [PATCH 101/231] Add support for display names for BongaCams --- .../java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java | 3 +++ common/src/main/java/ctbrec/sites/bonga/BongaCams.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java index 80f979fe..cd52462a 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java @@ -70,6 +70,9 @@ public class BongaCamsUpdateService extends PaginatedScheduledService { model.setOnlineState("offline"); } model.setPreview("https:" + m.getString("thumb_image")); + if(m.has("display_name")) { + model.setDisplayName(m.getString("display_name")); + } models.add(model); } } diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java index d74288e0..fc847912 100644 --- a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java +++ b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java @@ -162,6 +162,9 @@ public class BongaCams extends AbstractSite { if(thumb != null) { model.setPreview("https:" + thumb); } + if(result.has("display_name")) { + model.setDisplayName(result.getString("display_name")); + } models.add(model); } return models; From f0edfb167cbdc69142bab13b757350694251a05f Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 16:34:09 +0100 Subject: [PATCH 102/231] Added support for display names from Camsoda --- .../java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java | 7 ++++++- common/src/main/java/ctbrec/sites/camsoda/Camsoda.java | 3 +++ .../main/java/ctbrec/sites/chaturbate/ChaturbateModel.java | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java index f7b6e321..4b035962 100644 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java @@ -62,6 +62,7 @@ public class CamsodaUpdateService extends PaginatedScheduledService { if(result.has("tpl")) { JSONArray tpl = result.getJSONArray("tpl"); String name = tpl.getString(0); + String displayName = tpl.getString(1); // int connections = tpl.getInt(2); String streamName = tpl.getString(5); String tsize = tpl.getString(6); @@ -77,11 +78,11 @@ public class CamsodaUpdateService extends PaginatedScheduledService { JSONArray edgeServers = result.getJSONArray("edge_servers"); model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); } + model.setDisplayName(displayName); models.add(model); } else { String name = result.getString("username"); CamsodaModel model = (CamsodaModel) camsoda.createModel(name); - if(result.has("server_prefix")) { String serverPrefix = result.getString("server_prefix"); String streamName = result.getString("stream_name"); @@ -91,6 +92,10 @@ public class CamsodaUpdateService extends PaginatedScheduledService { model.setOnlineState(result.getString("status")); } + if(result.has("display_name")) { + model.setDisplayName(result.getString("display_name")); + } + if(result.has("edge_servers")) { JSONArray edgeServers = result.getJSONArray("edge_servers"); model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); diff --git a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java index 346c9315..3008f14b 100644 --- a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java +++ b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java @@ -140,6 +140,9 @@ public class Camsoda extends AbstractSite { if(thumb != null) { model.setPreview("https:" + thumb); } + if(result.has("display_name")) { + model.setDisplayName(result.getString("display_name")); + } models.add(model); } return models; diff --git a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java index 6328840f..5ca806c1 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java @@ -111,6 +111,9 @@ public class ChaturbateModel extends AbstractModel { String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); String segmentUri = baseUrl + playlist.getUri(); src.mediaPlaylistUrl = segmentUri; + if(src.mediaPlaylistUrl.contains("?")) { + src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?')); + } LOG.trace("Media playlist {}", src.mediaPlaylistUrl); sources.add(src); } From 4a8e0e3beafe6d5a51129189e2a9d30ac31dd40a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 16:52:51 +0100 Subject: [PATCH 103/231] Removed buggy optimization for puased checkboxes --- .../java/ctbrec/ui/RecordedModelsTab.java | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index e7a397a8..ec927897 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -32,8 +32,6 @@ import ctbrec.ui.controls.AutoFillTextField; import ctbrec.ui.controls.Toast; import javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.concurrent.ScheduledService; @@ -321,20 +319,11 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { int index = observableModels.indexOf(updatedModel); if (index == -1) { observableModels.add(updatedModel); - updatedModel.getPausedProperty().addListener(new ChangeListener() { - boolean firstChange = true; - @Override - public void changed(ObservableValue obs, Boolean oldV, Boolean newV) { - if(firstChange) { - // don't react to the first change, because that is made by the recorder and not by the user - firstChange = false; - return; - } - if (newV) { - pauseRecording(Collections.singletonList(updatedModel)); - } else { - resumeRecording(Collections.singletonList(updatedModel)); - } + updatedModel.getPausedProperty().addListener((obs, oldV, newV) -> { + if (newV) { + pauseRecording(Collections.singletonList(updatedModel)); + } else { + resumeRecording(Collections.singletonList(updatedModel)); } }); } else { From 6ab70dd5df0348a9a5a6ad77573f6f534425ac55 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 16:56:43 +0100 Subject: [PATCH 104/231] In resumeRecording don't start a recording, if the model is offline --- common/src/main/java/ctbrec/recorder/LocalRecorder.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 9ddb7eaa..4d71e58e 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -761,13 +761,17 @@ public class LocalRecorder implements Recorder { int index = models.indexOf(model); Model m = models.get(index); m.setSuspended(false); - startRecordingProcess(m); + if(m.isOnline()) { + startRecordingProcess(m); + } model.setSuspended(false); config.save(); } else { LOG.warn("Couldn't resume model {}. Not found in list", model.getName()); return; } + } catch (ExecutionException | InterruptedException e) { + LOG.error("Couldn't check, if model {} is online", model.getName()); } finally { lock.unlock(); } From 3188511c6ac8857beca99cf5192c538be9327aea Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 19:29:41 +0100 Subject: [PATCH 105/231] Make log messages less serious for player errors --- .../main/java/ctbrec/ui/PreviewPopupHandler.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java index 78eb1559..adc3fbe6 100644 --- a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java +++ b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java @@ -14,6 +14,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Config; +import ctbrec.io.HttpException; import ctbrec.recorder.download.StreamSource; import javafx.application.Platform; import javafx.event.EventHandler; @@ -199,11 +200,11 @@ public class PreviewPopupHandler implements EventHandler { double aspect = (double)video.getWidth() / video.getHeight(); double w = Config.getInstance().getSettings().thumbWidth; double h = w / aspect; + progressIndicator.setVisible(false); videoPreview.setVisible(true); videoPreview.setMediaPlayer(videoPlayer); resize(w, h); videoPlayer.play(); - progressIndicator.setVisible(false); }); } }); @@ -212,7 +213,12 @@ public class PreviewPopupHandler implements EventHandler { if(e.getMessage().equals("Stream url unknown")) { // fine hls url for mfc not known yet } else { - LOG.error("Couldn't start preview video", e); + LOG.warn("Couldn't start preview video: {}", e.getMessage()); + } + showTestImage(); + } catch (HttpException e) { + if(e.getResponseCode() != 404) { + LOG.warn("Couldn't start preview video: {}", e.getMessage()); } showTestImage(); } catch (InterruptedException | InterruptedIOException e) { @@ -221,11 +227,11 @@ public class PreviewPopupHandler implements EventHandler { if(e.getCause() instanceof InterruptedException || e.getCause() instanceof InterruptedIOException) { // future has been canceled, that's fine } else { - LOG.error("Couldn't start preview video", e); + LOG.warn("Couldn't start preview video: {}", e.getMessage()); showTestImage(); } } catch (Exception e) { - LOG.error("Couldn't start preview video", e); + LOG.warn("Couldn't start preview video: {}", e.getMessage()); showTestImage(); } }); From 857674c5cbabdbc5e99c67b1125c26a0cf9da706 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 20:01:08 +0100 Subject: [PATCH 106/231] Optimized paused checkbox event handling --- client/src/main/java/ctbrec/ui/RecordedModelsTab.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index ec927897..f636b424 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -321,9 +321,13 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { observableModels.add(updatedModel); updatedModel.getPausedProperty().addListener((obs, oldV, newV) -> { if (newV) { - pauseRecording(Collections.singletonList(updatedModel)); + if(!recorder.isSuspended(updatedModel)) { + pauseRecording(Collections.singletonList(updatedModel)); + } } else { - resumeRecording(Collections.singletonList(updatedModel)); + if(recorder.isSuspended(updatedModel)) { + resumeRecording(Collections.singletonList(updatedModel)); + } } }); } else { @@ -537,6 +541,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { Consumer action = (m) -> { try { recorder.suspendRecording(m); + m.setSuspended(true); } catch(Exception e) { Platform.runLater(() -> showErrorDialog(e, "Couldn't pause recording of model", "Pausing recording of " + m.getName() + " failed")); @@ -550,6 +555,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { Consumer action = (m) -> { try { recorder.resumeRecording(m); + m.setSuspended(false); } catch(Exception e) { Platform.runLater(() -> showErrorDialog(e, "Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed")); From 8a3f81a77ac45f83682816ec29c0c6ea6c779760 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 20:21:23 +0100 Subject: [PATCH 107/231] Fix: popup was cut off on south and east edge --- client/src/main/java/ctbrec/ui/PreviewPopupHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java index adc3fbe6..2305a73a 100644 --- a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java +++ b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java @@ -200,10 +200,10 @@ public class PreviewPopupHandler implements EventHandler { double aspect = (double)video.getWidth() / video.getHeight(); double w = Config.getInstance().getSettings().thumbWidth; double h = w / aspect; + resize(w, h); progressIndicator.setVisible(false); videoPreview.setVisible(true); videoPreview.setMediaPlayer(videoPlayer); - resize(w, h); videoPlayer.play(); }); } From 80381c0d49c6b8f033df28806f79608a2a21a0a5 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 1 Dec 2018 21:56:23 +0100 Subject: [PATCH 108/231] Fire events from LocalRecorder --- .../java/ctbrec/ui/CamrecApplication.java | 63 ++++++++++-------- .../resources/Oxygen-Im-Highlight-Msg.mp3 | Bin 0 -> 17664 bytes .../java/ctbrec/recorder/LocalRecorder.java | 27 ++++++++ 3 files changed, 62 insertions(+), 28 deletions(-) create mode 100644 client/src/main/resources/Oxygen-Im-Highlight-Msg.mp3 diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 2b06e7d1..58cfb1ef 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -48,6 +48,7 @@ import javafx.scene.control.Alert; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.image.Image; +import javafx.scene.media.AudioClip; import javafx.scene.paint.Color; import javafx.stage.Stage; import okhttp3.Request; @@ -93,14 +94,7 @@ public class CamrecApplication extends Application { createGui(primaryStage); checkForUpdates(); - new Thread(() -> { - try { - Thread.sleep(TimeUnit.SECONDS.toMillis(10)); - } catch (InterruptedException e) { - e.printStackTrace(); - } - Platform.runLater(() -> registerAlertSystem()); - }).start(); + registerAlertSystem(); } private void logEnvironment() { @@ -211,28 +205,41 @@ public class CamrecApplication extends Application { } private void registerAlertSystem() { - Notification.Notifier.setNotificationOwner(primaryStage); - EventBusHolder.BUS.register(new Object() { - @Subscribe - public void modelEvent(Map e) { - try { - if (Objects.equals("model.status", e.get("event"))) { - String status = (String) e.get("status"); - Model model = (Model) e.get("model"); - LOG.debug("Alert: {} is {}", model.getName(), status); - if (Objects.equals("online", status)) { - Platform.runLater(() -> { - notifier.notifyInfo("Model Online", model.getName() + " is now online"); - //AudioClip clip = new AudioClip("file:///tmp/Oxygen-Im-Highlight-Msg.mp3"); - //clip.play(); - }); + new Thread(() -> { + try { + // don't register before 1 minute has passed, because directly after + // the start of ctbrec, an event for every online model would be fired, + // which is annoying as f + Thread.sleep(TimeUnit.MINUTES.toMillis(1)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + LOG.debug("Alert System registered"); + Platform.runLater(() -> { + Notification.Notifier.setNotificationOwner(primaryStage); + EventBusHolder.BUS.register(new Object() { + @Subscribe + public void modelEvent(Map e) { + LOG.debug("Alert: {}", e); + try { + if (Objects.equals("model.status", e.get("event"))) { + String status = (String) e.get("status"); + Model model = (Model) e.get("model"); + if (Objects.equals("online", status)) { + Platform.runLater(() -> { + notifier.notifyInfo("Model Online", model.getName() + " is now online"); + AudioClip clip = new AudioClip(getClass().getResource("/Oxygen-Im-Highlight-Msg.mp3").toString()); + clip.play(); + }); + } + } + } catch (Exception e1) { + e1.printStackTrace(); } } - } catch (Exception e1) { - e1.printStackTrace(); - } - } - }); + }); + }); + }).start(); } private void writeColorSchemeStyleSheet(Stage primaryStage) { diff --git a/client/src/main/resources/Oxygen-Im-Highlight-Msg.mp3 b/client/src/main/resources/Oxygen-Im-Highlight-Msg.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ad3a8a54e4198e2d4bef1bd1adacdd0834626450 GIT binary patch literal 17664 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 literal 0 HcmV?d00001 diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 4d71e58e..c7f5b897 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -36,6 +36,7 @@ import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; import ctbrec.Config; +import ctbrec.EventBusHolder; import ctbrec.Model; import ctbrec.OS; import ctbrec.Recording; @@ -179,6 +180,7 @@ public class LocalRecorder implements Recorder { } } }.start(); + fireRecordingStateChanged(model, true); } private void stopRecordingProcess(Model model) { @@ -188,6 +190,7 @@ public class LocalRecorder implements Recorder { if(!Config.isServerMode()) { postprocess(download); } + fireRecordingStateChanged(model, false); } private void postprocess(Download download) { @@ -368,6 +371,7 @@ public class LocalRecorder implements Recorder { } else { postprocess(d); } + fireRecordingStateChanged(m, false); } } for (Model m : restart) { @@ -434,7 +438,11 @@ public class LocalRecorder implements Recorder { List models = getModelsRecording(); for (Model model : models) { try { + boolean wasOnline = model.isOnline(); boolean isOnline = model.isOnline(IGNORE_CACHE); + if(wasOnline != isOnline) { + fireModelOnlineStateChanged(model, isOnline); + } LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline")); if (isOnline && !isSuspended(model) && !recordingProcesses.containsKey(model)) { LOG.info("Model {}'s room back to public", model); @@ -468,6 +476,25 @@ public class LocalRecorder implements Recorder { } LOG.debug(getName() + " terminated"); } + + } + + private void fireModelOnlineStateChanged(Model model, boolean online) { + Map evt = new HashMap<>(); + evt.put("event", "model.status"); + evt.put("status", online ? "online" : "offline"); + evt.put("model", model); + EventBusHolder.BUS.post(evt); + LOG.debug("Event fired {}", evt); + } + + private void fireRecordingStateChanged(Model model, boolean recording) { + Map evt = new HashMap<>(); + evt.put("event", "recording.status"); + evt.put("status", recording ? "started" : "stopped"); + evt.put("model", model); + EventBusHolder.BUS.post(evt); + LOG.debug("Event fired {}", evt); } private class PostProcessingTrigger extends Thread { From 0ff04ed9ef960fa3d421f916a002ccc0109402d1 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 2 Dec 2018 02:07:30 +0100 Subject: [PATCH 109/231] Use /bin/bash in shebang ... otherwise pushd and popd might not work --- client/src/assembly/ctbrec-linux-jre.sh | 2 +- client/src/assembly/ctbrec-linux.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/assembly/ctbrec-linux-jre.sh b/client/src/assembly/ctbrec-linux-jre.sh index 6c68d4d8..e6880968 100755 --- a/client/src/assembly/ctbrec-linux-jre.sh +++ b/client/src/assembly/ctbrec-linux-jre.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash pushd $(dirname $0) JAVA=./jre/bin/java diff --git a/client/src/assembly/ctbrec-linux.sh b/client/src/assembly/ctbrec-linux.sh index df9c22eb..6ddab21e 100755 --- a/client/src/assembly/ctbrec-linux.sh +++ b/client/src/assembly/ctbrec-linux.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash pushd $(dirname $0) JAVA=java From 1feea03ec32fe7290989769ae04ba7930b5762a9 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 2 Dec 2018 13:32:38 +0100 Subject: [PATCH 110/231] Dispose the video player on error --- client/src/main/java/ctbrec/ui/PreviewPopupHandler.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java index 2305a73a..e6ffc72a 100644 --- a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java +++ b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java @@ -242,6 +242,7 @@ public class PreviewPopupHandler implements EventHandler { if(videoPlayer.getError().getCause() != null) { LOG.error("Error while starting preview stream root cause:", videoPlayer.getError().getCause()); } + videoPlayer.dispose(); Platform.runLater(() -> { showTestImage(); }); @@ -277,6 +278,9 @@ public class PreviewPopupHandler implements EventHandler { } private void hidePopup() { + if(future != null && !future.isDone()) { + future.cancel(true); + } Platform.runLater(() -> { popup.setX(-1000); popup.setY(-1000); From 723909aec327dfef5ab4ce6f145b65bb9d5310ca Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 2 Dec 2018 17:01:00 +0100 Subject: [PATCH 111/231] Remove notification class and resources Notifications are done with SystemTray or a call to notify-send --- .../enzo/notification/Notification.java | 373 ------------------ .../eu/hansolo/enzo/notification/README.md | 4 - .../eu/hansolo/enzo/notification/error.png | Bin 704 -> 0 bytes .../eu/hansolo/enzo/notification/info.png | Bin 810 -> 0 bytes .../eu/hansolo/enzo/notification/license.txt | 202 ---------- .../eu/hansolo/enzo/notification/notifier.css | 40 -- .../eu/hansolo/enzo/notification/success.png | Bin 839 -> 0 bytes .../eu/hansolo/enzo/notification/warning.png | Bin 623 -> 0 bytes 8 files changed, 619 deletions(-) delete mode 100644 client/src/main/java/eu/hansolo/enzo/notification/Notification.java delete mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/README.md delete mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/error.png delete mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/info.png delete mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/license.txt delete mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/notifier.css delete mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/success.png delete mode 100644 client/src/main/resources/eu/hansolo/enzo/notification/warning.png diff --git a/client/src/main/java/eu/hansolo/enzo/notification/Notification.java b/client/src/main/java/eu/hansolo/enzo/notification/Notification.java deleted file mode 100644 index 567beaaa..00000000 --- a/client/src/main/java/eu/hansolo/enzo/notification/Notification.java +++ /dev/null @@ -1,373 +0,0 @@ -/* - * Copyright (c) 2013 by Gerrit Grunwald - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.hansolo.enzo.notification; - -import javafx.animation.KeyFrame; -import javafx.animation.KeyValue; -import javafx.animation.Timeline; -import javafx.application.Platform; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Scene; -import javafx.scene.control.Label; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; -import javafx.stage.Popup; -import javafx.stage.Screen; -import javafx.stage.Stage; -import javafx.stage.StageStyle; -import javafx.util.Duration; - - -/** - * Created by - * User: hansolo - * Date: 01.07.13 - * Time: 07:10 - */ -public class Notification { - public static final Image INFO_ICON = new Image(Notifier.class.getResourceAsStream("info.png")); - public static final Image WARNING_ICON = new Image(Notifier.class.getResourceAsStream("warning.png")); - public static final Image SUCCESS_ICON = new Image(Notifier.class.getResourceAsStream("success.png")); - public static final Image ERROR_ICON = new Image(Notifier.class.getResourceAsStream("error.png")); - public final String TITLE; - public final String MESSAGE; - public final Image IMAGE; - - - // ******************** Constructors ************************************** - public Notification(final String TITLE, final String MESSAGE) { - this(TITLE, MESSAGE, null); - } - public Notification(final String MESSAGE, final Image IMAGE) { - this("", MESSAGE, IMAGE); - } - public Notification(final String TITLE, final String MESSAGE, final Image IMAGE) { - this.TITLE = TITLE; - this.MESSAGE = MESSAGE; - this.IMAGE = IMAGE; - } - - - // ******************** Inner Classes ************************************* - public enum Notifier { - INSTANCE; - - private static final double ICON_WIDTH = 24; - private static final double ICON_HEIGHT = 24; - private static double width = 300; - private static double height = 80; - private static double offsetX = 0; - private static double offsetY = 25; - private static double spacingY = 5; - private static Pos popupLocation = Pos.BOTTOM_RIGHT; - private static Stage stageRef = null; - private Duration popupLifetime; - private Stage stage; - private Scene scene; - private ObservableList popups; - - - // ******************** Constructor *************************************** - private Notifier() { - init(); - initGraphics(); - } - - - // ******************** Initialization ************************************ - private void init() { - popupLifetime = Duration.millis(5000); - popups = FXCollections.observableArrayList(); - } - - private void initGraphics() { - Region region = new Region(); - region.resize(0, 0); - region.setVisible(false); - scene = new Scene(region); - scene.setFill(null); - scene.getStylesheets().add(getClass().getResource("notifier.css").toExternalForm()); - - stage = new Stage(); - stage.initStyle(StageStyle.TRANSPARENT); - stage.setScene(scene); - } - - - // ******************** Methods ******************************************* - /** - * @param STAGE_REF The Notification will be positioned relative to the given Stage.
    - * If null then the Notification will be positioned relative to the primary Screen. - * @param POPUP_LOCATION The default is TOP_RIGHT of primary Screen. - */ - public static void setPopupLocation(final Stage STAGE_REF, final Pos POPUP_LOCATION) { - if (null != STAGE_REF) { - INSTANCE.stage.initOwner(STAGE_REF); - Notifier.stageRef = STAGE_REF; - } - Notifier.popupLocation = POPUP_LOCATION; - } - - /** - * Sets the Notification's owner stage so that when the owner - * stage is closed Notifications will be shut down as well.
    - * This is only needed if setPopupLocation is called - * without a stage reference. - * @param OWNER - */ - public static void setNotificationOwner(final Stage OWNER) { - INSTANCE.stage.initOwner(OWNER); - INSTANCE.stage.getScene().getStylesheets().addAll(OWNER.getScene().getStylesheets()); - } - - /** - * @param OFFSET_X The horizontal shift required. - *
    The default is 0 px. - */ - public static void setOffsetX(final double OFFSET_X) { - Notifier.offsetX = OFFSET_X; - } - - /** - * @param OFFSET_Y The vertical shift required. - *
    The default is 25 px. - */ - public static void setOffsetY(final double OFFSET_Y) { - Notifier.offsetY = OFFSET_Y; - } - - /** - * @param WIDTH The default is 300 px. - */ - public static void setWidth(final double WIDTH) { - Notifier.width = WIDTH; - } - - /** - * @param HEIGHT The default is 80 px. - */ - public static void setHeight(final double HEIGHT) { - Notifier.height = HEIGHT; - } - - /** - * @param SPACING_Y The spacing between multiple Notifications. - *
    The default is 5 px. - */ - public static void setSpacingY(final double SPACING_Y) { - Notifier.spacingY = SPACING_Y; - } - - public void stop() { - popups.clear(); - stage.close(); - } - - /** - * Returns the Duration that the notification will stay on screen before it - * will fade out. - * @return the Duration the popup notification will stay on screen - */ - public Duration getPopupLifetime() { - return popupLifetime; - } - - /** - * Defines the Duration that the popup notification will stay on screen before it - * will fade out. The parameter is limited to values between 2 and 20 seconds. - * @param POPUP_LIFETIME - */ - public void setPopupLifetime(final Duration POPUP_LIFETIME) { - popupLifetime = Duration.millis(clamp(2000, 20000, POPUP_LIFETIME.toMillis())); - } - - /** - * Show the given Notification on the screen - * @param NOTIFICATION - */ - public void notify(final Notification NOTIFICATION) { - preOrder(); - showPopup(NOTIFICATION); - } - - /** - * Show a Notification with the given parameters on the screen - * @param TITLE - * @param MESSAGE - * @param IMAGE - */ - public void notify(final String TITLE, final String MESSAGE, final Image IMAGE) { - notify(new Notification(TITLE, MESSAGE, IMAGE)); - } - - /** - * Show a Notification with the given title and message and an Info icon - * @param TITLE - * @param MESSAGE - */ - public void notifyInfo(final String TITLE, final String MESSAGE) { - notify(new Notification(TITLE, MESSAGE, Notification.INFO_ICON)); - } - - /** - * Show a Notification with the given title and message and a Warning icon - * @param TITLE - * @param MESSAGE - */ - public void notifyWarning(final String TITLE, final String MESSAGE) { - notify(new Notification(TITLE, MESSAGE, Notification.WARNING_ICON)); - } - - /** - * Show a Notification with the given title and message and a Checkmark icon - * @param TITLE - * @param MESSAGE - */ - public void notifySuccess(final String TITLE, final String MESSAGE) { - notify(new Notification(TITLE, MESSAGE, Notification.SUCCESS_ICON)); - } - - /** - * Show a Notification with the given title and message and an Error icon - * @param TITLE - * @param MESSAGE - */ - public void notifyError(final String TITLE, final String MESSAGE) { - notify(new Notification(TITLE, MESSAGE, Notification.ERROR_ICON)); - } - - /** - * Makes sure that the given VALUE is within the range of MIN to MAX - * @param MIN - * @param MAX - * @param VALUE - * @return - */ - private double clamp(final double MIN, final double MAX, final double VALUE) { - if (VALUE < MIN) return MIN; - if (VALUE > MAX) return MAX; - return VALUE; - } - - /** - * Reorder the popup Notifications on screen so that the latest Notification will stay on top - */ - private void preOrder() { - if (popups.isEmpty()) return; - for (int i = 0 ; i < popups.size() ; i++) { - switch (popupLocation) { - case TOP_LEFT: case TOP_CENTER: case TOP_RIGHT: popups.get(i).setY(popups.get(i).getY() + height + spacingY); break; - default: popups.get( i ).setY( popups.get( i ).getY() - height - spacingY); - } - } - } - - /** - * Creates and shows a popup with the data from the given Notification object - * @param NOTIFICATION - */ - private void showPopup(final Notification NOTIFICATION) { - Label title = new Label(NOTIFICATION.TITLE); - title.getStyleClass().add("title"); - - ImageView icon = new ImageView(NOTIFICATION.IMAGE); - icon.setFitWidth(ICON_WIDTH); - icon.setFitHeight(ICON_HEIGHT); - - Label message = new Label(NOTIFICATION.MESSAGE, icon); - message.getStyleClass().add("message"); - - VBox popupLayout = new VBox(); - popupLayout.setSpacing(10); - popupLayout.setPadding(new Insets(10, 10, 10, 10)); - popupLayout.getChildren().addAll(title, message); - - StackPane popupContent = new StackPane(); - popupContent.setPrefSize(width, height); - popupContent.getStyleClass().add("notification"); - popupContent.getChildren().addAll(popupLayout); - - final Popup POPUP = new Popup(); - POPUP.setX( getX() ); - POPUP.setY( getY() ); - POPUP.getContent().add(popupContent); - - popups.add(POPUP); - - // Add a timeline for popup fade out - KeyValue fadeOutBegin = new KeyValue(POPUP.opacityProperty(), 1.0); - KeyValue fadeOutEnd = new KeyValue(POPUP.opacityProperty(), 0.0); - - KeyFrame kfBegin = new KeyFrame(Duration.ZERO, fadeOutBegin); - KeyFrame kfEnd = new KeyFrame(Duration.millis(500), fadeOutEnd); - - Timeline timeline = new Timeline(kfBegin, kfEnd); - timeline.setDelay(popupLifetime); - timeline.setOnFinished(actionEvent -> Platform.runLater(() -> { - POPUP.hide(); - popups.remove(POPUP); - stage.hide(); - })); - - // Move popup to the right during fade out - //POPUP.opacityProperty().addListener((observableValue, oldOpacity, opacity) -> popup.setX(popup.getX() + (1.0 - opacity.doubleValue()) * popup.getWidth()) ); - - if (stage.isShowing()) { - stage.toFront(); - } else { - stage.show(); - } - - POPUP.show(stage); - timeline.play(); - } - - private double getX() { - if (null == stageRef) return calcX( 0.0, Screen.getPrimary().getBounds().getWidth() ); - - return calcX(stageRef.getX(), stageRef.getWidth()); - } - private double getY() { - if (null == stageRef) return calcY( 0.0, Screen.getPrimary().getBounds().getHeight() ); - - return calcY(stageRef.getY(), stageRef.getHeight()); - } - - private double calcX(final double LEFT, final double TOTAL_WIDTH) { - switch (popupLocation) { - case TOP_LEFT : case CENTER_LEFT : case BOTTOM_LEFT : return LEFT + offsetX; - case TOP_CENTER: case CENTER : case BOTTOM_CENTER: return LEFT + (TOTAL_WIDTH - width) * 0.5 - offsetX; - case TOP_RIGHT : case CENTER_RIGHT: case BOTTOM_RIGHT : return LEFT + TOTAL_WIDTH - width - offsetX; - default: return 0.0; - } - } - private double calcY(final double TOP, final double TOTAL_HEIGHT ) { - switch (popupLocation) { - case TOP_LEFT : case TOP_CENTER : case TOP_RIGHT : return TOP + offsetY; - case CENTER_LEFT: case CENTER : case CENTER_RIGHT: return TOP + (TOTAL_HEIGHT- height)/2 - offsetY; - case BOTTOM_LEFT: case BOTTOM_CENTER: case BOTTOM_RIGHT: return TOP + TOTAL_HEIGHT - height - offsetY; - default: return 0.0; - } - } - } -} diff --git a/client/src/main/resources/eu/hansolo/enzo/notification/README.md b/client/src/main/resources/eu/hansolo/enzo/notification/README.md deleted file mode 100644 index 849c6e92..00000000 --- a/client/src/main/resources/eu/hansolo/enzo/notification/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Enzo -==== - -A repo that contains custom controls for JavaFX 8 (current version is hosted on bitbucket) diff --git a/client/src/main/resources/eu/hansolo/enzo/notification/error.png b/client/src/main/resources/eu/hansolo/enzo/notification/error.png deleted file mode 100644 index f0651ec351db0a270c295195802c436869650cb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 704 zcmV;x0zdtUP)P000;W1^@s654Bdt00004b3#c}2nYxW zdP}K-Mc%gai#jxU)FM;%3j;$J^wJoDM9Ho%zrIKX2dc>;?`o6&k<| z;5u*#xB%3E9`G4>2fP5Dhn#MbLzPfLUIXp|_kpvA6=L=scmO;OIlUuAR5%B$16Q2; z7w`($27UskfEnN_aMIyk1B)T&n-_@%nkuwaP`h#!?x;|&*m2$Bxyxy*&`gS`FrmVZ z1Fu$*SBR~;+#MArQX#~i;n1`3cD8DEeK8EOYC=fD;Ddh zFqQ#xK*Qosq9@RGRhQjV}NzlCjCd{ ztvI8ZM9w5IZnr-&;Dd`_P6%vKK7(77&*){`-Vx8o40sEJF?~A%H()SYiZggdxf$xf zi70Q;)-FX8sIcbxdMet@&@ZF;r&Q<~-`db)rE1KiaH+B4D;Vv7G(bG+SB`zu8+ari mt$*_Q{Xahvk=U{P68!-uhjovBUk&#F0000P000;W1^@s654Bdt00004b3#c}2nYxW zd4ig;WsWT#P=-jJl|dS-K&M`Y0k`G` z%sDgDqfWBC58JT~>oI^{yucm&h9B^qkWO}kXti2;D!_Yj0AFEQZ6byIi(@z=q%Yn~ zlI8uljEyDh6a0kRc!)(J+k~C9F91run&?LIEcLw=89%B)a3momSZVCowCwwj^Ik1?TxsRghNq3$3aKd zwQ5N6s2?lJw_8%D74{N8N{^Sy)?Iyk0?YSkV{l#$1ZkxLZL|##x(`GI}PYkGc@-K)<42 z7Dwy<_`Px;1FQim)wM5yy|bUb@qzjB_i&uZmKG&nkk3OXEvOWcltIlATdc;(8*a z6BvueU9x=T6!+py*;=xU;cHBEg)icFwS8M7gA?t8KN)o&PCGhYzMVh9+|v9a#)Ncq z8bJ|v8gt6H1xZM6pb;0gN|LM;dQZ_0llUH+g|wlYB*}4|T{j|b?Nxsc2j0b1|DGSM oPT&kbza63({zyzEdnaF_mll`s5^Y~IWdHyG07*qoM6N<$f{XfRrvLx| diff --git a/client/src/main/resources/eu/hansolo/enzo/notification/license.txt b/client/src/main/resources/eu/hansolo/enzo/notification/license.txt deleted file mode 100644 index 7a4a3ea2..00000000 --- a/client/src/main/resources/eu/hansolo/enzo/notification/license.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/client/src/main/resources/eu/hansolo/enzo/notification/notifier.css b/client/src/main/resources/eu/hansolo/enzo/notification/notifier.css deleted file mode 100644 index ded966ed..00000000 --- a/client/src/main/resources/eu/hansolo/enzo/notification/notifier.css +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2013 by Gerrit Grunwald - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -.root { - -fx-background-color: transparent; - -fx-fill : transparent; -} -.notification { - -fx-background-color : -fx-base; - -fx-background-radius: 5; - -fx-effect : dropshadow(gaussian, rgba(0, 0, 0, 0.8), 20, 0, 0, 0); - -foreground-color : -fx-text-background-color; - -icon-color : -fx-text-background-color; -} - -.notification .title { - -fx-font-size : 1.083333em; - -fx-font-weight: bold; - -fx-text-fill : -foreground-color; -} -.notification .message { - -fx-font-size : 1.0em; - -fx-content-display : left; - -fx-graphic-text-gap: 10; - -fx-text-fill : -foreground-color; -} - diff --git a/client/src/main/resources/eu/hansolo/enzo/notification/success.png b/client/src/main/resources/eu/hansolo/enzo/notification/success.png deleted file mode 100644 index 472804af55afc0b647b833e12992f5efd7cd3f0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 839 zcmV-N1GxN&P)P000;W1^@s654Bdt00004b3#c}2nYxW zdJNR7i=v*3YY!MHC0{&pS8rk`$Jk2%_8sl0FawDG`W~iFdMvBwRPCoAMv1u$Gz7 z&W#!bx3*?rIz|vgn=qk}CJPC}lFFdeexO_l-?zncUU|6P_q}(Kp3RwaX3qC}<~(y| zo}Mw1OEZaQ@eCfr3QXWS&fz4E;e(Rv?0gu5!9Z^W_yk_WYq)n@A|b!xbsQ|YUSE(T zm*zek!s8wHU-%f`;4<#U!*~jJb=G}`ttHoAMv~;xJcv*6K)BAuZr(4sUYjSOCD;*r+L$J5xx_cq; zp+q9vb7?N%mB87Dosc4vN3bJej`#chR~Jjrm43g!8^^=k5x6Z0UJ88;yFT1}daxMK zT$|I6Pb@^R>FBB+-p00YeTyecu78feS)H_v1y@S0f6U2v zb1u!{D2+`=SM^d#`?aNj@9}iW^|x+Z;9Lpwo@9jN(mbK9`4+sBOEZyD+N-^8;AgBa zx&AbM@9pWj9OI?AG|M`3XS7WP)A$-MhkZfY)#pbfxC~3fyp$q%C~JpP000;W1^@s654Bdt00004b3#c}2nYxW zd-V>?93NPk=`)k%dJvbS%{5D9M>W@VH302Swo&~&OGP*&tYcnZFM>wyR9~~TCFT| zis1+*@wTk$B|W%}MS%Ns8eFK4BOOc#kn$!f#Y%Re#CFbilL3!eqA=2Nihr!48au6W=6VzuM7PVTUazwbNipofM~rTh{1xwT-M^oiZY|?X zS=B$;cqWhf0AReV>M4x(@r~l)+M<9I!*M+BlQ^iRQw+z}A$WM>vZ~u_g(s|DyW!scf%OZEG?VPMvj6cr(O=Ovc^M;Tyk`Ia002ov JPDHLkV1h)#3WNXv From 96b5c2627761618b83658c02957fda6bacd7bcdf Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 2 Dec 2018 17:02:17 +0100 Subject: [PATCH 112/231] Implement notification messages with SystemTray and notify-send --- .../java/ctbrec/ui/CamrecApplication.java | 15 ++-- common/src/main/java/ctbrec/OS.java | 75 +++++++++++++++++++ 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 58cfb1ef..c59708fe 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -12,7 +12,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +24,7 @@ import com.squareup.moshi.Types; import ctbrec.Config; import ctbrec.EventBusHolder; import ctbrec.Model; +import ctbrec.OS; import ctbrec.StringUtil; import ctbrec.Version; import ctbrec.io.HttpClient; @@ -37,7 +37,6 @@ import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.mfc.MyFreeCams; -import eu.hansolo.enzo.notification.Notification; import javafx.application.Application; import javafx.application.HostServices; import javafx.application.Platform; @@ -48,7 +47,6 @@ import javafx.scene.control.Alert; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.image.Image; -import javafx.scene.media.AudioClip; import javafx.scene.paint.Color; import javafx.stage.Stage; import okhttp3.Request; @@ -66,7 +64,6 @@ public class CamrecApplication extends Application { private TabPane rootPane = new TabPane(); private List sites = new ArrayList<>(); public static HttpClient httpClient; - private Notification.Notifier notifier = Notification.Notifier.INSTANCE; @Override public void start(Stage primaryStage) throws Exception { @@ -210,13 +207,13 @@ public class CamrecApplication extends Application { // don't register before 1 minute has passed, because directly after // the start of ctbrec, an event for every online model would be fired, // which is annoying as f - Thread.sleep(TimeUnit.MINUTES.toMillis(1)); + //Thread.sleep(TimeUnit.MINUTES.toMillis(1)); + Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } LOG.debug("Alert System registered"); Platform.runLater(() -> { - Notification.Notifier.setNotificationOwner(primaryStage); EventBusHolder.BUS.register(new Object() { @Subscribe public void modelEvent(Map e) { @@ -227,9 +224,9 @@ public class CamrecApplication extends Application { Model model = (Model) e.get("model"); if (Objects.equals("online", status)) { Platform.runLater(() -> { - notifier.notifyInfo("Model Online", model.getName() + " is now online"); - AudioClip clip = new AudioClip(getClass().getResource("/Oxygen-Im-Highlight-Msg.mp3").toString()); - clip.play(); + String header = "Model Online"; + String msg = model.getDisplayName() + " is now online"; + OS.notification(primaryStage.getTitle(), header, msg); }); } } diff --git a/common/src/main/java/ctbrec/OS.java b/common/src/main/java/ctbrec/OS.java index cc9fbb45..c388e3c1 100644 --- a/common/src/main/java/ctbrec/OS.java +++ b/common/src/main/java/ctbrec/OS.java @@ -1,12 +1,27 @@ package ctbrec; +import java.awt.AWTException; +import java.awt.Image; +import java.awt.SystemTray; +import java.awt.Toolkit; +import java.awt.TrayIcon; +import java.awt.TrayIcon.MessageType; import java.io.File; +import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map.Entry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.io.StreamRedirectThread; +import javafx.scene.media.AudioClip; + public class OS { + private static final transient Logger LOG = LoggerFactory.getLogger(OS.class); + public static enum TYPE { LINUX, MAC, @@ -72,4 +87,64 @@ public class OS { } return env; } + + public static void notification(String title, String header, String msg) { + if(OS.getOsType() == OS.TYPE.LINUX) { + notifyLinux(title, header, msg); + } else if(OS.getOsType() == OS.TYPE.WINDOWS) { + notifyWindows(title, header, msg); + } else if(OS.getOsType() == OS.TYPE.MAC) { + // TODO find out, if it makes a sound or if we have to play a sound + notifyMac(title, header, msg); + } else { + // unknown system, try systemtray notification anyways + notifySystemTray(title, header, msg); + } + } + + private static void notifyLinux(String title, String header, String msg) { + try { + Process p = Runtime.getRuntime().exec(new String[] { + "notify-send", + "-u", "normal", + "-t", "5000", + "-a", title, + header, + msg.replaceAll("-", "\\\\-").replaceAll("\\s", "\\\\ "), + "--icon=dialog-information" + }); + new Thread(new StreamRedirectThread(p.getInputStream(), System.out)).start(); + new Thread(new StreamRedirectThread(p.getErrorStream(), System.err)).start(); + AudioClip clip = new AudioClip(OS.class.getResource("/Oxygen-Im-Highlight-Msg.mp3").toString()); + clip.play(); + } catch (IOException e1) { + LOG.error("Notification failed", e1); + } + } + + private static void notifyWindows(String title, String header, String msg) { + notifySystemTray(title, header, msg); + } + + private static void notifyMac(String title, String header, String msg) { + notifySystemTray(title, header, msg); + } + + private static void notifySystemTray(String title, String header, String msg) { + if(SystemTray.isSupported()) { + SystemTray tray = SystemTray.getSystemTray(); + Image image = Toolkit.getDefaultToolkit().createImage(OS.class.getResource("/icon64.png")); + TrayIcon trayIcon = new TrayIcon(image, title); + trayIcon.setImageAutoSize(true); + trayIcon.setToolTip(title); + try { + tray.add(trayIcon); + } catch (AWTException e) { + LOG.error("Coulnd't add tray icon", e); + } + trayIcon.displayMessage(header, msg, MessageType.INFO); + } else { + LOG.error("SystemTray notifications not supported by this OS"); + } + } } From 353f3fb317fa5bcd59da981079b9c5afb3374071 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 2 Dec 2018 22:33:49 +0100 Subject: [PATCH 113/231] Use baseUrl in loadStreamInfo --- common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java index 99128330..bd96ad96 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java @@ -240,7 +240,7 @@ public class Chaturbate extends AbstractSite { .add("bandwidth", "high") .build(); Request req = new Request.Builder() - .url("https://chaturbate.com/get_edge_hls_url_ajax/") + .url(getBaseUrl() + "/get_edge_hls_url_ajax/") .post(body) .addHeader("X-Requested-With", "XMLHttpRequest") .build(); From 6b4d320bc2e86a06600bb5ddce6664384e089480 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 2 Dec 2018 22:35:12 +0100 Subject: [PATCH 114/231] Add setting to set the base URL for MFC --- .../ui/sites/myfreecams/MyFreeCamsConfigUI.java | 17 ++++++++++++++++- common/src/main/java/ctbrec/Settings.java | 1 + .../main/java/ctbrec/sites/mfc/MyFreeCams.java | 11 ++++++----- .../java/ctbrec/sites/mfc/MyFreeCamsClient.java | 2 +- .../ctbrec/sites/mfc/MyFreeCamsHttpClient.java | 8 ++++---- .../java/ctbrec/sites/mfc/MyFreeCamsModel.java | 2 +- .../java/ctbrec/sites/mfc/ServerConfig.java | 13 +++++++++---- 7 files changed, 38 insertions(+), 16 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java index 79d3bdc9..bf64358b 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java @@ -26,6 +26,7 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { GridPane layout = SettingsTab.createGridLayout(); layout.add(new Label("MyFreeCams User"), 0, 0); TextField username = new TextField(Config.getInstance().getSettings().mfcUsername); + username.setPrefWidth(300); username.textProperty().addListener((ob, o, n) -> { Config.getInstance().getSettings().mfcUsername = username.getText(); save(); @@ -47,13 +48,27 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { GridPane.setColumnSpan(password, 2); layout.add(password, 1, 1); + layout.add(new Label("MyFreeCams Base URL"), 0, 2); + TextField baseUrl = new TextField(); + baseUrl.setText(Config.getInstance().getSettings().mfcBaseUrl); + baseUrl.textProperty().addListener((ob, o, n) -> { + Config.getInstance().getSettings().mfcBaseUrl = baseUrl.getText(); + save(); + }); + GridPane.setFillWidth(baseUrl, true); + GridPane.setHgrow(baseUrl, Priority.ALWAYS); + GridPane.setColumnSpan(baseUrl, 2); + layout.add(baseUrl, 1, 2); + Button createAccount = new Button("Create new Account"); createAccount.setOnAction((e) -> DesktopIntegration.open(myFreeCams.getAffiliateLink())); - layout.add(createAccount, 1, 2); + layout.add(createAccount, 1, 3); 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(baseUrl, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + return layout; } } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 8a155a19..64fc8e42 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -48,6 +48,7 @@ public class Settings { public String bongaPassword = ""; public String mfcUsername = ""; public String mfcPassword = ""; + public String mfcBaseUrl = "https://www.myfreecams.com"; public String camsodaUsername = ""; public String camsodaPassword = ""; public String cam4Username; diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java index a72191e2..315d040c 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java @@ -17,13 +17,14 @@ import okhttp3.Response; public class MyFreeCams extends AbstractSite { - public static final String BASE_URI = "https://www.myfreecams.com"; + static String baseUrl = "https://www.myfreecams.com"; private MyFreeCamsClient client; private MyFreeCamsHttpClient httpClient; @Override public void init() throws IOException { + baseUrl = Config.getInstance().getSettings().mfcBaseUrl; client = MyFreeCamsClient.getInstance(); client.setSite(this); client.start(); @@ -41,12 +42,12 @@ public class MyFreeCams extends AbstractSite { @Override public String getBaseUrl() { - return BASE_URI; + return baseUrl; } @Override public String getAffiliateLink() { - return BASE_URI + "/?baf=8127165"; + return baseUrl + "/?baf=8127165"; } @Override @@ -59,7 +60,7 @@ public class MyFreeCams extends AbstractSite { @Override public Integer getTokenBalance() throws IOException { - Request req = new Request.Builder().url(BASE_URI + "/php/account.php?request=status").build(); + Request req = new Request.Builder().url(baseUrl + "/php/account.php?request=status").build(); try(Response response = getHttpClient().execute(req)) { if(response.isSuccessful()) { String content = response.body().string(); @@ -74,7 +75,7 @@ public class MyFreeCams extends AbstractSite { @Override public String getBuyTokensLink() { - return BASE_URI + "/php/purchase.php?request=tokens"; + return baseUrl + "/php/purchase.php?request=tokens"; } @Override diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index 013dea20..6f167b77 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -85,7 +85,7 @@ public class MyFreeCamsClient { public void start() throws IOException { running = true; - serverConfig = new ServerConfig(mfc.getHttpClient()); + serverConfig = new ServerConfig(mfc); List websocketServers = new ArrayList(serverConfig.wsServers.keySet()); String server = websocketServers.get((int) (Math.random()*websocketServers.size())); String wsUrl = "ws://" + server + ".myfreecams.com:8080/fcsl"; diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java index 1225ee45..8100b7b8 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java @@ -53,8 +53,8 @@ public class MyFreeCamsHttpClient extends HttpClient { .add("submit_login", "97") .build(); Request req = new Request.Builder() - .url(MyFreeCams.BASE_URI + "/php/login.php") - .header("Referer", MyFreeCams.BASE_URI) + .url(MyFreeCams.baseUrl + "/php/login.php") + .header("Referer", MyFreeCams.baseUrl) .header("Content-Type", "application/x-www-form-urlencoded") .post(body) .build(); @@ -75,7 +75,7 @@ public class MyFreeCamsHttpClient extends HttpClient { } private boolean checkLogin() throws IOException { - Request req = new Request.Builder().url(MyFreeCams.BASE_URI + "/php/account.php?request=status").build(); + Request req = new Request.Builder().url(MyFreeCams.baseUrl + "/php/account.php?request=status").build(); try(Response response = execute(req)) { if(response.isSuccessful()) { String content = response.body().string(); @@ -99,7 +99,7 @@ public class MyFreeCamsHttpClient extends HttpClient { public Cookie getCookie(String name) { CookieJar jar = client.cookieJar(); - HttpUrl url = HttpUrl.parse(MyFreeCams.BASE_URI); + HttpUrl url = HttpUrl.parse(MyFreeCams.baseUrl); List cookies = jar.loadForRequest(url); for (Cookie cookie : cookies) { if(Objects.equals(cookie.name(), name)) { diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index e0937cb5..d79afa01 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -132,7 +132,7 @@ public class MyFreeCamsModel extends AbstractModel { @Override public void receiveTip(int tokens) throws IOException { - String tipUrl = MyFreeCams.BASE_URI + "/php/tip.php"; + String tipUrl = MyFreeCams.baseUrl + "/php/tip.php"; String initUrl = tipUrl + "?request=tip&username="+getName()+"&broadcaster_id="+getUid(); Request req = new Request.Builder().url(initUrl).build(); try(Response resp = site.getHttpClient().execute(req)) { diff --git a/common/src/main/java/ctbrec/sites/mfc/ServerConfig.java b/common/src/main/java/ctbrec/sites/mfc/ServerConfig.java index 6713212c..a880e633 100644 --- a/common/src/main/java/ctbrec/sites/mfc/ServerConfig.java +++ b/common/src/main/java/ctbrec/sites/mfc/ServerConfig.java @@ -9,13 +9,16 @@ import java.util.Objects; import org.json.JSONArray; import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import ctbrec.io.HttpClient; import okhttp3.Request; import okhttp3.Response; public class ServerConfig { + private static final transient Logger LOG = LoggerFactory.getLogger(ServerConfig.class); + List ajaxServers; List videoServers; List chatServers; @@ -24,9 +27,11 @@ public class ServerConfig { Map wzobsServers; Map ngVideoServers; - public ServerConfig(HttpClient client) throws IOException { - Request req = new Request.Builder().url("http://www.myfreecams.com/_js/serverconfig.js").build(); - Response resp = client.execute(req); + public ServerConfig(MyFreeCams mfc) throws IOException { + String url = mfc.getBaseUrl() + "/_js/serverconfig.js"; + LOG.debug("Loading server config from {}", url); + Request req = new Request.Builder().url(url).build(); + Response resp = mfc.getHttpClient().execute(req); String json = resp.body().string(); JSONObject serverConfig = new JSONObject(json); From 3b3a804251213345d6752d71f112c3c0aa6a0853 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 2 Dec 2018 23:09:43 +0100 Subject: [PATCH 115/231] Use ctbrec fork of open-m3u8 Forked this project, because it is kind of inactive and ctbrec needs are more lenient implementation --- master/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/master/pom.xml b/master/pom.xml index 869bcf9e..20251429 100644 --- a/master/pom.xml +++ b/master/pom.xml @@ -68,7 +68,7 @@ com.iheartradio.m3u8 open-m3u8 - 0.2.4 + 0.2.7-CTBREC org.jcodec From 889dbecb144390caadb3580fc3471912e80cd493 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 3 Dec 2018 00:14:50 +0100 Subject: [PATCH 116/231] Bump version to 1.13.0 --- CHANGELOG.md | 9 +++++++-- client/pom.xml | 2 +- common/pom.xml | 2 +- master/pom.xml | 2 +- server/pom.xml | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b160520..7e3294b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,16 @@ -1.12.2 +1.13.0 ======================== -* Fix: Player not starting when path contains spaces +* Added possibility to open small live previews of online models + int the Recording tab * Added setting to toggle "Player Starting" message * Added possibility to add models by their URL * Added pause / resume all buttons +* Setting to define the base URL for MFC and CTB +* The paused checkbox are now clickable * Implemented multi-selection for Recording and Recordings tab +* Fix: Don't throw exceptions for unknown attributes in PlaylistParser * Fix: Don't do space check, if minimum is set to 0 +* Fix: Player not starting when path contains spaces 1.12.1 ======================== diff --git a/client/pom.xml b/client/pom.xml index 343c11d1..7b866a81 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.12.1 + 1.13.0 ../master diff --git a/common/pom.xml b/common/pom.xml index 3206ef56..3fef25a0 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.12.1 + 1.13.0 ../master diff --git a/master/pom.xml b/master/pom.xml index 20251429..8a107451 100644 --- a/master/pom.xml +++ b/master/pom.xml @@ -6,7 +6,7 @@ ctbrec master pom - 1.12.1 + 1.13.0 ../common diff --git a/server/pom.xml b/server/pom.xml index dcd1739e..32ae4365 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.12.1 + 1.13.0 ../master From 0d6c5627930234034ef2e50c9816bd50c0c3bb35 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 3 Dec 2018 00:41:22 +0100 Subject: [PATCH 117/231] Fix typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e3294b2..004bfdf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ 1.13.0 ======================== * Added possibility to open small live previews of online models - int the Recording tab + in the Recording tab * Added setting to toggle "Player Starting" message * Added possibility to add models by their URL * Added pause / resume all buttons From ad3343d6b66a297c9eed70f0a6348d8c241afe1f Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 3 Dec 2018 00:42:15 +0100 Subject: [PATCH 118/231] Update download links to 1.13.0 --- docs/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.html b/docs/index.html index ca67aab2..c8b66352 100644 --- a/docs/index.html +++ b/docs/index.html @@ -118,19 +118,19 @@
    - + Download for Linux! From da87a1ae3925bdafc01ec35923d5ef2b0f878664 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 3 Dec 2018 14:35:14 +0100 Subject: [PATCH 119/231] Add log message which websocket server is used --- common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index 6f167b77..b7b20df3 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -89,6 +89,7 @@ public class MyFreeCamsClient { List websocketServers = new ArrayList(serverConfig.wsServers.keySet()); String server = websocketServers.get((int) (Math.random()*websocketServers.size())); String wsUrl = "ws://" + server + ".myfreecams.com:8080/fcsl"; + LOG.debug("Connecting to random websocket server {}", wsUrl); Thread watchDog = new Thread(() -> { while(running) { From b97449a980c4a6637091c8d8c0c5da93dec2acd3 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 3 Dec 2018 15:24:44 +0100 Subject: [PATCH 120/231] Filter out websocket servers with the wrong protocol --- .../main/java/ctbrec/sites/mfc/MyFreeCamsClient.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index b7b20df3..dc1d6b3a 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ExecutorService; @@ -86,8 +87,13 @@ public class MyFreeCamsClient { public void start() throws IOException { running = true; serverConfig = new ServerConfig(mfc); - List websocketServers = new ArrayList(serverConfig.wsServers.keySet()); - String server = websocketServers.get((int) (Math.random()*websocketServers.size())); + List websocketServers = new ArrayList(serverConfig.wsServers.size()); + for (Entry entry : serverConfig.wsServers.entrySet()) { + if (entry.getValue().equals("rfc6455")) { + websocketServers.add(entry.getKey()); + } + } + String server = websocketServers.get((int) (Math.random() * websocketServers.size() - 1)); String wsUrl = "ws://" + server + ".myfreecams.com:8080/fcsl"; LOG.debug("Connecting to random websocket server {}", wsUrl); From cd903566de191b79bc317585cbdf80944b4c68be Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 3 Dec 2018 16:26:37 +0100 Subject: [PATCH 121/231] Use baseUrl in requestExtData --- common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index dc1d6b3a..ba137ef4 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -314,7 +314,7 @@ public class MyFreeCamsClient { long opts = json.getInt("opts"); long serv = json.getInt("serv"); long type = json.getInt("type"); - String base = "http://www.myfreecams.com/php/FcwExtResp.php"; + String base = mfc.getBaseUrl() + "/php/FcwExtResp.php"; String url = base + "?respkey="+respkey+"&opts="+opts+"&serv="+serv+"&type="+type; Request req = new Request.Builder().url(url).build(); LOG.trace("Requesting EXTDATA {}", url); From 1f6e03979edb9e6c3d55bbaed95c36fd40527a3a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 4 Dec 2018 17:08:44 +0100 Subject: [PATCH 122/231] Fix: ThumbCell resumes recordings This happens because the update services don't set the suspended property and ThumbCell copied the property from the updated model. So, suspended would be set to false, which would cause an update of the property change listener and that would restart the recording. --- client/src/main/java/ctbrec/ui/ThumbCell.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index ece9efce..8c5f9e05 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -492,7 +492,7 @@ public class ThumbCell extends StackPane { this.model.setPreview(model.getPreview()); this.model.setTags(model.getTags()); this.model.setUrl(model.getUrl()); - this.model.setSuspended(model.isSuspended()); + this.model.setSuspended(recorder.isSuspended(model)); update(); } From 9791427aeb9ab00bc0314482c2a7198b239abd7a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 4 Dec 2018 17:09:19 +0100 Subject: [PATCH 123/231] Add "Follow" to the context menu of the recorded models tab --- .../java/ctbrec/ui/RecordedModelsTab.java | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index f636b424..1d815ed6 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -3,6 +3,7 @@ package ctbrec.ui; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -296,13 +297,13 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { massEdit(models, action); } - private void massEdit(List models, Consumer action) { - getTabPane().setCursor(Cursor.WAIT); + private void massEdit(List models, Consumer action) { + table.setCursor(Cursor.WAIT); threadPool.submit(() -> { for (Model model : models) { action.accept(model); } - Platform.runLater(() -> getTabPane().setCursor(Cursor.DEFAULT)); + Platform.runLater(() -> table.setCursor(Cursor.DEFAULT)); }); } @@ -443,6 +444,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { openInPlayer.setOnAction((e) -> openInPlayer(selectedModels.get(0))); MenuItem switchStreamSource = new MenuItem("Switch resolution"); switchStreamSource.setOnAction((e) -> switchStreamSource(selectedModels.get(0))); + MenuItem follow = new MenuItem("Follow"); + follow.setOnAction((e) -> follow(selectedModels)); ContextMenu menu = new ContextMenu(stop); if (selectedModels.size() == 1) { @@ -450,7 +453,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } else { menu.getItems().addAll(resumeRecording, pauseRecording); } - menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, switchStreamSource); + menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, switchStreamSource, follow); if (selectedModels.size() > 1) { copyUrl.setDisable(true); @@ -462,6 +465,19 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { return menu; } + private void follow(ObservableList selectedModels) { + Consumer action = (m) -> { + try { + m.follow(); + } catch(Throwable e) { + LOG.error("Couldn't follow model {}", m, e); + Platform.runLater(() -> + showErrorDialog(e, "Couldn't follow model", "Following " + m.getName() + " failed: " + e.getMessage())); + } + }; + massEdit(new ArrayList(selectedModels), action); + } + private void openInPlayer(JavaFxModel selectedModel) { table.setCursor(Cursor.WAIT); new Thread(() -> { @@ -533,8 +549,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { showErrorDialog(e, "Couldn't stop recording", "Stopping recording of " + m.getName() + " failed")); } }; - List models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList()); - massEdit(models, action); + massEdit(new ArrayList(selectedModels), action); }; private void pauseRecording(List selectedModels) { @@ -547,8 +562,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { showErrorDialog(e, "Couldn't pause recording of model", "Pausing recording of " + m.getName() + " failed")); } }; - List models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList()); - massEdit(models, action); + massEdit(new ArrayList(selectedModels), action); }; private void resumeRecording(List selectedModels) { @@ -561,8 +575,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { showErrorDialog(e, "Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed")); } }; - List models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList()); - massEdit(models, action); + massEdit(new ArrayList(selectedModels), action); } public void saveState() { From 45e493a35a37be53f163ab4cdfffc84221daf922 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 4 Dec 2018 18:28:30 +0100 Subject: [PATCH 124/231] Add javadoc --- common/src/main/java/ctbrec/Model.java | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/common/src/main/java/ctbrec/Model.java b/common/src/main/java/ctbrec/Model.java index e13f2fcd..895f711e 100644 --- a/common/src/main/java/ctbrec/Model.java +++ b/common/src/main/java/ctbrec/Model.java @@ -14,33 +14,71 @@ import ctbrec.sites.Site; public interface Model { public String getUrl(); + public void setUrl(String url); + public String getDisplayName(); + public void setDisplayName(String name); + public String getName(); + public void setName(String name); + public String getPreview(); + public void setPreview(String preview); + public List getTags(); + public void setTags(List tags); + public String getDescription(); + public void setDescription(String description); + public int getStreamUrlIndex(); + public void setStreamUrlIndex(int streamUrlIndex); + public boolean isOnline() throws IOException, ExecutionException, InterruptedException; + public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException; + public String getOnlineState(boolean failFast) throws IOException, ExecutionException; + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException; + public void invalidateCacheEntries(); + public void receiveTip(int tokens) throws IOException; + + /** + * Determines the stream resolution for this model + * + * @param failFast + * If set to true, the method returns emmediately, even if the resolution is unknown. If + * the resolution is unknown, the array contains 0,0 + * + * @return a tupel of width and height represented by an int[2] + * @throws ExecutionException + */ public int[] getStreamResolution(boolean failFast) throws ExecutionException; + public boolean follow() throws IOException; + public boolean unfollow() throws IOException; + public void setSite(Site site); + public Site getSite(); + public void writeSiteSpecificData(JsonWriter writer) throws IOException; + public void readSiteSpecificData(JsonReader reader) throws IOException; + public boolean isSuspended(); + public void setSuspended(boolean suspended); } \ No newline at end of file From b99e88d2c8c4aa8057faeb44e0d5228a8d93a1a2 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 4 Dec 2018 18:30:46 +0100 Subject: [PATCH 125/231] Add cache for the resolution This makes the display of the resolution much faster and the information is retained, even if the ThumbCell is "destroyed" --- client/src/main/java/ctbrec/ui/ThumbCell.java | 69 ++++++++++++------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index 8c5f9e05..5e9bdb16 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -7,11 +7,14 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.iheartradio.m3u8.ParseException; import ctbrec.Config; @@ -80,6 +83,10 @@ public class ThumbCell extends StackPane { private boolean mouseHovering = false; private boolean recording = false; private static ExecutorService imageLoadingThreadPool = Executors.newFixedThreadPool(30); + private static Cache resolutionCache = CacheBuilder.newBuilder() + .expireAfterAccess(4, TimeUnit.HOURS) + .maximumSize(1000) + .build(); public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder) { this.thumbCellList = parent.grid.getChildren(); @@ -212,35 +219,45 @@ public class ThumbCell extends StackPane { return; } - ThumbOverviewTab.threadPool.submit(() -> { + int[] resolution = resolutionCache.getIfPresent(model); + if(resolution != null) { try { - ThumbOverviewTab.resolutionProcessing.add(model); - int[] resolution = model.getStreamResolution(false); updateResolutionTag(resolution); - - // the model is online, but the resolution is 0. probably something went wrong - // when we first requested the stream info, so we remove this invalid value from the "cache" - // so that it is requested again - if (model.isOnline() && resolution[1] == 0) { - LOG.trace("Removing invalid resolution value for {}", model.getName()); - model.invalidateCacheEntries(); - } - - Thread.sleep(500); - } catch (IOException | InterruptedException e1) { - LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1); - } catch(ExecutionException e) { - if(e.getCause() instanceof EOFException) { - LOG.warn("Couldn't update resolution tag for model {}. Playlist empty", model.getName()); - } else if(e.getCause() instanceof ParseException) { - LOG.warn("Couldn't update resolution tag for model {} - {}", model.getName(), e.getMessage()); - } else { - LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e); - } - } finally { - ThumbOverviewTab.resolutionProcessing.remove(model); + } catch(Exception e) { + LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e); } - }); + } else { + ThumbOverviewTab.threadPool.submit(() -> { + try { + ThumbOverviewTab.resolutionProcessing.add(model); + int[] _resolution = model.getStreamResolution(false); + resolutionCache.put(model, _resolution); + updateResolutionTag(_resolution); + + // the model is online, but the resolution is 0. probably something went wrong + // when we first requested the stream info, so we remove this invalid value from the "cache" + // so that it is requested again + if (model.isOnline() && _resolution[1] == 0) { + LOG.trace("Removing invalid resolution value for {}", model.getName()); + model.invalidateCacheEntries(); + } + + Thread.sleep(100); + } catch (IOException | InterruptedException e1) { + LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1); + } catch(ExecutionException e) { + if(e.getCause() instanceof EOFException) { + LOG.warn("Couldn't update resolution tag for model {}. Playlist empty", model.getName()); + } else if(e.getCause() instanceof ParseException) { + LOG.warn("Couldn't update resolution tag for model {} - {}", model.getName(), e.getMessage()); + } else { + LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e); + } + } finally { + ThumbOverviewTab.resolutionProcessing.remove(model); + } + }); + } } private void updateResolutionTag(int[] resolution) throws IOException, ExecutionException, InterruptedException { From d4dadf9fea891806dc0b7b9dc46c5e506c82492a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 4 Dec 2018 18:31:31 +0100 Subject: [PATCH 126/231] Remove resolution cache Resolution caching is done globally in ThumbCell --- .../ctbrec/sites/chaturbate/Chaturbate.java | 18 ++--------------- .../sites/chaturbate/ChaturbateModel.java | 20 +++++++++---------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java index bd96ad96..56dffb1b 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java @@ -189,17 +189,6 @@ public class Chaturbate extends AbstractSite { } }); - LoadingCache streamResolutionCache = CacheBuilder.newBuilder() - .initialCapacity(10_000) - .maximumSize(10_000) - .expireAfterWrite(5, TimeUnit.MINUTES) - .build(new CacheLoader () { - @Override - public int[] load(String model) throws Exception { - return loadResolution(model); - } - }); - public void sendTip(String name, int tokens) throws IOException { if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { RequestBody body = new FormBody.Builder() @@ -264,11 +253,9 @@ public class Chaturbate extends AbstractSite { } } - public int[] getResolution(String modelName) throws ExecutionException { - return streamResolutionCache.get(modelName); - } + public int[] getResolution(String modelName) throws ExecutionException, IOException, ParseException, PlaylistException, InterruptedException { + throttleRequests(); - private int[] loadResolution(String modelName) throws IOException, ParseException, PlaylistException, ExecutionException, InterruptedException { int[] res = new int[2]; StreamInfo streamInfo = getStreamInfo(modelName); if(!streamInfo.url.startsWith("http")) { @@ -303,7 +290,6 @@ public class Chaturbate extends AbstractSite { throw ex; } - streamResolutionCache.put(modelName, res); return res; } diff --git a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java index 5ca806c1..5a6acce3 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java @@ -25,6 +25,7 @@ import okhttp3.Response; public class ChaturbateModel extends AbstractModel { private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateModel.class); + private int[] resolution = new int[2]; /** * This constructor exists only for deserialization. Please don't call it directly @@ -52,16 +53,16 @@ public class ChaturbateModel extends AbstractModel { @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { - int[] resolution = getChaturbate().streamResolutionCache.getIfPresent(getName()); - if(resolution != null) { - return getChaturbate().getResolution(getName()); - } else { - if(failFast) { - return new int[2]; - } else { - return getChaturbate().getResolution(getName()); - } + if(failFast) { + return resolution; } + + try { + resolution = getChaturbate().getResolution(getName()); + } catch(Exception e) { + throw new ExecutionException(e); + } + return resolution; } /** @@ -71,7 +72,6 @@ public class ChaturbateModel extends AbstractModel { @Override public void invalidateCacheEntries() { getChaturbate().streamInfoCache.invalidate(getName()); - getChaturbate().streamResolutionCache.invalidate(getName()); } public String getOnlineState() throws IOException, ExecutionException { From 8abb3db8a53ebbdd7d3ebacc3ae7dddc09edb420 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 5 Dec 2018 12:10:21 +0100 Subject: [PATCH 127/231] Remove single thread executor --- .../ctbrec/sites/mfc/MyFreeCamsClient.java | 7 ----- .../ctbrec/sites/mfc/MyFreeCamsModel.java | 31 +++++++------------ 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index ba137ef4..c9317abf 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -13,8 +13,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -53,7 +51,6 @@ public class MyFreeCamsClient { private Cache sessionStates = CacheBuilder.newBuilder().maximumSize(4000).build(); private Cache models = CacheBuilder.newBuilder().maximumSize(4000).build(); private Lock lock = new ReentrantLock(); - private ExecutorService executor = Executors.newSingleThreadExecutor(); private ServerConfig serverConfig; @SuppressWarnings("unused") private String tkx; @@ -574,10 +571,6 @@ public class MyFreeCamsClient { return models.getIfPresent(uid); } - public void execute(Runnable r) { - executor.execute(r); - } - public void getSessionState(ctbrec.Model model) { for (SessionState state : sessionStates.asMap().values()) { if(Objects.equals(state.getNm(), model.getName())) { diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index d79afa01..a85c3425 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -45,7 +45,7 @@ public class MyFreeCamsModel extends AbstractModel { private double camScore; private int viewerCount; private State state; - private int resolution[]; + private int resolution[] = new int[2]; /** * This constructor exists only for deserialization. Please don't call it directly @@ -174,26 +174,19 @@ public class MyFreeCamsModel extends AbstractModel { @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { - if(resolution == null) { - if(failFast || hlsUrl == null) { - return new int[2]; + if (!failFast && hlsUrl != null) { + try { + List streamSources = getStreamSources(); + Collections.sort(streamSources); + StreamSource best = streamSources.get(streamSources.size() - 1); + resolution = new int[] { best.width, best.height }; + } catch (ParseException | PlaylistException e) { + LOG.warn("Couldn't determine stream resolution - {}", e.getMessage()); + } catch (ExecutionException | IOException e) { + LOG.error("Couldn't determine stream resolution", e); } - MyFreeCamsClient.getInstance().execute(()->{ - try { - List streamSources = getStreamSources(); - Collections.sort(streamSources); - StreamSource best = streamSources.get(streamSources.size()-1); - resolution = new int[] {best.width, best.height}; - } catch (ParseException | PlaylistException e) { - LOG.warn("Couldn't determine stream resolution - {}", e.getMessage()); - } catch (ExecutionException | IOException e) { - LOG.error("Couldn't determine stream resolution", e); - } - }); - return new int[2]; - } else { - return resolution; } + return resolution; } public void setStreamUrl(String hlsUrl) { From 6db00969d7c30164b840de27cc0e098aafd2fc9d Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 5 Dec 2018 12:20:26 +0100 Subject: [PATCH 128/231] Revert: RemoteRecorder does not work, if called with JavaFxModels It does not work, because it uses the class name for the type and the server doesn't know JavaFxModel. It only knowns the unwrapped model classes. --- client/src/main/java/ctbrec/ui/RecordedModelsTab.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index 1d815ed6..878c8d00 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -549,7 +549,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { showErrorDialog(e, "Couldn't stop recording", "Stopping recording of " + m.getName() + " failed")); } }; - massEdit(new ArrayList(selectedModels), action); + List models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList()); + massEdit(models, action); }; private void pauseRecording(List selectedModels) { @@ -562,7 +563,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { showErrorDialog(e, "Couldn't pause recording of model", "Pausing recording of " + m.getName() + " failed")); } }; - massEdit(new ArrayList(selectedModels), action); + List models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList()); + massEdit(models, action); }; private void resumeRecording(List selectedModels) { @@ -575,7 +577,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { showErrorDialog(e, "Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed")); } }; - massEdit(new ArrayList(selectedModels), action); + List models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList()); + massEdit(models, action); } public void saveState() { From 28fee0b2e6abc28035d94ede98e32db8078973e8 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 5 Dec 2018 12:20:50 +0100 Subject: [PATCH 129/231] Preselect the right entry, if stream url index is set --- .../src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java b/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java index 2347493b..2ff98c50 100644 --- a/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java +++ b/client/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java @@ -25,7 +25,8 @@ public class StreamSourceSelectionDialog { List sources; try { sources = selectStreamSource.get(); - ChoiceDialog choiceDialog = new ChoiceDialog(sources.get(sources.size()-1), sources); + int selectedIndex = model.getStreamUrlIndex() > -1 ? Math.min(model.getStreamUrlIndex(), sources.size()-1) : sources.size()-1; + ChoiceDialog choiceDialog = new ChoiceDialog(sources.get(selectedIndex), sources); choiceDialog.setTitle("Stream Quality"); choiceDialog.setHeaderText("Select your preferred stream quality"); choiceDialog.setResizable(true); From a7b0b3f37478bcf4a8c8342a7b100760b9bcdece Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 5 Dec 2018 12:30:27 +0100 Subject: [PATCH 130/231] Remove resolution cache Resolutions are cached by ThumbCell --- .../java/ctbrec/sites/camsoda/CamsodaModel.java | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index b3d03d94..3c938eb4 100644 --- a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -6,14 +6,11 @@ import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; import com.iheartradio.m3u8.Encoding; import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; @@ -41,12 +38,7 @@ public class CamsodaModel extends AbstractModel { private List streamSources = null; private String status = "n/a"; private float sortOrder = 0; - - private static Cache streamResolutionCache = CacheBuilder.newBuilder() - .initialCapacity(10_000) - .maximumSize(10_000) - .expireAfterWrite(30, TimeUnit.MINUTES) - .build(); + int[] resolution = new int[2]; public String getStreamUrl() throws IOException { if(streamUrl == null) { @@ -139,13 +131,11 @@ public class CamsodaModel extends AbstractModel { @Override public void invalidateCacheEntries() { streamSources = null; - streamResolutionCache.invalidate(getName()); } @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { - int[] resolution = streamResolutionCache.getIfPresent(getName()); - if(resolution != null) { + if(failFast) { return resolution; } else { if(failFast) { @@ -158,7 +148,6 @@ public class CamsodaModel extends AbstractModel { } else { StreamSource src = streamSources.get(0); resolution = new int[] {src.width, src.height}; - streamResolutionCache.put(getName(), resolution); return resolution; } } catch (IOException | ParseException | PlaylistException e) { From 9109fc8689da63f6e3c85358a6ce88662bb904c2 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 5 Dec 2018 12:38:06 +0100 Subject: [PATCH 131/231] Display "unkown resolution" instead of Integer.MAX_VALUE --- .../main/java/ctbrec/recorder/download/StreamSource.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/recorder/download/StreamSource.java b/common/src/main/java/ctbrec/recorder/download/StreamSource.java index dbb86f03..6044716b 100644 --- a/common/src/main/java/ctbrec/recorder/download/StreamSource.java +++ b/common/src/main/java/ctbrec/recorder/download/StreamSource.java @@ -44,7 +44,11 @@ public class StreamSource implements Comparable { public String toString() { DecimalFormat df = new DecimalFormat("0.00"); float mbit = bandwidth / 1024.0f / 1024.0f; - return height + "p (" + df.format(mbit) + " Mbit/s)"; + if(height == Integer.MAX_VALUE) { + return "unknown resolution (" + df.format(mbit) + " Mbit/s)"; + } else { + return height + "p (" + df.format(mbit) + " Mbit/s)"; + } } /** From 42177b4399f0129984c79c541a7fd1bdb66ce1ac Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 5 Dec 2018 15:51:21 +0100 Subject: [PATCH 132/231] Add enum to Model for the online states --- .../src/main/java/ctbrec/ui/JavaFxModel.java | 2 +- client/src/main/java/ctbrec/ui/ThumbCell.java | 20 +++--- .../main/java/ctbrec/ui/ThumbOverviewTab.java | 1 - .../sites/bonga/BongaCamsUpdateService.java | 24 +++++-- .../sites/cam4/Cam4FollowedUpdateService.java | 2 +- .../camsoda/CamsodaFollowedUpdateService.java | 4 +- .../sites/camsoda/CamsodaUpdateService.java | 2 +- .../src/main/java/ctbrec/AbstractModel.java | 10 +++ common/src/main/java/ctbrec/Model.java | 4 +- .../ctbrec/sites/bonga/BongaCamsModel.java | 5 +- .../java/ctbrec/sites/cam4/Cam4Model.java | 64 +++++++++++++++---- .../ctbrec/sites/camsoda/CamsodaModel.java | 42 ++++++++---- .../sites/chaturbate/ChaturbateModel.java | 42 ++++++++++-- .../ctbrec/sites/mfc/MyFreeCamsModel.java | 38 ++++++----- 14 files changed, 186 insertions(+), 74 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java index 99c4fcb6..e57cc45d 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java @@ -110,7 +110,7 @@ public class JavaFxModel implements Model { } @Override - public String getOnlineState(boolean failFast) throws IOException, ExecutionException { + public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { return delegate.getOnlineState(failFast); } diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index 5e9bdb16..914f2916 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -192,10 +192,6 @@ public class ThumbCell extends StackPane { setThumbWidth(Config.getInstance().getSettings().thumbWidth); setRecording(recording); - if(Config.getInstance().getSettings().determineResolution) { - determineResolution(); - } - update(); } @@ -221,11 +217,13 @@ public class ThumbCell extends StackPane { int[] resolution = resolutionCache.getIfPresent(model); if(resolution != null) { - try { - updateResolutionTag(resolution); - } catch(Exception e) { - LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e); - } + ThumbOverviewTab.threadPool.submit(() -> { + try { + updateResolutionTag(resolution); + } catch(Exception e) { + LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e); + } + }); } else { ThumbOverviewTab.threadPool.submit(() -> { try { @@ -263,14 +261,14 @@ public class ThumbCell extends StackPane { private void updateResolutionTag(int[] resolution) throws IOException, ExecutionException, InterruptedException { String _res = "n/a"; Paint resolutionBackgroundColor = resolutionOnlineColor; - String state = model.getOnlineState(false); + String state = model.getOnlineState(false).toString(); if (model.isOnline()) { LOG.trace("Model resolution {} {}x{}", model.getName(), resolution[0], resolution[1]); LOG.trace("Resolution queue size: {}", ThumbOverviewTab.queue.size()); final int w = resolution[1]; _res = w > 0 ? w != Integer.MAX_VALUE ? Integer.toString(w) : "HD" : state; } else { - _res = model.getOnlineState(false); + _res = model.getOnlineState(false).toString(); resolutionBackgroundColor = resolutionOfflineColor; } final String resText = _res; diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index c12bc189..00d3fe77 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -340,7 +340,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } List models = updateService.getValue(); updateGrid(models); - } protected void updateGrid(List models) { diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java index cd52462a..06b1824c 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java @@ -1,5 +1,7 @@ package ctbrec.ui.sites.bonga; +import static ctbrec.Model.STATUS.*; + import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -58,16 +60,30 @@ public class BongaCamsUpdateService extends PaginatedScheduledService { BongaCamsModel model = (BongaCamsModel) bongaCams.createModel(name); model.setUserId(m.getInt("user_id")); boolean away = m.optBoolean("is_away"); - boolean online = m.optBoolean("online") && !away; + boolean online = m.optBoolean("online"); model.setOnline(online); + if(online) { + model.setOnlineState(ONLINE); if(away) { - model.setOnlineState("away"); + model.setOnlineState(AWAY); } else { - model.setOnlineState(m.getString("room")); + switch(m.optString("room")) { + case "private": + case "fullprivate": + model.setOnlineState(PRIVATE); + break; + case "group": + case "public": + model.setOnlineState(ONLINE); + break; + default: + LOG.debug(m.optString("room")); + model.setOnlineState(ONLINE); + } } } else { - model.setOnlineState("offline"); + model.setOnlineState(OFFLINE); } model.setPreview("https:" + m.getString("thumb_image")); if(m.has("display_name")) { diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4FollowedUpdateService.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4FollowedUpdateService.java index 60047998..4b68232b 100644 --- a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4FollowedUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4FollowedUpdateService.java @@ -68,7 +68,7 @@ public class Cam4FollowedUpdateService extends PaginatedScheduledService { 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)); + model.setOnlineStateByShowType(parseOnlineState(cellHtml)); models.add(model); } return models.stream() diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaFollowedUpdateService.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaFollowedUpdateService.java index 4e92d25c..39935366 100644 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaFollowedUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaFollowedUpdateService.java @@ -1,5 +1,7 @@ package ctbrec.ui.sites.camsoda; +import static ctbrec.Model.STATUS.*; + import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -47,7 +49,7 @@ public class CamsodaFollowedUpdateService extends PaginatedScheduledService { JSONObject m = following.getJSONObject(i); CamsodaModel model = (CamsodaModel) camsoda.createModel(m.getString("followname")); boolean online = m.getInt("online") == 1; - model.setOnlineState(online ? "online" : "offline"); + model.setOnlineState(online ? ONLINE : OFFLINE); model.setPreview("https://md.camsoda.com/thumbs/" + model.getName() + ".jpg"); models.add(model); } diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java index 4b035962..3e3ff047 100644 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java @@ -89,7 +89,7 @@ public class CamsodaUpdateService extends PaginatedScheduledService { model.setSortOrder(result.getFloat("sort_value")); models.add(model); if(result.has("status")) { - model.setOnlineState(result.getString("status")); + model.setOnlineStateByStatus(result.getString("status")); } if(result.has("display_name")) { diff --git a/common/src/main/java/ctbrec/AbstractModel.java b/common/src/main/java/ctbrec/AbstractModel.java index 61238759..2c925369 100644 --- a/common/src/main/java/ctbrec/AbstractModel.java +++ b/common/src/main/java/ctbrec/AbstractModel.java @@ -21,6 +21,7 @@ public abstract class AbstractModel implements Model { private int streamUrlIndex = -1; private boolean suspended = false; protected Site site; + protected STATUS onlineState = STATUS.UNKNOWN; @Override public boolean isOnline() throws IOException, ExecutionException, InterruptedException { @@ -121,6 +122,15 @@ public abstract class AbstractModel implements Model { this.suspended = suspended; } + @Override + public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { + return onlineState; + } + + public void setOnlineState(STATUS status) { + this.onlineState = status; + } + @Override public int hashCode() { final int prime = 31; diff --git a/common/src/main/java/ctbrec/Model.java b/common/src/main/java/ctbrec/Model.java index 81cf2497..7df65838 100644 --- a/common/src/main/java/ctbrec/Model.java +++ b/common/src/main/java/ctbrec/Model.java @@ -65,7 +65,7 @@ public interface Model { public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException; - public String getOnlineState(boolean failFast) throws IOException, ExecutionException; + public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException; public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException; @@ -101,4 +101,6 @@ public interface Model { public void setSuspended(boolean suspended); + + } \ No newline at end of file diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java b/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java index eaad5853..2391547c 100644 --- a/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java +++ b/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java @@ -36,7 +36,6 @@ public class BongaCamsModel extends AbstractModel { private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsModel.class); private int userId; - private String onlineState = "n/a"; private boolean online = false; private List streamSources = new ArrayList<>(); private int[] resolution; @@ -84,11 +83,11 @@ public class BongaCamsModel extends AbstractModel { } @Override - public String getOnlineState(boolean failFast) throws IOException, ExecutionException { + public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { return onlineState; } - public void setOnlineState(String onlineState) { + public void setOnlineState(STATUS onlineState) { this.onlineState = onlineState; } diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java index 9381b9ad..680b0d59 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java @@ -1,5 +1,7 @@ package ctbrec.sites.cam4; +import static ctbrec.Model.STATUS.*; + import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -39,22 +41,19 @@ public class Cam4Model extends AbstractModel { private static final transient Logger LOG = LoggerFactory.getLogger(Cam4Model.class); private String playlistUrl; - private String onlineState = "offline"; private int[] resolution = null; private boolean privateRoom = false; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { - if(ignoreCache || onlineState == null) { + if(ignoreCache || onlineState == UNKNOWN) { try { loadModelDetails(); } catch (ModelDetailsEmptyException e) { return false; } } - return (Objects.equals("NORMAL", onlineState) || Objects.equals("GROUP_SHOW_SELLING_TICKETS", onlineState)) - && StringUtil.isNotBlank(playlistUrl) - && !privateRoom; + return onlineState == ONLINE && StringUtil.isNotBlank(playlistUrl) && !privateRoom; } private void loadModelDetails() throws IOException, ModelDetailsEmptyException { @@ -65,13 +64,17 @@ public class Cam4Model extends AbstractModel { if(response.isSuccessful()) { JSONArray json = new JSONArray(response.body().string()); if(json.length() == 0) { - onlineState = "offline"; + onlineState = OFFLINE; throw new ModelDetailsEmptyException("Model details are empty"); } JSONObject details = json.getJSONObject(0); - onlineState = details.getString("showType"); + String showType = details.getString("showType"); + setOnlineStateByShowType(showType); playlistUrl = details.getString("hlsPreviewUrl"); privateRoom = details.getBoolean("privateRoom"); + if(privateRoom) { + onlineState = PRIVATE; + } if(details.has("resolution")) { String res = details.getString("resolution"); String[] tokens = res.split(":"); @@ -83,9 +86,42 @@ public class Cam4Model extends AbstractModel { } } + public void setOnlineStateByShowType(String showType) { + switch(showType) { + case "NORMAL": + case "GROUP_SHOW_SELLING_TICKETS": + onlineState = ONLINE; + break; + case "PRIVATE_SHOW": + onlineState = PRIVATE; + break; + case "GROUP_SHOW": + onlineState = GROUP; + break; + case "OFFLINE": + onlineState = OFFLINE; + break; + default: + LOG.debug("Unknown show type {}", showType); + onlineState = UNKNOWN; + } + + } + @Override - public String getOnlineState(boolean failFast) throws IOException, ExecutionException { - return onlineState; + public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { + if(failFast) { + return onlineState; + } else { + if(onlineState == UNKNOWN) { + try { + loadModelDetails(); + } catch (ModelDetailsEmptyException e) { + LOG.warn("Couldn't load model details", e.getMessage()); + } + } + return onlineState; + } } private String getPlaylistUrl() throws IOException { @@ -152,7 +188,11 @@ public class Cam4Model extends AbstractModel { return new int[2]; } else { try { - loadModelDetails(); + if(onlineState != OFFLINE) { + loadModelDetails(); + } else { + resolution = new int[2]; + } } catch (Exception e) { throw new ExecutionException(e); } @@ -226,10 +266,6 @@ public class Cam4Model extends AbstractModel { this.playlistUrl = playlistUrl; } - public void setOnlineState(String onlineState) { - this.onlineState = onlineState; - } - public class ModelDetailsEmptyException extends Exception { public ModelDetailsEmptyException(String msg) { super(msg); diff --git a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index 3c938eb4..12421216 100644 --- a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -1,5 +1,7 @@ package ctbrec.sites.camsoda; +import static ctbrec.Model.STATUS.*; + import java.io.IOException; import java.io.InputStream; import java.util.Collections; @@ -36,7 +38,6 @@ public class CamsodaModel extends AbstractModel { private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaModel.class); private String streamUrl; private List streamSources = null; - private String status = "n/a"; private float sortOrder = 0; int[] resolution = new int[2]; @@ -56,7 +57,8 @@ public class CamsodaModel extends AbstractModel { JSONObject result = new JSONObject(response.body().string()); if(result.getBoolean("status")) { JSONObject chat = result.getJSONObject("user").getJSONObject("chat"); - status = chat.getString("status"); + String status = chat.getString("status"); + setOnlineStateByStatus(status); if(chat.has("edge_servers")) { String edgeServer = chat.getJSONArray("edge_servers").getString(0); String streamName = chat.getString("stream_name"); @@ -71,30 +73,46 @@ public class CamsodaModel extends AbstractModel { } } + public void setOnlineStateByStatus(String status) { + switch(status) { + case "online": + onlineState = ONLINE; + break; + case "offline": + onlineState = OFFLINE; + break; + case "private": + onlineState = PRIVATE; + break; + case "limited": + onlineState = GROUP; + break; + default: + LOG.debug("Unknown show type {}", status); + onlineState = UNKNOWN; + } + } + @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { - if(ignoreCache) { + if(ignoreCache || onlineState == UNKNOWN) { loadModel(); } - return Objects.equals(status, "online"); + return onlineState == ONLINE; } @Override - public String getOnlineState(boolean failFast) throws IOException, ExecutionException { + public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { if(failFast) { - return status; + return onlineState; } else { - if(status.equals("n/a")) { + if(onlineState == UNKNOWN) { loadModel(); } - return status; + return onlineState; } } - public void setOnlineState(String state) { - this.status = state; - } - @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { String streamUrl = getStreamUrl(); diff --git a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java index 5a6acce3..faad4fd8 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java @@ -1,5 +1,7 @@ package ctbrec.sites.chaturbate; +import static ctbrec.Model.STATUS.*; + import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -74,14 +76,46 @@ public class ChaturbateModel extends AbstractModel { getChaturbate().streamInfoCache.invalidate(getName()); } - public String getOnlineState() throws IOException, ExecutionException { + public STATUS getOnlineState() throws IOException, ExecutionException { return getOnlineState(false); } @Override - public String getOnlineState(boolean failFast) throws IOException, ExecutionException { - StreamInfo info = getChaturbate().streamInfoCache.getIfPresent(getName()); - return info != null ? info.room_status : "n/a"; + public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { + if(failFast) { + StreamInfo info = getChaturbate().streamInfoCache.getIfPresent(getName()); + setOnlineStateByRoomStatus(info.room_status); + } else { + StreamInfo info = getChaturbate().streamInfoCache.get(getName()); + setOnlineStateByRoomStatus(info.room_status); + } + return onlineState; + } + + private void setOnlineStateByRoomStatus(String room_status) { + if(room_status != null) { + switch(room_status) { + case "public": + onlineState = ONLINE; + break; + case "offline": + onlineState = OFFLINE; + break; + case "private": + case "hidden": + onlineState = PRIVATE; + break; + case "away": + onlineState = AWAY; + break; + case "group": + onlineState = STATUS.GROUP; + break; + default: + LOG.debug("Unknown show type {}", room_status); + onlineState = STATUS.UNKNOWN; + } + } } public StreamInfo getStreamInfo() throws IOException, ExecutionException { diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index 5bb3d998..69e67bed 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -68,28 +68,26 @@ public class MyFreeCamsModel extends AbstractModel { } @Override - public String getOnlineState(boolean failFast) throws IOException, ExecutionException { - return state != null ? state.toString() : "offline"; + public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { + switch(this.state) { + case ONLINE: + case RECORDING: + return ctbrec.Model.STATUS.ONLINE; + case AWAY: + return ctbrec.Model.STATUS.AWAY; + case PRIVATE: + return ctbrec.Model.STATUS.PRIVATE; + case GROUP_SHOW: + return ctbrec.Model.STATUS.GROUP; + case OFFLINE: + case CAMOFF: + return ctbrec.Model.STATUS.OFFLINE; + default: + LOG.debug("State {} is not mapped", this.state); + return ctbrec.Model.STATUS.UNKNOWN; + } } - // @Override - // public STATUS getOnlineState() { - // switch(this.state) { - // case ONLINE: - // case RECORDING: - // return ctbrec.Model.STATUS.ONLINE; - // case AWAY: - // return ctbrec.Model.STATUS.AWAY; - // case PRIVATE: - // return ctbrec.Model.STATUS.PRIVATE; - // case GROUP_SHOW: - // return ctbrec.Model.STATUS.GROUP; - // default: - // LOG.debug("State {} is not mapped", this.state); - // return ctbrec.Model.STATUS.UNKNOWN; - // } - // } - @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { MasterPlaylist masterPlaylist = getMasterPlaylist(); From e6476e95ec174a36c615e02b284b261c5e2041a1 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 5 Dec 2018 16:08:44 +0100 Subject: [PATCH 133/231] Add setting to ignore the upscaled video stream on MFC --- .../sites/myfreecams/MyFreeCamsConfigUI.java | 25 +++++++++++++------ common/src/main/java/ctbrec/Settings.java | 1 + .../ctbrec/sites/mfc/MyFreeCamsModel.java | 10 +++++++- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java index bf64358b..0783a68a 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java @@ -8,6 +8,7 @@ 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; @@ -23,8 +24,9 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { @Override public Parent createConfigPanel() { + int row = 0; GridPane layout = SettingsTab.createGridLayout(); - layout.add(new Label("MyFreeCams User"), 0, 0); + layout.add(new Label("MyFreeCams User"), 0, row); TextField username = new TextField(Config.getInstance().getSettings().mfcUsername); username.setPrefWidth(300); username.textProperty().addListener((ob, o, n) -> { @@ -34,9 +36,9 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); GridPane.setColumnSpan(username, 2); - layout.add(username, 1, 0); + layout.add(username, 1, row++); - layout.add(new Label("MyFreeCams Password"), 0, 1); + layout.add(new Label("MyFreeCams Password"), 0, row); PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().mfcPassword); password.textProperty().addListener((ob, o, n) -> { @@ -46,9 +48,9 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); GridPane.setColumnSpan(password, 2); - layout.add(password, 1, 1); + layout.add(password, 1, row++); - layout.add(new Label("MyFreeCams Base URL"), 0, 2); + layout.add(new Label("MyFreeCams Base URL"), 0, row); TextField baseUrl = new TextField(); baseUrl.setText(Config.getInstance().getSettings().mfcBaseUrl); baseUrl.textProperty().addListener((ob, o, n) -> { @@ -58,15 +60,24 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { GridPane.setFillWidth(baseUrl, true); GridPane.setHgrow(baseUrl, Priority.ALWAYS); GridPane.setColumnSpan(baseUrl, 2); - layout.add(baseUrl, 1, 2); + layout.add(baseUrl, 1, row++); + + layout.add(new Label("Ignore upscaled stream (960p)"), 0, row); + CheckBox ignoreUpscaled = new CheckBox(); + ignoreUpscaled.setSelected(Config.getInstance().getSettings().mfcIgnoreUpscaled); + ignoreUpscaled.selectedProperty().addListener((obs, oldV, newV) -> { + Config.getInstance().getSettings().mfcIgnoreUpscaled = newV; + }); + layout.add(ignoreUpscaled, 1, row++); Button createAccount = new Button("Create new Account"); createAccount.setOnAction((e) -> DesktopIntegration.open(myFreeCams.getAffiliateLink())); - layout.add(createAccount, 1, 3); + 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(baseUrl, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(ignoreUpscaled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); return layout; diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 64fc8e42..bb19eb34 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -49,6 +49,7 @@ public class Settings { public String mfcUsername = ""; public String mfcPassword = ""; public String mfcBaseUrl = "https://www.myfreecams.com"; + public boolean mfcIgnoreUpscaled = false; public String camsodaUsername = ""; public String camsodaPassword = ""; public String cam4Username; diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index a85c3425..ec8301ef 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; import org.jsoup.nodes.Element; import org.slf4j.Logger; @@ -28,6 +29,7 @@ import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import ctbrec.AbstractModel; +import ctbrec.Config; import ctbrec.io.HtmlParser; import ctbrec.io.HttpException; import ctbrec.recorder.download.StreamSource; @@ -95,7 +97,13 @@ public class MyFreeCamsModel extends AbstractModel { sources.add(src); } } - return sources; + if(Config.getInstance().getSettings().mfcIgnoreUpscaled) { + return sources.stream() + .filter(src -> src.height != 960) + .collect(Collectors.toList()); + } else { + return sources; + } } private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException { From 00869315fb8d22d4a8a00d6caeb0d1e642ce5354 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 5 Dec 2018 20:59:03 +0100 Subject: [PATCH 134/231] Don't throw exception, if no sync happened yet --- common/src/main/java/ctbrec/recorder/RemoteRecorder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java index 8844d5c8..4056bdc7 100644 --- a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -132,7 +132,7 @@ public class RemoteRecorder implements Recorder { @Override public List getModelsRecording() { - if(lastSync.isBefore(Instant.now().minusSeconds(60))) { + if(!lastSync.equals(Instant.EPOCH) && lastSync.isBefore(Instant.now().minusSeconds(60))) { throw new RuntimeException("Last sync was over a minute ago"); } return models; From 1970f087009da357fa65164098b8b547a2c9e672 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 5 Dec 2018 20:59:51 +0100 Subject: [PATCH 135/231] Return UNKOWN, if state is null --- common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index 8acb7e7e..0c30428d 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -71,7 +71,11 @@ public class MyFreeCamsModel extends AbstractModel { @Override public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { - switch(this.state) { + if(state == null) { + return STATUS.UNKNOWN; + } + + switch(state) { case ONLINE: case RECORDING: return ctbrec.Model.STATUS.ONLINE; From 022997f6b61e25d5a74d7039e7effc32cd4f2312 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 5 Dec 2018 21:00:19 +0100 Subject: [PATCH 136/231] Add new event property OLD --- common/src/main/java/ctbrec/EventBusHolder.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/common/src/main/java/ctbrec/EventBusHolder.java b/common/src/main/java/ctbrec/EventBusHolder.java index d1435407..05e42ee7 100644 --- a/common/src/main/java/ctbrec/EventBusHolder.java +++ b/common/src/main/java/ctbrec/EventBusHolder.java @@ -8,11 +8,26 @@ import com.google.common.eventbus.EventBus; public class EventBusHolder { public static final String EVENT = "event"; + public static final String OLD = "old"; public static final String STATUS = "status"; public static final String MODEL = "model"; public static enum EVENT_TYPE { + /** + * This event is fired every time the OnlineMonitor sees a model online + * It is also fired, if the model was online before. You can see it as a "still online ping". + */ + MODEL_ONLINE, + + /** + * This event is fired whenever the model's online state (Model.STATUS) changes. + */ MODEL_STATUS_CHANGED, + + + /** + * This event is fired whenever the state of a recording changes. + */ RECORDING_STATUS_CHANGED } From 093b36270ae45fdd057f89a23daca9fd5f7965b7 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 5 Dec 2018 21:01:01 +0100 Subject: [PATCH 137/231] Return at least ONLINE / OFFLINE, if the state is UNKNOWN --- .../main/java/ctbrec/sites/bonga/BongaCamsModel.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java b/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java index 2391547c..8414adda 100644 --- a/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java +++ b/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java @@ -1,5 +1,7 @@ package ctbrec.sites.bonga; +import static ctbrec.Model.STATUS.*; + import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -84,9 +86,17 @@ public class BongaCamsModel extends AbstractModel { @Override public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { - return onlineState; + if(failFast) { + return onlineState; + } else { + if(onlineState == UNKNOWN) { + return online ? ONLINE : OFFLINE; + } + return onlineState; + } } + @Override public void setOnlineState(STATUS onlineState) { this.onlineState = onlineState; } From 69544a7a602fdee059effbd36d5e2bbba69fa5ed Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 5 Dec 2018 21:01:33 +0100 Subject: [PATCH 138/231] Pull OnlineMonitor out of LocalRecorder --- .../java/ctbrec/ui/CamrecApplication.java | 11 +- .../java/ctbrec/recorder/LocalRecorder.java | 90 ++++---------- .../java/ctbrec/recorder/OnlineMonitor.java | 117 ++++++++++++++++++ .../ctbrec/recorder/server/HttpServer.java | 7 ++ 4 files changed, 155 insertions(+), 70 deletions(-) create mode 100644 common/src/main/java/ctbrec/recorder/OnlineMonitor.java diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index bbac61ce..7218895a 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -2,6 +2,7 @@ package ctbrec.ui; import static ctbrec.EventBusHolder.*; import static ctbrec.EventBusHolder.EVENT_TYPE.*; +import static ctbrec.Model.STATUS.*; import java.io.BufferedReader; import java.io.File; @@ -32,6 +33,7 @@ import ctbrec.StringUtil; import ctbrec.Version; import ctbrec.io.HttpClient; import ctbrec.recorder.LocalRecorder; +import ctbrec.recorder.OnlineMonitor; import ctbrec.recorder.Recorder; import ctbrec.recorder.RemoteRecorder; import ctbrec.sites.Site; @@ -62,12 +64,14 @@ public class CamrecApplication extends Application { private Stage primaryStage; private Config config; private Recorder recorder; + private OnlineMonitor onlineMonitor; static HostServices hostServices; private SettingsTab settingsTab; private TabPane rootPane = new TabPane(); private List sites = new ArrayList<>(); public static HttpClient httpClient; + @Override public void start(Stage primaryStage) throws Exception { this.primaryStage = primaryStage; @@ -82,6 +86,8 @@ public class CamrecApplication extends Application { createHttpClient(); hostServices = getHostServices(); createRecorder(); + onlineMonitor = new OnlineMonitor(recorder); + onlineMonitor.start(); for (Site site : sites) { if(site.isEnabled()) { try { @@ -165,6 +171,7 @@ public class CamrecApplication extends Application { modelsTab.saveState(); recordingsTab.saveState(); settingsTab.saveConfig(); + onlineMonitor.shutdown(); recorder.shutdown(); for (Site site : sites) { if(site.isEnabled()) { @@ -219,12 +226,12 @@ public class CamrecApplication extends Application { EventBusHolder.BUS.register(new Object() { @Subscribe public void modelEvent(Map e) { - LOG.debug("Alert: {}", e); try { if (Objects.equals(e.get(EVENT), MODEL_STATUS_CHANGED)) { + LOG.debug("Alert: {}", e); Model.STATUS status = (Model.STATUS) e.get(STATUS); Model model = (Model) e.get(MODEL); - if (Objects.equals(Model.STATUS.ONLINE, status)) { + if (status == ONLINE) { Platform.runLater(() -> { String header = "Model Online"; String msg = model.getDisplayName() + " is now online"; diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 1efdb1b1..85309816 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -7,13 +7,11 @@ import static ctbrec.Recording.STATUS.*; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; -import java.net.SocketTimeoutException; import java.nio.file.FileStore; import java.nio.file.Files; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; -import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; @@ -26,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @@ -34,6 +33,7 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.eventbus.Subscribe; import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; @@ -44,7 +44,6 @@ import ctbrec.OS; import ctbrec.Recording; import ctbrec.Recording.STATUS; import ctbrec.io.HttpClient; -import ctbrec.io.HttpException; import ctbrec.io.StreamRedirectThread; import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException; import ctbrec.recorder.download.Download; @@ -62,7 +61,6 @@ public class LocalRecorder implements Recorder { private Map playlistGenerators = new HashMap<>(); private Config config; private ProcessMonitor processMonitor; - private OnlineMonitor onlineMonitor; private PostProcessingTrigger postProcessingTrigger; private volatile boolean recording = true; private List deleteInProgress = Collections.synchronizedList(new ArrayList<>()); @@ -83,19 +81,37 @@ public class LocalRecorder implements Recorder { recording = true; processMonitor = new ProcessMonitor(); processMonitor.start(); - onlineMonitor = new OnlineMonitor(); - onlineMonitor.start(); postProcessingTrigger = new PostProcessingTrigger(); if(Config.isServerMode()) { postProcessingTrigger.start(); } + registerEventBusListener(); + LOG.debug("Recorder initialized"); LOG.info("Models to record: {}", models); LOG.info("Saving recordings in {}", config.getSettings().recordingsDir); } + private void registerEventBusListener() { + EventBusHolder.BUS.register(new Object() { + @Subscribe + public void modelEvent(Map e) { + try { + if (Objects.equals(e.get(EVENT), MODEL_ONLINE)) { + Model model = (Model) e.get(MODEL); + if(!isSuspended(model) && !recordingProcesses.containsKey(model)) { + startRecordingProcess(model); + } + } + } catch (Exception e1) { + LOG.error("Error while handling model state changed event", e); + } + } + }); + } + @Override public void startRecording(Model model) { if (!models.contains(model)) { @@ -288,7 +304,6 @@ public class LocalRecorder implements Recorder { LOG.info("Shutting down"); recording = false; LOG.debug("Stopping monitor threads"); - onlineMonitor.running = false; processMonitor.running = false; postProcessingTrigger.running = false; LOG.debug("Stopping all recording processes"); @@ -424,67 +439,6 @@ public class LocalRecorder implements Recorder { } } - private class OnlineMonitor extends Thread { - private volatile boolean running = false; - - public OnlineMonitor() { - setName("OnlineMonitor"); - setDaemon(true); - } - - @Override - public void run() { - running = true; - while (running) { - Instant begin = Instant.now(); - List models = getModelsRecording(); - for (Model model : models) { - try { - boolean isOnline = model.isOnline(IGNORE_CACHE); - LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline")); - if (isOnline && !isSuspended(model) && !recordingProcesses.containsKey(model)) { - LOG.info("Model {}'s room back to public", model); - startRecordingProcess(model); - } - } catch (HttpException e) { - LOG.error("Couldn't check if model {} is online. HTTP Response: {} - {}", - model.getName(), e.getResponseCode(), e.getResponseMessage()); - } catch (SocketTimeoutException e) { - LOG.error("Couldn't check if model {} is online. Request timed out", model.getName()); - } catch (Exception e) { - LOG.error("Couldn't check if model {} is online", model.getName(), e); - } - } - Instant end = Instant.now(); - Duration timeCheckTook = Duration.between(begin, end); - LOG.trace("Online check for {} models took {} seconds", models.size(), timeCheckTook.getSeconds()); - - long sleepTime = Config.getInstance().getSettings().onlineCheckIntervalInSecs; - if(timeCheckTook.getSeconds() < sleepTime) { - try { - if (running) { - long millis = TimeUnit.SECONDS.toMillis(sleepTime - timeCheckTook.getSeconds()); - LOG.trace("Sleeping {}ms", millis); - Thread.sleep(millis); - } - } catch (InterruptedException e) { - LOG.trace("Sleep interrupted"); - } - } - } - LOG.debug(getName() + " terminated"); - } - } - - private void fireModelOnlineStateChanged(Model model, Model.STATUS status) { - Map evt = new HashMap<>(); - evt.put(EVENT, MODEL_STATUS_CHANGED); - evt.put(STATUS, status); - evt.put(MODEL, model); - EventBusHolder.BUS.post(evt); - LOG.debug("Event fired {}", evt); - } - private void fireRecordingStateChanged(Model model, Recording.STATUS status) { Map evt = new HashMap<>(); evt.put(EVENT, RECORDING_STATUS_CHANGED); diff --git a/common/src/main/java/ctbrec/recorder/OnlineMonitor.java b/common/src/main/java/ctbrec/recorder/OnlineMonitor.java new file mode 100644 index 00000000..5bd96975 --- /dev/null +++ b/common/src/main/java/ctbrec/recorder/OnlineMonitor.java @@ -0,0 +1,117 @@ +package ctbrec.recorder; + +import static ctbrec.EventBusHolder.*; +import static ctbrec.EventBusHolder.EVENT_TYPE.*; +import static ctbrec.Model.STATUS.*; + +import java.net.SocketTimeoutException; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.EventBusHolder; +import ctbrec.Model; +import ctbrec.Model.STATUS; +import ctbrec.io.HttpException; + +public class OnlineMonitor extends Thread { + private static final transient Logger LOG = LoggerFactory.getLogger(OnlineMonitor.class); + private static final boolean IGNORE_CACHE = true; + + private volatile boolean running = false; + private Recorder recorder; + + private Map states = new HashMap<>(); + + public OnlineMonitor(Recorder recorder) { + this.recorder = recorder; + setName("OnlineMonitor"); + setDaemon(true); + } + + @Override + public void run() { + running = true; + while (running) { + Instant begin = Instant.now(); + List models = recorder.getModelsRecording(); + + // remove models, which are not recorded anymore + for (Iterator iterator = states.keySet().iterator(); iterator.hasNext();) { + Model model = iterator.next(); + if(!models.contains(model)) { + iterator.remove(); + } + } + + // update the currently recorded models + for (Model model : models) { + try { + if(model.isOnline(IGNORE_CACHE)) { + fireModelOnline(model); + } + STATUS state = model.getOnlineState(false); + STATUS oldState = states.getOrDefault(model, UNKNOWN); + states.put(model, state); + if(state != oldState) { + fireModelOnlineStateChanged(model, oldState, state); + } + } catch (HttpException e) { + LOG.error("Couldn't check if model {} is online. HTTP Response: {} - {}", + model.getName(), e.getResponseCode(), e.getResponseMessage()); + } catch (SocketTimeoutException e) { + LOG.error("Couldn't check if model {} is online. Request timed out", model.getName()); + } catch (Exception e) { + LOG.error("Couldn't check if model {} is online", model.getName(), e); + } + } + Instant end = Instant.now(); + Duration timeCheckTook = Duration.between(begin, end); + LOG.trace("Online check for {} models took {} seconds", models.size(), timeCheckTook.getSeconds()); + + long sleepTime = Config.getInstance().getSettings().onlineCheckIntervalInSecs; + if(timeCheckTook.getSeconds() < sleepTime) { + try { + if (running) { + long millis = TimeUnit.SECONDS.toMillis(sleepTime - timeCheckTook.getSeconds()); + LOG.trace("Sleeping {}ms", millis); + Thread.sleep(millis); + } + } catch (InterruptedException e) { + LOG.trace("Sleep interrupted"); + } + } + } + LOG.debug(getName() + " terminated"); + } + + private void fireModelOnline(Model model) { + Map evt = new HashMap<>(); + evt.put(EVENT, MODEL_ONLINE); + evt.put(MODEL, model); + EventBusHolder.BUS.post(evt); + } + + private void fireModelOnlineStateChanged(Model model, STATUS oldStatus, STATUS newStatus) { + Map evt = new HashMap<>(); + evt.put(EVENT, MODEL_STATUS_CHANGED); + evt.put(STATUS, newStatus); + evt.put(OLD, oldStatus); + evt.put(MODEL, model); + EventBusHolder.BUS.post(evt); + LOG.debug("Event fired {}", evt); + } + + public void shutdown() { + running = false; + interrupt(); + } +} \ No newline at end of file diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index 5767b72e..ab07dabc 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.recorder.LocalRecorder; +import ctbrec.recorder.OnlineMonitor; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; import ctbrec.sites.bonga.BongaCams; @@ -30,6 +31,7 @@ public class HttpServer { private static final transient Logger LOG = LoggerFactory.getLogger(HttpServer.class); private Recorder recorder; + private OnlineMonitor onlineMonitor; private Config config; private Server server = new Server(); private List sites = new ArrayList<>(); @@ -54,6 +56,8 @@ public class HttpServer { LOG.info("HMAC authentication is enabled"); } recorder = new LocalRecorder(config); + OnlineMonitor monitor = new OnlineMonitor(recorder); + monitor.start(); for (Site site : sites) { if(site.isEnabled()) { site.init(); @@ -75,6 +79,9 @@ public class HttpServer { @Override public void run() { LOG.info("Shutting down"); + if(onlineMonitor != null) { + onlineMonitor.shutdown(); + } if(recorder != null) { recorder.shutdown(); } From 90e033d2acfe959ed3fc5a55a00b6a035ccb81ba Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 6 Dec 2018 13:44:10 +0100 Subject: [PATCH 139/231] Don't log exception if interrupted, but not running anymore --- common/src/main/java/ctbrec/recorder/OnlineMonitor.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/recorder/OnlineMonitor.java b/common/src/main/java/ctbrec/recorder/OnlineMonitor.java index 5bd96975..5ec98567 100644 --- a/common/src/main/java/ctbrec/recorder/OnlineMonitor.java +++ b/common/src/main/java/ctbrec/recorder/OnlineMonitor.java @@ -4,6 +4,7 @@ import static ctbrec.EventBusHolder.*; import static ctbrec.EventBusHolder.EVENT_TYPE.*; import static ctbrec.Model.STATUS.*; +import java.io.InterruptedIOException; import java.net.SocketTimeoutException; import java.time.Duration; import java.time.Instant; @@ -69,6 +70,10 @@ public class OnlineMonitor extends Thread { model.getName(), e.getResponseCode(), e.getResponseMessage()); } catch (SocketTimeoutException e) { LOG.error("Couldn't check if model {} is online. Request timed out", model.getName()); + } catch (InterruptedException | InterruptedIOException e) { + if(running) { + LOG.error("Couldn't check if model {} is online", model.getName(), e); + } } catch (Exception e) { LOG.error("Couldn't check if model {} is online", model.getName(), e); } @@ -107,7 +112,6 @@ public class OnlineMonitor extends Thread { evt.put(OLD, oldStatus); evt.put(MODEL, model); EventBusHolder.BUS.post(evt); - LOG.debug("Event fired {}", evt); } public void shutdown() { From b100f107d4299ef865cd9029eb418d97b25f9b5c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 6 Dec 2018 14:16:10 +0100 Subject: [PATCH 140/231] Move site activation checkbox to their config ui --- .../src/main/java/ctbrec/ui/SettingsTab.java | 39 ++----------------- .../ui/sites/bonga/BongaCamsConfigUI.java | 38 +++++++++++++----- .../ctbrec/ui/sites/cam4/Cam4ConfigUI.java | 36 ++++++++++++++--- .../java/ctbrec/ui/sites/cam4/Cam4SiteUi.java | 2 +- .../ui/sites/camsoda/CamsodaConfigUI.java | 30 +++++++++++--- .../sites/chaturbate/ChaturbateConfigUi.java | 39 +++++++++++++++---- .../ui/sites/chaturbate/ChaturbateSiteUi.java | 2 +- .../sites/myfreecams/MyFreeCamsConfigUI.java | 18 +++++++++ 8 files changed, 141 insertions(+), 63 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/SettingsTab.java index a5182481..7d56a308 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/SettingsTab.java @@ -13,7 +13,6 @@ import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.Hmac; -import ctbrec.Settings; import ctbrec.Settings.DirectoryStructure; import ctbrec.StringUtil; import ctbrec.sites.ConfigUI; @@ -83,7 +82,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private ComboBox startTab; private List sites; private Label restartLabel; - private Accordion credentialsAccordion = new Accordion(); + private Accordion siteConfigAccordion = new Accordion(); public SettingsTab(List sites) { this.sites = sites; @@ -127,8 +126,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { leftSide.getChildren().add(createRecordLocationPanel()); //right side - rightSide.getChildren().add(createSiteSelectionPanel()); - rightSide.getChildren().add(credentialsAccordion); + rightSide.getChildren().add(siteConfigAccordion); proxySettingsPane = new ProxySettingsPane(this); rightSide.getChildren().add(proxySettingsPane); for (int i = 0; i < sites.size(); i++) { @@ -136,39 +134,10 @@ public class SettingsTab extends Tab implements TabSelectionListener { ConfigUI siteConfig = SiteUiFactory.getUi(site).getConfigUI(); if(siteConfig != null) { TitledPane pane = new TitledPane(site.getName(), siteConfig.createConfigPanel()); - credentialsAccordion.getPanes().add(pane); + siteConfigAccordion.getPanes().add(pane); } } - credentialsAccordion.setExpandedPane(credentialsAccordion.getPanes().get(0)); - } - - private Node createSiteSelectionPanel() { - Settings settings = Config.getInstance().getSettings(); - GridPane layout = createGridLayout(); - - int row = 0; - for (Site site : sites) { - Label l = new Label(site.getName()); - 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()); - } - saveConfig(); - showRestartRequired(); - }); - GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); - GridPane.setMargin(enabled, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - layout.add(enabled, 1, row++); - } - - TitledPane siteSelection = new TitledPane("Enabled Sites", layout); - siteSelection.setCollapsible(false); - return siteSelection; + siteConfigAccordion.setExpandedPane(siteConfigAccordion.getPanes().get(0)); } private Node createRecordLocationPanel() { diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java index ce8381dd..9597977f 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java @@ -1,6 +1,7 @@ package ctbrec.ui.sites.bonga; import ctbrec.Config; +import ctbrec.Settings; import ctbrec.sites.bonga.BongaCams; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.SettingsTab; @@ -8,6 +9,7 @@ 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; @@ -24,32 +26,50 @@ public class BongaCamsConfigUI extends AbstractConfigUI { @Override public Parent createConfigPanel() { GridPane layout = SettingsTab.createGridLayout(); - layout.add(new Label("BongaCams User"), 0, 0); - TextField username = new TextField(Config.getInstance().getSettings().bongaUsername); + 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(bongaCams.getName())); + enabled.setOnAction((e) -> { + if(enabled.isSelected()) { + settings.disabledSites.remove(bongaCams.getName()); + } else { + settings.disabledSites.add(bongaCams.getName()); + } + save(); + }); + GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + layout.add(enabled, 1, row++); + + layout.add(new Label("BongaCams User"), 0, row); + TextField username = new TextField(settings.bongaUsername); username.textProperty().addListener((ob, o, n) -> { - Config.getInstance().getSettings().bongaUsername = username.getText(); + settings.bongaUsername = username.getText(); save(); }); GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); GridPane.setColumnSpan(username, 2); - layout.add(username, 1, 0); + layout.add(username, 1, row++); - layout.add(new Label("BongaCams Password"), 0, 1); + layout.add(new Label("BongaCams Password"), 0, row); PasswordField password = new PasswordField(); - password.setText(Config.getInstance().getSettings().bongaPassword); + password.setText(settings.bongaPassword); password.focusedProperty().addListener((e) -> { - Config.getInstance().getSettings().bongaPassword = password.getText(); + settings.bongaPassword = password.getText(); save(); }); GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); GridPane.setColumnSpan(password, 2); - layout.add(password, 1, 1); + layout.add(password, 1, row++); Button createAccount = new Button("Create new Account"); createAccount.setOnAction((e) -> DesktopIntegration.open(bongaCams.getAffiliateLink())); - layout.add(createAccount, 1, 2); + 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)); diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java index bacea351..bd61bc90 100644 --- a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java @@ -1,6 +1,7 @@ package ctbrec.ui.sites.cam4; import ctbrec.Config; +import ctbrec.Settings; import ctbrec.sites.cam4.Cam4; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.SettingsTab; @@ -8,6 +9,7 @@ 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; @@ -15,10 +17,34 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; public class Cam4ConfigUI extends AbstractConfigUI { + private Cam4 cam4; + + public Cam4ConfigUI(Cam4 cam4) { + this.cam4 = cam4; + } + @Override public Parent createConfigPanel() { GridPane layout = SettingsTab.createGridLayout(); - layout.add(new Label("Cam4 User"), 0, 0); + 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(cam4.getName())); + enabled.setOnAction((e) -> { + if(enabled.isSelected()) { + settings.disabledSites.remove(cam4.getName()); + } else { + settings.disabledSites.add(cam4.getName()); + } + save(); + }); + GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + layout.add(enabled, 1, row++); + + layout.add(new Label("Cam4 User"), 0, row); TextField username = new TextField(Config.getInstance().getSettings().cam4Username); username.textProperty().addListener((ob, o, n) -> { Config.getInstance().getSettings().cam4Username = username.getText(); @@ -27,9 +53,9 @@ public class Cam4ConfigUI extends AbstractConfigUI { GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); GridPane.setColumnSpan(username, 2); - layout.add(username, 1, 0); + layout.add(username, 1, row++); - layout.add(new Label("Cam4 Password"), 0, 1); + layout.add(new Label("Cam4 Password"), 0, row); PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().cam4Password); password.focusedProperty().addListener((e) -> { @@ -39,11 +65,11 @@ public class Cam4ConfigUI extends AbstractConfigUI { GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); GridPane.setColumnSpan(password, 2); - layout.add(password, 1, 1); + layout.add(password, 1, row++); Button createAccount = new Button("Create new Account"); createAccount.setOnAction((e) -> DesktopIntegration.open(Cam4.AFFILIATE_LINK)); - layout.add(createAccount, 1, 2); + 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)); diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java index d0c5d2b5..66c4d776 100644 --- a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java @@ -30,7 +30,7 @@ public class Cam4SiteUi implements SiteUI { public Cam4SiteUi(Cam4 cam4) { this.cam4 = cam4; tabProvider = new Cam4TabProvider(cam4); - configUI = new Cam4ConfigUI(); + configUI = new Cam4ConfigUI(cam4); } @Override diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java index b19abc95..7cb3e964 100644 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java @@ -1,6 +1,7 @@ package ctbrec.ui.sites.camsoda; import ctbrec.Config; +import ctbrec.Settings; import ctbrec.sites.camsoda.Camsoda; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.SettingsTab; @@ -8,6 +9,7 @@ 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; @@ -24,7 +26,25 @@ public class CamsodaConfigUI extends AbstractConfigUI { @Override public Parent createConfigPanel() { GridPane layout = SettingsTab.createGridLayout(); - layout.add(new Label("CamSoda User"), 0, 0); + 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(camsoda.getName())); + enabled.setOnAction((e) -> { + if(enabled.isSelected()) { + settings.disabledSites.remove(camsoda.getName()); + } else { + settings.disabledSites.add(camsoda.getName()); + } + save(); + }); + GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + layout.add(enabled, 1, row++); + + layout.add(new Label("CamSoda User"), 0, row); TextField username = new TextField(Config.getInstance().getSettings().camsodaUsername); username.textProperty().addListener((ob, o, n) -> { Config.getInstance().getSettings().camsodaUsername = username.getText(); @@ -33,9 +53,9 @@ public class CamsodaConfigUI extends AbstractConfigUI { GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); GridPane.setColumnSpan(username, 2); - layout.add(username, 1, 0); + layout.add(username, 1, row++); - layout.add(new Label("CamSoda Password"), 0, 1); + layout.add(new Label("CamSoda Password"), 0, row); PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().camsodaPassword); password.textProperty().addListener((ob, o, n) -> { @@ -45,11 +65,11 @@ public class CamsodaConfigUI extends AbstractConfigUI { GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); GridPane.setColumnSpan(password, 2); - layout.add(password, 1, 1); + layout.add(password, 1, row++); Button createAccount = new Button("Create new Account"); createAccount.setOnAction((e) -> DesktopIntegration.open(camsoda.getAffiliateLink())); - layout.add(createAccount, 1, 2); + 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)); diff --git a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java index 706ff33c..f1e1cf55 100644 --- a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java +++ b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java @@ -1,6 +1,7 @@ package ctbrec.ui.sites.chaturbate; import ctbrec.Config; +import ctbrec.Settings; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.SettingsTab; @@ -8,6 +9,7 @@ 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; @@ -15,11 +17,34 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; public class ChaturbateConfigUi extends AbstractConfigUI { + private Chaturbate chaturbate; + + public ChaturbateConfigUi(Chaturbate chaturbate) { + this.chaturbate = chaturbate; + } + @Override public Parent createConfigPanel() { + Settings settings = Config.getInstance().getSettings(); GridPane layout = SettingsTab.createGridLayout(); - layout.add(new Label("Chaturbate User"), 0, 0); + int row = 0; + Label l = new Label("Active"); + layout.add(l, 0, row); + CheckBox enabled = new CheckBox(); + enabled.setSelected(!settings.disabledSites.contains(chaturbate.getName())); + enabled.setOnAction((e) -> { + if(enabled.isSelected()) { + settings.disabledSites.remove(chaturbate.getName()); + } else { + settings.disabledSites.add(chaturbate.getName()); + } + save(); + }); + GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + layout.add(enabled, 1, row++); + + layout.add(new Label("Chaturbate User"), 0, row); TextField username = new TextField(Config.getInstance().getSettings().username); username.textProperty().addListener((ob, o, n) -> { Config.getInstance().getSettings().username = username.getText(); @@ -28,9 +53,9 @@ public class ChaturbateConfigUi extends AbstractConfigUI { GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); GridPane.setColumnSpan(username, 2); - layout.add(username, 1, 0); + layout.add(username, 1, row++); - layout.add(new Label("Chaturbate Password"), 0, 1); + layout.add(new Label("Chaturbate Password"), 0, row); PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().password); password.textProperty().addListener((ob, o, n) -> { @@ -40,9 +65,9 @@ public class ChaturbateConfigUi extends AbstractConfigUI { GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); GridPane.setColumnSpan(password, 2); - layout.add(password, 1, 1); + layout.add(password, 1, row++); - layout.add(new Label("Chaturbate Base URL"), 0, 2); + layout.add(new Label("Chaturbate Base URL"), 0, row); TextField baseUrl = new TextField(); baseUrl.setText(Config.getInstance().getSettings().chaturbateBaseUrl); baseUrl.textProperty().addListener((ob, o, n) -> { @@ -52,11 +77,11 @@ public class ChaturbateConfigUi extends AbstractConfigUI { GridPane.setFillWidth(baseUrl, true); GridPane.setHgrow(baseUrl, Priority.ALWAYS); GridPane.setColumnSpan(baseUrl, 2); - layout.add(baseUrl, 1, 2); + layout.add(baseUrl, 1, row++); Button createAccount = new Button("Create new Account"); createAccount.setOnAction((e) -> DesktopIntegration.open(Chaturbate.REGISTRATION_LINK)); - layout.add(createAccount, 1, 3); + 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)); diff --git a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateSiteUi.java b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateSiteUi.java index 7765e717..fdca9ec5 100644 --- a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateSiteUi.java @@ -16,7 +16,7 @@ public class ChaturbateSiteUi implements SiteUI { public ChaturbateSiteUi(Chaturbate chaturbate) { this.chaturbate = chaturbate; tabProvider = new ChaturbateTabProvider(chaturbate); - configUi = new ChaturbateConfigUi(); + configUi = new ChaturbateConfigUi(chaturbate); } @Override diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java index 0783a68a..67d30a07 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java @@ -1,6 +1,7 @@ package ctbrec.ui.sites.myfreecams; import ctbrec.Config; +import ctbrec.Settings; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.SettingsTab; @@ -26,6 +27,23 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { public Parent createConfigPanel() { int row = 0; GridPane layout = SettingsTab.createGridLayout(); + Settings settings = Config.getInstance().getSettings(); + + Label l = new Label("Active"); + layout.add(l, 0, row); + CheckBox enabled = new CheckBox(); + enabled.setSelected(!settings.disabledSites.contains(myFreeCams.getName())); + enabled.setOnAction((e) -> { + if(enabled.isSelected()) { + settings.disabledSites.remove(myFreeCams.getName()); + } else { + settings.disabledSites.add(myFreeCams.getName()); + } + save(); + }); + GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + layout.add(enabled, 1, row++); + layout.add(new Label("MyFreeCams User"), 0, row); TextField username = new TextField(Config.getInstance().getSettings().mfcUsername); username.setPrefWidth(300); From b50df194a009a995e5fdacb738e30a3a7a3c5a52 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 6 Dec 2018 17:36:13 +0100 Subject: [PATCH 141/231] Move settings to own package --- .../ctbrec/ui/{ => settings}/ColorSettingsPane.css | 0 .../ctbrec/ui/{ => settings}/ColorSettingsPane.java | 2 +- .../ctbrec/ui/{ => settings}/ProxySettingsPane.java | 2 +- .../java/ctbrec/ui/{ => settings}/SettingsTab.java | 11 +++++------ .../java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java | 2 +- .../main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java | 2 +- .../java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java | 2 +- .../ui/sites/chaturbate/ChaturbateConfigUi.java | 2 +- .../ui/sites/myfreecams/MyFreeCamsConfigUI.java | 2 +- .../main/java/ctbrec/{ => event}/EventBusHolder.java | 0 10 files changed, 12 insertions(+), 13 deletions(-) rename client/src/main/java/ctbrec/ui/{ => settings}/ColorSettingsPane.css (100%) rename client/src/main/java/ctbrec/ui/{ => settings}/ColorSettingsPane.java (99%) rename client/src/main/java/ctbrec/ui/{ => settings}/ProxySettingsPane.java (99%) rename client/src/main/java/ctbrec/ui/{ => settings}/SettingsTab.java (99%) rename common/src/main/java/ctbrec/{ => event}/EventBusHolder.java (100%) diff --git a/client/src/main/java/ctbrec/ui/ColorSettingsPane.css b/client/src/main/java/ctbrec/ui/settings/ColorSettingsPane.css similarity index 100% rename from client/src/main/java/ctbrec/ui/ColorSettingsPane.css rename to client/src/main/java/ctbrec/ui/settings/ColorSettingsPane.css diff --git a/client/src/main/java/ctbrec/ui/ColorSettingsPane.java b/client/src/main/java/ctbrec/ui/settings/ColorSettingsPane.java similarity index 99% rename from client/src/main/java/ctbrec/ui/ColorSettingsPane.java rename to client/src/main/java/ctbrec/ui/settings/ColorSettingsPane.java index be6feb4e..fd3ac43e 100644 --- a/client/src/main/java/ctbrec/ui/ColorSettingsPane.java +++ b/client/src/main/java/ctbrec/ui/settings/ColorSettingsPane.java @@ -1,4 +1,4 @@ -package ctbrec.ui; +package ctbrec.ui.settings; import ctbrec.Config; import javafx.scene.control.Button; diff --git a/client/src/main/java/ctbrec/ui/ProxySettingsPane.java b/client/src/main/java/ctbrec/ui/settings/ProxySettingsPane.java similarity index 99% rename from client/src/main/java/ctbrec/ui/ProxySettingsPane.java rename to client/src/main/java/ctbrec/ui/settings/ProxySettingsPane.java index 1d2ff789..c7de4c8e 100644 --- a/client/src/main/java/ctbrec/ui/ProxySettingsPane.java +++ b/client/src/main/java/ctbrec/ui/settings/ProxySettingsPane.java @@ -1,4 +1,4 @@ -package ctbrec.ui; +package ctbrec.ui.settings; import static ctbrec.Settings.ProxyType.*; import java.util.ArrayList; diff --git a/client/src/main/java/ctbrec/ui/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java similarity index 99% rename from client/src/main/java/ctbrec/ui/SettingsTab.java rename to client/src/main/java/ctbrec/ui/settings/SettingsTab.java index 7d56a308..0115f0a4 100644 --- a/client/src/main/java/ctbrec/ui/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -1,4 +1,4 @@ -package ctbrec.ui; +package ctbrec.ui.settings; import static ctbrec.Settings.DirectoryStructure.*; @@ -17,6 +17,9 @@ import ctbrec.Settings.DirectoryStructure; import ctbrec.StringUtil; import ctbrec.sites.ConfigUI; import ctbrec.sites.Site; +import ctbrec.ui.AutosizeAlert; +import ctbrec.ui.SiteUiFactory; +import ctbrec.ui.TabSelectionListener; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; @@ -127,6 +130,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { //right side rightSide.getChildren().add(siteConfigAccordion); + rightSide.getChildren().add(new ActionSettingsPanel(this)); proxySettingsPane = new ProxySettingsPane(this); rightSide.getChildren().add(proxySettingsPane); for (int i = 0; i < sites.size(); i++) { @@ -390,7 +394,6 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(showPlayerStarting, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(showPlayerStarting, 1, row++); - l = new Label("Display stream resolution in overview"); layout.add(l, 0, row); loadResolution = new CheckBox(); @@ -398,15 +401,11 @@ public class SettingsTab extends Tab implements TabSelectionListener { loadResolution.setOnAction((e) -> { Config.getInstance().getSettings().determineResolution = loadResolution.isSelected(); saveConfig(); - if(!loadResolution.isSelected()) { - ThumbOverviewTab.queue.clear(); - } }); GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); GridPane.setMargin(loadResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(loadResolution, 1, row++); - l = new Label("Manually select stream quality"); layout.add(l, 0, row); chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality); diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java index 9597977f..0755f024 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java @@ -4,7 +4,7 @@ import ctbrec.Config; import ctbrec.Settings; import ctbrec.sites.bonga.BongaCams; import ctbrec.ui.DesktopIntegration; -import ctbrec.ui.SettingsTab; +import ctbrec.ui.settings.SettingsTab; import ctbrec.ui.sites.AbstractConfigUI; import javafx.geometry.Insets; import javafx.scene.Parent; diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java index bd61bc90..46ce3490 100644 --- a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java @@ -4,7 +4,7 @@ import ctbrec.Config; import ctbrec.Settings; import ctbrec.sites.cam4.Cam4; import ctbrec.ui.DesktopIntegration; -import ctbrec.ui.SettingsTab; +import ctbrec.ui.settings.SettingsTab; import ctbrec.ui.sites.AbstractConfigUI; import javafx.geometry.Insets; import javafx.scene.Parent; diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java index 7cb3e964..197108dc 100644 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java @@ -4,7 +4,7 @@ import ctbrec.Config; import ctbrec.Settings; import ctbrec.sites.camsoda.Camsoda; import ctbrec.ui.DesktopIntegration; -import ctbrec.ui.SettingsTab; +import ctbrec.ui.settings.SettingsTab; import ctbrec.ui.sites.AbstractConfigUI; import javafx.geometry.Insets; import javafx.scene.Parent; diff --git a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java index f1e1cf55..8133bf90 100644 --- a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java +++ b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java @@ -4,7 +4,7 @@ import ctbrec.Config; import ctbrec.Settings; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.ui.DesktopIntegration; -import ctbrec.ui.SettingsTab; +import ctbrec.ui.settings.SettingsTab; import ctbrec.ui.sites.AbstractConfigUI; import javafx.geometry.Insets; import javafx.scene.Parent; diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java index 67d30a07..cf258844 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java @@ -4,7 +4,7 @@ import ctbrec.Config; import ctbrec.Settings; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.ui.DesktopIntegration; -import ctbrec.ui.SettingsTab; +import ctbrec.ui.settings.SettingsTab; import ctbrec.ui.sites.AbstractConfigUI; import javafx.geometry.Insets; import javafx.scene.Parent; diff --git a/common/src/main/java/ctbrec/EventBusHolder.java b/common/src/main/java/ctbrec/event/EventBusHolder.java similarity index 100% rename from common/src/main/java/ctbrec/EventBusHolder.java rename to common/src/main/java/ctbrec/event/EventBusHolder.java From 2dc5fd4581d69f82749481077b98511495f41e74 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 6 Dec 2018 17:39:33 +0100 Subject: [PATCH 142/231] Add Event and EventReaction classes --- .../java/ctbrec/ui/CamrecApplication.java | 82 ++++++++++--------- .../src/main/java/ctbrec/ui/JavaFxModel.java | 2 +- .../main/java/ctbrec/ui/JavaFxRecording.java | 6 +- .../java/ctbrec/ui/RecordedModelsTab.java | 2 +- .../main/java/ctbrec/ui/RecordingsTab.java | 25 +++--- .../main/java/ctbrec/ui/ThumbOverviewTab.java | 2 +- .../src/main/java/ctbrec/ui/TokenLabel.java | 2 +- .../ui/settings/ActionSettingsPanel.java | 30 +++++++ .../sites/bonga/BongaCamsUpdateService.java | 2 +- .../camsoda/CamsodaFollowedUpdateService.java | 2 +- .../src/main/java/ctbrec/AbstractModel.java | 6 +- common/src/main/java/ctbrec/Model.java | 6 +- common/src/main/java/ctbrec/Recording.java | 8 +- .../java/ctbrec/event/AbstractModelEvent.java | 16 ++++ common/src/main/java/ctbrec/event/Event.java | 28 +++++++ .../java/ctbrec/event/EventBusHolder.java | 21 +---- .../main/java/ctbrec/event/EventReaction.java | 32 ++++++++ .../main/java/ctbrec/event/LogReaction.java | 15 ++++ .../java/ctbrec/event/ModelIsOnlineEvent.java | 36 ++++++++ .../ctbrec/event/ModelStateChangedEvent.java | 59 +++++++++++++ .../event/RecordingStateChangedEvent.java | 55 +++++++++++++ .../main/java/ctbrec/event/TypePredicate.java | 23 ++++++ .../java/ctbrec/io/StreamRedirectThread.java | 2 +- .../java/ctbrec/recorder/LocalRecorder.java | 35 ++++---- .../java/ctbrec/recorder/OnlineMonitor.java | 35 ++------ .../java/ctbrec/recorder/RemoteRecorder.java | 2 +- .../ctbrec/recorder/download/HlsDownload.java | 8 ++ .../recorder/download/MergedHlsDownload.java | 8 ++ .../ctbrec/sites/bonga/BongaCamsModel.java | 6 +- .../java/ctbrec/sites/cam4/Cam4Model.java | 4 +- .../ctbrec/sites/camsoda/CamsodaModel.java | 4 +- .../sites/chaturbate/ChaturbateModel.java | 10 +-- .../ctbrec/sites/mfc/MyFreeCamsClient.java | 2 +- .../ctbrec/sites/mfc/MyFreeCamsModel.java | 26 +++--- 34 files changed, 440 insertions(+), 162 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java create mode 100644 common/src/main/java/ctbrec/event/AbstractModelEvent.java create mode 100644 common/src/main/java/ctbrec/event/Event.java create mode 100644 common/src/main/java/ctbrec/event/EventReaction.java create mode 100644 common/src/main/java/ctbrec/event/LogReaction.java create mode 100644 common/src/main/java/ctbrec/event/ModelIsOnlineEvent.java create mode 100644 common/src/main/java/ctbrec/event/ModelStateChangedEvent.java create mode 100644 common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java create mode 100644 common/src/main/java/ctbrec/event/TypePredicate.java diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 7218895a..70bbf4bd 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -1,8 +1,8 @@ package ctbrec.ui; -import static ctbrec.EventBusHolder.*; -import static ctbrec.EventBusHolder.EVENT_TYPE.*; -import static ctbrec.Model.STATUS.*; + +import static ctbrec.Model.State.*; +import static ctbrec.event.Event.Type.*; import java.io.BufferedReader; import java.io.File; @@ -14,7 +14,6 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Objects; import org.slf4j.Logger; @@ -26,11 +25,14 @@ import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import ctbrec.Config; -import ctbrec.EventBusHolder; import ctbrec.Model; import ctbrec.OS; import ctbrec.StringUtil; import ctbrec.Version; +import ctbrec.event.Event; +import ctbrec.event.EventBusHolder; +import ctbrec.event.LogReaction; +import ctbrec.event.ModelStateChangedEvent; import ctbrec.io.HttpClient; import ctbrec.recorder.LocalRecorder; import ctbrec.recorder.OnlineMonitor; @@ -42,6 +44,7 @@ import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.mfc.MyFreeCams; +import ctbrec.ui.settings.SettingsTab; import javafx.application.Application; import javafx.application.HostServices; import javafx.application.Platform; @@ -143,7 +146,7 @@ public class CamrecApplication extends Application { loadStyleSheet(primaryStage, "color.css"); } loadStyleSheet(primaryStage, "style.css"); - primaryStage.getScene().getStylesheets().add("/ctbrec/ui/ColorSettingsPane.css"); + primaryStage.getScene().getStylesheets().add("/ctbrec/ui/settings/ColorSettingsPane.css"); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/ThumbCell.css"); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/SearchBox.css"); primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/Popover.css"); @@ -212,40 +215,43 @@ public class CamrecApplication extends Application { } private void registerAlertSystem() { - new Thread(() -> { - // try { - // // don't register before 1 minute has passed, because directly after - // // the start of ctbrec, an event for every online model would be fired, - // // which is annoying as f - // Thread.sleep(TimeUnit.MINUTES.toMillis(1)); - // } catch (InterruptedException e) { - // e.printStackTrace(); - // } - LOG.debug("Alert System registered"); - Platform.runLater(() -> { - EventBusHolder.BUS.register(new Object() { - @Subscribe - public void modelEvent(Map e) { - try { - if (Objects.equals(e.get(EVENT), MODEL_STATUS_CHANGED)) { - LOG.debug("Alert: {}", e); - Model.STATUS status = (Model.STATUS) e.get(STATUS); - Model model = (Model) e.get(MODEL); - if (status == ONLINE) { - Platform.runLater(() -> { - String header = "Model Online"; - String msg = model.getDisplayName() + " is now online"; - OS.notification(primaryStage.getTitle(), header, msg); - }); - } - } - } catch (Exception e1) { - e1.printStackTrace(); + // try { + // // don't register before 1 minute has passed, because directly after + // // the start of ctbrec, an event for every online model would be fired, + // // which is annoying as f + // Thread.sleep(TimeUnit.MINUTES.toMillis(1)); + // } catch (InterruptedException e) { + // e.printStackTrace(); + // } + EventBusHolder.BUS.register(new Object() { + @Subscribe + public void modelEvent(Event e) { + try { + if (e.getType() == MODEL_STATUS_CHANGED) { + ModelStateChangedEvent evt = (ModelStateChangedEvent) e; + Model model = evt.getModel(); + if (evt.getNewState() == ONLINE) { + String header = "Model Online"; + String msg = model.getDisplayName() + " is now online"; + OS.notification(primaryStage.getTitle(), header, msg); } } - }); - }); - }).start(); + } catch (Exception e1) { + LOG.error("Couldn't show notification", e1); + } + } + }); + + EventBusHolder.BUS.register(new Object() { + LogReaction reaction = new LogReaction(); + @Subscribe + public void modelEvent(Event e) { + reaction.reactToEvent(e); + } + }); + + + LOG.debug("Alert System registered"); } private void writeColorSchemeStyleSheet(Stage primaryStage) { diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java index e57cc45d..63193d69 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java @@ -110,7 +110,7 @@ public class JavaFxModel implements Model { } @Override - public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { + public State getOnlineState(boolean failFast) throws IOException, ExecutionException { return delegate.getOnlineState(failFast); } diff --git a/client/src/main/java/ctbrec/ui/JavaFxRecording.java b/client/src/main/java/ctbrec/ui/JavaFxRecording.java index 74926de8..e737ce8e 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxRecording.java +++ b/client/src/main/java/ctbrec/ui/JavaFxRecording.java @@ -43,7 +43,7 @@ public class JavaFxRecording extends Recording { } @Override - public STATUS getStatus() { + public State getStatus() { return delegate.getStatus(); } @@ -52,7 +52,7 @@ public class JavaFxRecording extends Recording { } @Override - public void setStatus(STATUS status) { + public void setStatus(State status) { delegate.setStatus(status); switch(status) { case RECORDING: @@ -121,7 +121,7 @@ public class JavaFxRecording extends Recording { public void update(Recording updated) { if(!Config.getInstance().getSettings().localRecording) { - if(getStatus() == STATUS.DOWNLOADING && updated.getStatus() != STATUS.DOWNLOADING) { + if(getStatus() == State.DOWNLOADING && updated.getStatus() != State.DOWNLOADING) { // ignore, because the the status coming from the server is FINISHED and we are // overriding it with DOWNLOADING return; diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index 878c8d00..c4276e8a 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -369,7 +369,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { .map(m -> new JavaFxModel(m)) .peek(fxm -> { for (Recording recording : recordings) { - if(recording.getStatus() == Recording.STATUS.RECORDING && + if(recording.getStatus() == Recording.State.RECORDING && recording.getModelName().equals(fxm.getName())) { fxm.getRecordingProperty().set(true); diff --git a/client/src/main/java/ctbrec/ui/RecordingsTab.java b/client/src/main/java/ctbrec/ui/RecordingsTab.java index da238bc3..46bfb00f 100644 --- a/client/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordingsTab.java @@ -1,5 +1,6 @@ package ctbrec.ui; +import static ctbrec.Recording.State.*; import static javafx.scene.control.ButtonType.*; import java.io.File; @@ -34,7 +35,7 @@ import com.iheartradio.m3u8.PlaylistException; import ctbrec.Config; import ctbrec.Recording; -import ctbrec.Recording.STATUS; +import ctbrec.Recording.State; import ctbrec.StringUtil; import ctbrec.recorder.Recorder; import ctbrec.recorder.download.MergedHlsDownload; @@ -172,7 +173,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) { int row = this.getTableRow().getIndex(); JavaFxRecording rec = tableViewProperty().get().getItems().get(row); - if(!rec.valueChanged() && rec.getStatus() == STATUS.RECORDING) { + if(!rec.valueChanged() && rec.getStatus() == State.RECORDING) { setStyle("-fx-alignment: CENTER-RIGHT; -fx-background-color: red"); } } @@ -212,11 +213,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener { List recordings = table.getSelectionModel().getSelectedItems(); if (recordings != null && !recordings.isEmpty()) { if (event.getCode() == KeyCode.DELETE) { - if(recordings.size() > 1 || recordings.get(0).getStatus() == STATUS.FINISHED) { + if(recordings.size() > 1 || recordings.get(0).getStatus() == State.FINISHED) { delete(recordings); } } else if (event.getCode() == KeyCode.ENTER) { - if(recordings.get(0).getStatus() == STATUS.FINISHED) { + if(recordings.get(0).getStatus() == State.FINISHED) { play(recordings.get(0)); } } @@ -376,7 +377,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { openInPlayer.setOnAction((e) -> { play(recordings.get(0)); }); - if(recordings.get(0).getStatus() == STATUS.FINISHED || Config.getInstance().getSettings().localRecording) { + if(recordings.get(0).getStatus() == State.FINISHED || Config.getInstance().getSettings().localRecording) { contextMenu.getItems().add(openInPlayer); } @@ -398,7 +399,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { deleteRecording.setOnAction((e) -> { delete(recordings); }); - if(recordings.get(0).getStatus() == STATUS.FINISHED || recordings.size() > 1) { + if(recordings.get(0).getStatus() == State.FINISHED || recordings.size() > 1) { contextMenu.getItems().add(deleteRecording); } @@ -424,7 +425,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { LOG.error("Error while downloading recording", e1); } }); - if (!Config.getInstance().getSettings().localRecording && recordings.get(0).getStatus() == STATUS.FINISHED) { + if (!Config.getInstance().getSettings().localRecording && recordings.get(0).getStatus() == State.FINISHED) { contextMenu.getItems().add(downloadRecording); } @@ -463,11 +464,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener { download.start(url.toString(), target, (progress) -> { Platform.runLater(() -> { if (progress == 100) { - recording.setStatus(STATUS.FINISHED); + recording.setStatus(FINISHED); recording.setProgress(-1); LOG.debug("Download finished for recording {}", recording.getPath()); } else { - recording.setStatus(STATUS.DOWNLOADING); + recording.setStatus(DOWNLOADING); recording.setProgress(progress); } }); @@ -482,7 +483,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { Platform.runLater(new Runnable() { @Override public void run() { - recording.setStatus(STATUS.FINISHED); + recording.setStatus(FINISHED); recording.setProgress(-1); } }); @@ -493,7 +494,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { t.setName("Download Thread " + recording.getPath()); t.start(); - recording.setStatus(STATUS.DOWNLOADING); + recording.setStatus(State.DOWNLOADING); recording.setProgress(0); } } @@ -563,7 +564,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { List deleted = new ArrayList<>(); for (Iterator iterator = recordings.iterator(); iterator.hasNext();) { JavaFxRecording r = iterator.next(); - if(r.getStatus() != STATUS.FINISHED) { + if(r.getStatus() != FINISHED) { continue; } try { diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index 00d3fe77..ee38177a 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -25,8 +25,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Config; -import ctbrec.EventBusHolder; import ctbrec.Model; +import ctbrec.event.EventBusHolder; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; import ctbrec.sites.mfc.MyFreeCamsClient; diff --git a/client/src/main/java/ctbrec/ui/TokenLabel.java b/client/src/main/java/ctbrec/ui/TokenLabel.java index c24c40a9..2117c444 100644 --- a/client/src/main/java/ctbrec/ui/TokenLabel.java +++ b/client/src/main/java/ctbrec/ui/TokenLabel.java @@ -9,7 +9,7 @@ import org.slf4j.LoggerFactory; import com.google.common.eventbus.Subscribe; -import ctbrec.EventBusHolder; +import ctbrec.event.EventBusHolder; import ctbrec.sites.Site; import javafx.application.Platform; import javafx.concurrent.Task; diff --git a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java new file mode 100644 index 00000000..08e57242 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java @@ -0,0 +1,30 @@ +package ctbrec.ui.settings; + +import ctbrec.event.Event.Type; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TitledPane; +import javafx.scene.layout.GridPane; + +public class ActionSettingsPanel extends TitledPane { + + public ActionSettingsPanel(SettingsTab settingsTab) { + setText("Actions"); + setExpanded(true); + setCollapsible(false); + createGui(); + } + + private void createGui() { + GridPane mainLayout = SettingsTab.createGridLayout(); + setContent(mainLayout); + + int row = 0; + for (Type type : Type.values()) { + Label l = new Label(type.name()); + mainLayout.add(l, 0, row); + Button b = new Button("Configure"); + mainLayout.add(b, 1, row++); + } + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java index 06b1824c..d7eebd8d 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java @@ -1,6 +1,6 @@ package ctbrec.ui.sites.bonga; -import static ctbrec.Model.STATUS.*; +import static ctbrec.Model.State.*; import java.io.IOException; import java.util.ArrayList; diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaFollowedUpdateService.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaFollowedUpdateService.java index 39935366..2d225780 100644 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaFollowedUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaFollowedUpdateService.java @@ -1,6 +1,6 @@ package ctbrec.ui.sites.camsoda; -import static ctbrec.Model.STATUS.*; +import static ctbrec.Model.State.*; import java.io.IOException; import java.util.ArrayList; diff --git a/common/src/main/java/ctbrec/AbstractModel.java b/common/src/main/java/ctbrec/AbstractModel.java index 2c925369..cd9803e7 100644 --- a/common/src/main/java/ctbrec/AbstractModel.java +++ b/common/src/main/java/ctbrec/AbstractModel.java @@ -21,7 +21,7 @@ public abstract class AbstractModel implements Model { private int streamUrlIndex = -1; private boolean suspended = false; protected Site site; - protected STATUS onlineState = STATUS.UNKNOWN; + protected State onlineState = State.UNKNOWN; @Override public boolean isOnline() throws IOException, ExecutionException, InterruptedException { @@ -123,11 +123,11 @@ public abstract class AbstractModel implements Model { } @Override - public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { + public State getOnlineState(boolean failFast) throws IOException, ExecutionException { return onlineState; } - public void setOnlineState(STATUS status) { + public void setOnlineState(State status) { this.onlineState = status; } diff --git a/common/src/main/java/ctbrec/Model.java b/common/src/main/java/ctbrec/Model.java index 7df65838..77127903 100644 --- a/common/src/main/java/ctbrec/Model.java +++ b/common/src/main/java/ctbrec/Model.java @@ -14,7 +14,7 @@ import ctbrec.sites.Site; public interface Model { - public static enum STATUS { + public static enum State { ONLINE("online"), OFFLINE("offline"), AWAY("away"), @@ -23,7 +23,7 @@ public interface Model { UNKNOWN("unknown"); String display; - STATUS(String display) { + State(String display) { this.display = display; } @@ -65,7 +65,7 @@ public interface Model { public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException; - public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException; + public State getOnlineState(boolean failFast) throws IOException, ExecutionException; public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException; diff --git a/common/src/main/java/ctbrec/Recording.java b/common/src/main/java/ctbrec/Recording.java index cd41386d..e1417306 100644 --- a/common/src/main/java/ctbrec/Recording.java +++ b/common/src/main/java/ctbrec/Recording.java @@ -10,11 +10,11 @@ public class Recording { private Instant startDate; private String path; private boolean hasPlaylist; - private STATUS status = STATUS.UNKNOWN; + private State status = State.UNKNOWN; private int progress = -1; private long sizeInByte; - public static enum STATUS { + public static enum State { RECORDING, GENERATING_PLAYLIST, STOPPED, @@ -50,11 +50,11 @@ public class Recording { this.startDate = startDate; } - public STATUS getStatus() { + public State getStatus() { return status; } - public void setStatus(STATUS status) { + public void setStatus(State status) { this.status = status; } diff --git a/common/src/main/java/ctbrec/event/AbstractModelEvent.java b/common/src/main/java/ctbrec/event/AbstractModelEvent.java new file mode 100644 index 00000000..51da54e7 --- /dev/null +++ b/common/src/main/java/ctbrec/event/AbstractModelEvent.java @@ -0,0 +1,16 @@ +package ctbrec.event; + +import ctbrec.Model; + +public abstract class AbstractModelEvent extends Event { + + protected Model model; + + public Model getModel() { + return model; + } + + public void setModel(Model model) { + this.model = model; + } +} diff --git a/common/src/main/java/ctbrec/event/Event.java b/common/src/main/java/ctbrec/event/Event.java new file mode 100644 index 00000000..a5477e6d --- /dev/null +++ b/common/src/main/java/ctbrec/event/Event.java @@ -0,0 +1,28 @@ +package ctbrec.event; + +public abstract class Event { + + public static enum Type { + /** + * This event is fired every time the OnlineMonitor sees a model online + * It is also fired, if the model was online before. You can see it as a "still online ping". + */ + MODEL_ONLINE, + + /** + * This event is fired whenever the model's online state (Model.STATUS) changes. + */ + MODEL_STATUS_CHANGED, + + + /** + * This event is fired whenever the state of a recording changes. + */ + RECORDING_STATUS_CHANGED + } + + public abstract Type getType(); + public abstract String getName(); + public abstract String getDescription(); + public abstract String[] getExecutionParams(); +} diff --git a/common/src/main/java/ctbrec/event/EventBusHolder.java b/common/src/main/java/ctbrec/event/EventBusHolder.java index 05e42ee7..e848c892 100644 --- a/common/src/main/java/ctbrec/event/EventBusHolder.java +++ b/common/src/main/java/ctbrec/event/EventBusHolder.java @@ -1,4 +1,4 @@ -package ctbrec; +package ctbrec.event; import java.util.concurrent.Executors; @@ -12,24 +12,5 @@ public class EventBusHolder { public static final String STATUS = "status"; public static final String MODEL = "model"; - public static enum EVENT_TYPE { - /** - * This event is fired every time the OnlineMonitor sees a model online - * It is also fired, if the model was online before. You can see it as a "still online ping". - */ - MODEL_ONLINE, - - /** - * This event is fired whenever the model's online state (Model.STATUS) changes. - */ - MODEL_STATUS_CHANGED, - - - /** - * This event is fired whenever the state of a recording changes. - */ - RECORDING_STATUS_CHANGED - } - public static final EventBus BUS = new AsyncEventBus(Executors.newSingleThreadExecutor()); } diff --git a/common/src/main/java/ctbrec/event/EventReaction.java b/common/src/main/java/ctbrec/event/EventReaction.java new file mode 100644 index 00000000..8ee2df81 --- /dev/null +++ b/common/src/main/java/ctbrec/event/EventReaction.java @@ -0,0 +1,32 @@ +package ctbrec.event; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class EventReaction { + + private List> predicates = new ArrayList<>(); + private Consumer action; + + @SafeVarargs + public EventReaction(Consumer action, Predicate... predicates) { + this.action = action; + for (Predicate predicate : predicates) { + this.predicates.add(predicate); + } + } + + public void reactToEvent(Event evt) { + boolean matches = true; + for (Predicate predicate : predicates) { + if(!predicate.test(evt)) { + matches = false; + } + } + if(matches) { + action.accept(evt); + } + } +} diff --git a/common/src/main/java/ctbrec/event/LogReaction.java b/common/src/main/java/ctbrec/event/LogReaction.java new file mode 100644 index 00000000..ec5444f7 --- /dev/null +++ b/common/src/main/java/ctbrec/event/LogReaction.java @@ -0,0 +1,15 @@ +package ctbrec.event; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LogReaction extends EventReaction { + + private static final transient Logger LOG = LoggerFactory.getLogger(LogReaction.class); + + public LogReaction() { + super(evt -> { + LOG.debug("LogReaction: {}", evt); + }, TypePredicate.of(Event.Type.RECORDING_STATUS_CHANGED)); + } +} diff --git a/common/src/main/java/ctbrec/event/ModelIsOnlineEvent.java b/common/src/main/java/ctbrec/event/ModelIsOnlineEvent.java new file mode 100644 index 00000000..d93e7d5d --- /dev/null +++ b/common/src/main/java/ctbrec/event/ModelIsOnlineEvent.java @@ -0,0 +1,36 @@ +package ctbrec.event; + +import ctbrec.Model; + +public class ModelIsOnlineEvent extends AbstractModelEvent { + + public ModelIsOnlineEvent(Model model) { + super.model = model; + } + + @Override + public Type getType() { + return Event.Type.MODEL_ONLINE; + } + + @Override + public String getName() { + return "Model is online"; + } + + @Override + public String getDescription() { + return "Repeatedly fired when a model is online"; + } + + @Override + public String[] getExecutionParams() { + return new String[] { + getType().toString(), + model.getDisplayName(), + model.getUrl(), + model.getSite().getName() + }; + } + +} diff --git a/common/src/main/java/ctbrec/event/ModelStateChangedEvent.java b/common/src/main/java/ctbrec/event/ModelStateChangedEvent.java new file mode 100644 index 00000000..98d681e7 --- /dev/null +++ b/common/src/main/java/ctbrec/event/ModelStateChangedEvent.java @@ -0,0 +1,59 @@ +package ctbrec.event; + +import ctbrec.Model; +import ctbrec.Model.State; + +public class ModelStateChangedEvent extends AbstractModelEvent { + + private State oldState; + private State newState; + + public ModelStateChangedEvent(Model model, Model.State oldState, Model.State newState) { + super.model = model; + this.oldState = oldState; + this.newState = newState; + } + + @Override + public Type getType() { + return Event.Type.MODEL_STATUS_CHANGED; + } + + @Override + public String getName() { + return "Model state changed"; + } + + @Override + public String getDescription() { + return "Fired when a model state changed. E.g. from OFFLINE to ONLINE"; + } + + @Override + public String[] getExecutionParams() { + return new String[] { + getType().toString(), + model.getDisplayName(), + model.getUrl(), + model.getSite().getName(), + oldState.toString(), + newState.toString() + }; + } + + public State getOldState() { + return oldState; + } + + public void setOldState(State oldState) { + this.oldState = oldState; + } + + public State getNewState() { + return newState; + } + + public void setNewState(State newState) { + this.newState = newState; + } +} diff --git a/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java b/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java new file mode 100644 index 00000000..f260552e --- /dev/null +++ b/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java @@ -0,0 +1,55 @@ +package ctbrec.event; + +import java.io.File; +import java.time.Instant; + +import ctbrec.Model; +import ctbrec.Recording.State; + +public class RecordingStateChangedEvent extends AbstractModelEvent { + + private File path; + private State newState; + private Instant startTime; + + public RecordingStateChangedEvent(File recording, State newState, Model model, Instant startTime) { + super.model = model; + this.path = recording; + this.newState = newState; + this.startTime = startTime; + } + + @Override + public Type getType() { + return Event.Type.RECORDING_STATUS_CHANGED; + } + + @Override + public String getName() { + return "Recording state changed"; + } + + @Override + public String getDescription() { + return "Fired when a recording state changed. E.g. from RECORDING to STOPPED"; + } + + @Override + public String[] getExecutionParams() { + return new String[] { + getType().toString(), + path.getAbsolutePath(), + newState.toString(), + model.getDisplayName(), + model.getSite().getName(), + model.getUrl(), + Long.toString(startTime.getEpochSecond()) + }; + } + + @Override + public String toString() { + return "RecordingStateChanged[" + newState + "," + model.getDisplayName() + "," + path + "]"; + } + +} diff --git a/common/src/main/java/ctbrec/event/TypePredicate.java b/common/src/main/java/ctbrec/event/TypePredicate.java new file mode 100644 index 00000000..e84be4df --- /dev/null +++ b/common/src/main/java/ctbrec/event/TypePredicate.java @@ -0,0 +1,23 @@ +package ctbrec.event; + +import java.util.function.Predicate; + +import ctbrec.event.Event.Type; + +public class TypePredicate implements Predicate { + + private Type type; + + private TypePredicate(Type type) { + this.type = type; + } + + @Override + public boolean test(Event evt) { + return evt.getType() == type; + } + + public static TypePredicate of(Type type) { + return new TypePredicate(type); + } +} diff --git a/common/src/main/java/ctbrec/io/StreamRedirectThread.java b/common/src/main/java/ctbrec/io/StreamRedirectThread.java index 07d88485..2ff52c9c 100644 --- a/common/src/main/java/ctbrec/io/StreamRedirectThread.java +++ b/common/src/main/java/ctbrec/io/StreamRedirectThread.java @@ -26,7 +26,7 @@ public class StreamRedirectThread implements Runnable { while(in != null && (length = in.read(buffer)) >= 0) { out.write(buffer, 0, length); } - LOG.debug("Stream redirect thread ended"); + LOG.trace("Stream redirect thread ended"); } catch(Exception e) { LOG.error("Couldn't redirect stream: {}", e.getLocalizedMessage()); } diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 85309816..2cd50878 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -1,8 +1,7 @@ package ctbrec.recorder; -import static ctbrec.EventBusHolder.*; -import static ctbrec.EventBusHolder.EVENT_TYPE.*; -import static ctbrec.Recording.STATUS.*; +import static ctbrec.Recording.State.*; +import static ctbrec.event.Event.Type.*; import java.io.File; import java.io.FilenameFilter; @@ -24,7 +23,6 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; -import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @@ -38,11 +36,14 @@ import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; import ctbrec.Config; -import ctbrec.EventBusHolder; import ctbrec.Model; import ctbrec.OS; import ctbrec.Recording; -import ctbrec.Recording.STATUS; +import ctbrec.Recording.State; +import ctbrec.event.Event; +import ctbrec.event.EventBusHolder; +import ctbrec.event.ModelIsOnlineEvent; +import ctbrec.event.RecordingStateChangedEvent; import ctbrec.io.HttpClient; import ctbrec.io.StreamRedirectThread; import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException; @@ -97,10 +98,11 @@ public class LocalRecorder implements Recorder { private void registerEventBusListener() { EventBusHolder.BUS.register(new Object() { @Subscribe - public void modelEvent(Map e) { + public void modelEvent(Event e) { try { - if (Objects.equals(e.get(EVENT), MODEL_ONLINE)) { - Model model = (Model) e.get(MODEL); + if (e.getType() == MODEL_ONLINE) { + ModelIsOnlineEvent evt = (ModelIsOnlineEvent) e; + Model model = evt.getModel(); if(!isSuspended(model) && !recordingProcesses.containsKey(model)) { startRecordingProcess(model); } @@ -198,7 +200,6 @@ public class LocalRecorder implements Recorder { } } }.start(); - fireRecordingStateChanged(model, RECORDING); } private void stopRecordingProcess(Model model) { @@ -208,7 +209,7 @@ public class LocalRecorder implements Recorder { if(!Config.isServerMode()) { postprocess(download); } - fireRecordingStateChanged(model, FINISHED); + fireRecordingStateChanged(download.getTarget(), FINISHED, model, download.getStartTime()); } private void postprocess(Download download) { @@ -388,7 +389,7 @@ public class LocalRecorder implements Recorder { } else { postprocess(d); } - fireRecordingStateChanged(m, FINISHED); // TODO fire all the events + fireRecordingStateChanged(d.getTarget(), FINISHED, m, d.getStartTime()); // TODO fire all the events } } for (Model m : restart) { @@ -439,13 +440,9 @@ public class LocalRecorder implements Recorder { } } - private void fireRecordingStateChanged(Model model, Recording.STATUS status) { - Map evt = new HashMap<>(); - evt.put(EVENT, RECORDING_STATUS_CHANGED); - evt.put(STATUS, status); - evt.put(MODEL, model); + private void fireRecordingStateChanged(File path, Recording.State newState, Model model, Instant startTime) { + RecordingStateChangedEvent evt = new RecordingStateChangedEvent(path, newState, model, startTime); EventBusHolder.BUS.post(evt); - LOG.debug("Event fired {}", evt); } private class PostProcessingTrigger extends Thread { @@ -532,7 +529,7 @@ public class LocalRecorder implements Recorder { return recordings; } - private STATUS getStatus(Recording recording) { + private State getStatus(Recording recording) { File absolutePath = new File(Config.getInstance().getSettings().recordingsDir, recording.getPath()); PlaylistGenerator playlistGenerator = playlistGenerators.get(absolutePath); diff --git a/common/src/main/java/ctbrec/recorder/OnlineMonitor.java b/common/src/main/java/ctbrec/recorder/OnlineMonitor.java index 5ec98567..15370194 100644 --- a/common/src/main/java/ctbrec/recorder/OnlineMonitor.java +++ b/common/src/main/java/ctbrec/recorder/OnlineMonitor.java @@ -1,8 +1,6 @@ package ctbrec.recorder; -import static ctbrec.EventBusHolder.*; -import static ctbrec.EventBusHolder.EVENT_TYPE.*; -import static ctbrec.Model.STATUS.*; +import static ctbrec.Model.State.*; import java.io.InterruptedIOException; import java.net.SocketTimeoutException; @@ -18,9 +16,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Config; -import ctbrec.EventBusHolder; import ctbrec.Model; -import ctbrec.Model.STATUS; +import ctbrec.event.EventBusHolder; +import ctbrec.event.ModelIsOnlineEvent; +import ctbrec.event.ModelStateChangedEvent; import ctbrec.io.HttpException; public class OnlineMonitor extends Thread { @@ -30,7 +29,7 @@ public class OnlineMonitor extends Thread { private volatile boolean running = false; private Recorder recorder; - private Map states = new HashMap<>(); + private Map states = new HashMap<>(); public OnlineMonitor(Recorder recorder) { this.recorder = recorder; @@ -57,13 +56,13 @@ public class OnlineMonitor extends Thread { for (Model model : models) { try { if(model.isOnline(IGNORE_CACHE)) { - fireModelOnline(model); + EventBusHolder.BUS.post(new ModelIsOnlineEvent(model)); } - STATUS state = model.getOnlineState(false); - STATUS oldState = states.getOrDefault(model, UNKNOWN); + Model.State state = model.getOnlineState(false); + Model.State oldState = states.getOrDefault(model, UNKNOWN); states.put(model, state); if(state != oldState) { - fireModelOnlineStateChanged(model, oldState, state); + EventBusHolder.BUS.post(new ModelStateChangedEvent(model, oldState, state)); } } catch (HttpException e) { LOG.error("Couldn't check if model {} is online. HTTP Response: {} - {}", @@ -98,22 +97,6 @@ public class OnlineMonitor extends Thread { LOG.debug(getName() + " terminated"); } - private void fireModelOnline(Model model) { - Map evt = new HashMap<>(); - evt.put(EVENT, MODEL_ONLINE); - evt.put(MODEL, model); - EventBusHolder.BUS.post(evt); - } - - private void fireModelOnlineStateChanged(Model model, STATUS oldStatus, STATUS newStatus) { - Map evt = new HashMap<>(); - evt.put(EVENT, MODEL_STATUS_CHANGED); - evt.put(STATUS, newStatus); - evt.put(OLD, oldStatus); - evt.put(MODEL, model); - EventBusHolder.BUS.post(evt); - } - public void shutdown() { running = false; interrupt(); diff --git a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java index 4056bdc7..dc56188e 100644 --- a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -18,10 +18,10 @@ import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import ctbrec.Config; -import ctbrec.EventBusHolder; import ctbrec.Hmac; import ctbrec.Model; import ctbrec.Recording; +import ctbrec.event.EventBusHolder; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import ctbrec.io.InstantJsonAdapter; diff --git a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java index c2720b16..94aa5a4d 100644 --- a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java @@ -1,5 +1,7 @@ package ctbrec.recorder.download; +import static ctbrec.Recording.State.*; + import java.io.EOFException; import java.io.File; import java.io.FileNotFoundException; @@ -24,6 +26,8 @@ import com.iheartradio.m3u8.PlaylistException; import ctbrec.Config; import ctbrec.Model; +import ctbrec.event.EventBusHolder; +import ctbrec.event.RecordingStateChangedEvent; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import okhttp3.Request; @@ -54,6 +58,10 @@ public class HlsDownload extends AbstractHlsDownload { throw new IOException(model.getName() +"'s room is not public"); } + // let the world know, that we are recording now + RecordingStateChangedEvent evt = new RecordingStateChangedEvent(getTarget(), RECORDING, model, getStartTime()); + EventBusHolder.BUS.post(evt); + String segments = getSegmentPlaylistUrl(model); if(segments != null) { if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) { diff --git a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java index e876af62..e298d48a 100644 --- a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java @@ -1,5 +1,6 @@ package ctbrec.recorder.download; +import static ctbrec.Recording.State.*; import static java.nio.file.StandardOpenOption.*; import java.io.ByteArrayInputStream; @@ -44,6 +45,8 @@ import com.iheartradio.m3u8.PlaylistException; import ctbrec.Config; import ctbrec.Hmac; import ctbrec.Model; +import ctbrec.event.EventBusHolder; +import ctbrec.event.RecordingStateChangedEvent; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import ctbrec.recorder.ProgressListener; @@ -126,6 +129,11 @@ public class MergedHlsDownload extends AbstractHlsDownload { splitRecStartTime = ZonedDateTime.now(); super.model = model; targetFile = Config.getInstance().getFileForRecording(model); + + // let the world know, that we are recording now + RecordingStateChangedEvent evt = new RecordingStateChangedEvent(getTarget(), RECORDING, model, getStartTime()); + EventBusHolder.BUS.post(evt); + String segments = getSegmentPlaylistUrl(model); mergeThread = createMergeThread(targetFile, null, true); mergeThread.start(); diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java b/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java index 8414adda..bd891eba 100644 --- a/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java +++ b/common/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java @@ -1,6 +1,6 @@ package ctbrec.sites.bonga; -import static ctbrec.Model.STATUS.*; +import static ctbrec.Model.State.*; import java.io.IOException; import java.io.InputStream; @@ -85,7 +85,7 @@ public class BongaCamsModel extends AbstractModel { } @Override - public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { + public State getOnlineState(boolean failFast) throws IOException, ExecutionException { if(failFast) { return onlineState; } else { @@ -97,7 +97,7 @@ public class BongaCamsModel extends AbstractModel { } @Override - public void setOnlineState(STATUS onlineState) { + public void setOnlineState(State onlineState) { this.onlineState = onlineState; } diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java index 680b0d59..4b9c4842 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java @@ -1,6 +1,6 @@ package ctbrec.sites.cam4; -import static ctbrec.Model.STATUS.*; +import static ctbrec.Model.State.*; import java.io.IOException; import java.io.InputStream; @@ -109,7 +109,7 @@ public class Cam4Model extends AbstractModel { } @Override - public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { + public State getOnlineState(boolean failFast) throws IOException, ExecutionException { if(failFast) { return onlineState; } else { diff --git a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index 12421216..36bd9019 100644 --- a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -1,6 +1,6 @@ package ctbrec.sites.camsoda; -import static ctbrec.Model.STATUS.*; +import static ctbrec.Model.State.*; import java.io.IOException; import java.io.InputStream; @@ -102,7 +102,7 @@ public class CamsodaModel extends AbstractModel { } @Override - public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { + public State getOnlineState(boolean failFast) throws IOException, ExecutionException { if(failFast) { return onlineState; } else { diff --git a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java index faad4fd8..73531926 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java @@ -1,6 +1,6 @@ package ctbrec.sites.chaturbate; -import static ctbrec.Model.STATUS.*; +import static ctbrec.Model.State.*; import java.io.IOException; import java.util.ArrayList; @@ -76,12 +76,12 @@ public class ChaturbateModel extends AbstractModel { getChaturbate().streamInfoCache.invalidate(getName()); } - public STATUS getOnlineState() throws IOException, ExecutionException { + public State getOnlineState() throws IOException, ExecutionException { return getOnlineState(false); } @Override - public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { + public State getOnlineState(boolean failFast) throws IOException, ExecutionException { if(failFast) { StreamInfo info = getChaturbate().streamInfoCache.getIfPresent(getName()); setOnlineStateByRoomStatus(info.room_status); @@ -109,11 +109,11 @@ public class ChaturbateModel extends AbstractModel { onlineState = AWAY; break; case "group": - onlineState = STATUS.GROUP; + onlineState = State.GROUP; break; default: LOG.debug("Unknown show type {}", room_status); - onlineState = STATUS.UNKNOWN; + onlineState = State.UNKNOWN; } } } diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index c9317abf..c1bf7fea 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -601,7 +601,7 @@ public class MyFreeCamsClient { String name = json.getString("nm"); MyFreeCamsModel model = mfc.createModel(name); model.setUid(json.getInt("uid")); - model.setState(State.of(json.getInt("vs"))); + model.setMfcState(State.of(json.getInt("vs"))); String uid = Integer.toString(model.getUid()); String uidStart = uid.substring(0, 3); String previewUrl = "https://img.mfcimg.com/photos2/"+uidStart+'/'+uid+"/avatar.90x90.jpg"; diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index 0c30428d..4e07f0cd 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -46,7 +46,7 @@ public class MyFreeCamsModel extends AbstractModel { private String hlsUrl; private double camScore; private int viewerCount; - private State state; + private ctbrec.sites.mfc.State state; private int resolution[] = new int[2]; /** @@ -61,7 +61,7 @@ public class MyFreeCamsModel extends AbstractModel { @Override public boolean isOnline() throws IOException, ExecutionException, InterruptedException { MyFreeCamsClient.getInstance().update(this); - return state == State.ONLINE; + return state == ctbrec.sites.mfc.State.ONLINE; } @Override @@ -70,27 +70,27 @@ public class MyFreeCamsModel extends AbstractModel { } @Override - public STATUS getOnlineState(boolean failFast) throws IOException, ExecutionException { + public State getOnlineState(boolean failFast) throws IOException, ExecutionException { if(state == null) { - return STATUS.UNKNOWN; + return State.UNKNOWN; } switch(state) { case ONLINE: case RECORDING: - return ctbrec.Model.STATUS.ONLINE; + return ctbrec.Model.State.ONLINE; case AWAY: - return ctbrec.Model.STATUS.AWAY; + return ctbrec.Model.State.AWAY; case PRIVATE: - return ctbrec.Model.STATUS.PRIVATE; + return ctbrec.Model.State.PRIVATE; case GROUP_SHOW: - return ctbrec.Model.STATUS.GROUP; + return ctbrec.Model.State.GROUP; case OFFLINE: case CAMOFF: - return ctbrec.Model.STATUS.OFFLINE; + return ctbrec.Model.State.OFFLINE; default: LOG.debug("State {} is not mapped", this.state); - return ctbrec.Model.STATUS.UNKNOWN; + return ctbrec.Model.State.UNKNOWN; } } @@ -233,7 +233,7 @@ public class MyFreeCamsModel extends AbstractModel { this.camScore = camScore; } - public void setState(State state) { + public void setMfcState(ctbrec.sites.mfc.State state) { this.state = state; } @@ -249,7 +249,7 @@ public class MyFreeCamsModel extends AbstractModel { public void update(SessionState state, String streamUrl) { uid = Integer.parseInt(state.getUid().toString()); setName(state.getNm()); - setState(State.of(state.getVs())); + setMfcState(ctbrec.sites.mfc.State.of(state.getVs())); setStreamUrl(streamUrl); Optional camScore = Optional.ofNullable(state.getM()).map(m -> m.getCamscore()); setCamScore(camScore.orElse(0.0)); @@ -258,7 +258,7 @@ public class MyFreeCamsModel extends AbstractModel { String uid = state.getUid().toString(); String uidStart = uid.substring(0, 3); String previewUrl = "https://img.mfcimg.com/photos2/"+uidStart+'/'+uid+"/avatar.300x300.jpg"; - if(MyFreeCamsModel.this.state == State.ONLINE) { + if(MyFreeCamsModel.this.state == ctbrec.sites.mfc.State.ONLINE) { try { previewUrl = getLivePreviewUrl(state); } catch(Exception e) { From f7dfabb898545709ce2456949b35010d321fa5d6 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 8 Dec 2018 13:06:24 +0100 Subject: [PATCH 143/231] Remove playback of sound with notification --- common/src/main/java/ctbrec/OS.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/src/main/java/ctbrec/OS.java b/common/src/main/java/ctbrec/OS.java index c388e3c1..d5181887 100644 --- a/common/src/main/java/ctbrec/OS.java +++ b/common/src/main/java/ctbrec/OS.java @@ -16,7 +16,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.io.StreamRedirectThread; -import javafx.scene.media.AudioClip; public class OS { @@ -115,8 +114,6 @@ public class OS { }); new Thread(new StreamRedirectThread(p.getInputStream(), System.out)).start(); new Thread(new StreamRedirectThread(p.getErrorStream(), System.err)).start(); - AudioClip clip = new AudioClip(OS.class.getResource("/Oxygen-Im-Highlight-Msg.mp3").toString()); - clip.play(); } catch (IOException e1) { LOG.error("Notification failed", e1); } From 1fc16a0d4122ed8c04bc1c65c963ccd4343c7aca Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 8 Dec 2018 13:08:20 +0100 Subject: [PATCH 144/231] Add a few predicates and actions for the event system --- .../java/ctbrec/ui/CamrecApplication.java | 29 +++--- .../main/java/ctbrec/ui/controls/Wizard.java | 74 +++++++++++++++ .../ui/event/ModelStateNotification.java | 23 +++++ .../main/java/ctbrec/ui/event/PlaySound.java | 23 +++++ .../ui/settings/ActionSettingsPanel.java | 82 +++++++++++++++-- .../java/ctbrec/ui/settings/SettingsTab.java | 4 +- common/src/main/java/ctbrec/event/Action.java | 16 ++++ common/src/main/java/ctbrec/event/Event.java | 17 +++- .../{EventReaction.java => EventHandler.java} | 18 ++-- .../event/EventHandlerConfiguration.java | 89 +++++++++++++++++++ ...Predicate.java => EventTypePredicate.java} | 8 +- .../main/java/ctbrec/event/LogReaction.java | 15 ---- .../java/ctbrec/event/ModelPredicate.java | 30 +++++++ .../ctbrec/event/ModelStatePredicate.java | 29 ++++++ 14 files changed, 406 insertions(+), 51 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/controls/Wizard.java create mode 100644 client/src/main/java/ctbrec/ui/event/ModelStateNotification.java create mode 100644 client/src/main/java/ctbrec/ui/event/PlaySound.java create mode 100644 common/src/main/java/ctbrec/event/Action.java rename common/src/main/java/ctbrec/event/{EventReaction.java => EventHandler.java} (55%) create mode 100644 common/src/main/java/ctbrec/event/EventHandlerConfiguration.java rename common/src/main/java/ctbrec/event/{TypePredicate.java => EventTypePredicate.java} (55%) delete mode 100644 common/src/main/java/ctbrec/event/LogReaction.java create mode 100644 common/src/main/java/ctbrec/event/ModelPredicate.java create mode 100644 common/src/main/java/ctbrec/event/ModelStatePredicate.java diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 70bbf4bd..df11cabc 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -26,12 +26,10 @@ import com.squareup.moshi.Types; import ctbrec.Config; import ctbrec.Model; -import ctbrec.OS; import ctbrec.StringUtil; import ctbrec.Version; import ctbrec.event.Event; import ctbrec.event.EventBusHolder; -import ctbrec.event.LogReaction; import ctbrec.event.ModelStateChangedEvent; import ctbrec.io.HttpClient; import ctbrec.recorder.LocalRecorder; @@ -73,7 +71,7 @@ public class CamrecApplication extends Application { private TabPane rootPane = new TabPane(); private List sites = new ArrayList<>(); public static HttpClient httpClient; - + public static String title; @Override public void start(Stage primaryStage) throws Exception { @@ -114,7 +112,8 @@ public class CamrecApplication extends Application { private void createGui(Stage primaryStage) throws IOException { LOG.debug("Creating GUI"); - primaryStage.setTitle("CTB Recorder " + getVersion()); + CamrecApplication.title = "CTB Recorder " + getVersion(); + primaryStage.setTitle(title); InputStream icon = getClass().getResourceAsStream("/icon.png"); primaryStage.getIcons().add(new Image(icon)); int windowWidth = Config.getInstance().getSettings().windowWidth; @@ -230,10 +229,11 @@ public class CamrecApplication extends Application { if (e.getType() == MODEL_STATUS_CHANGED) { ModelStateChangedEvent evt = (ModelStateChangedEvent) e; Model model = evt.getModel(); - if (evt.getNewState() == ONLINE) { + if (evt.getNewState() == ONLINE && primaryStage != null && primaryStage.getTitle() != null) { String header = "Model Online"; String msg = model.getDisplayName() + " is now online"; - OS.notification(primaryStage.getTitle(), header, msg); + LOG.debug(msg); + //OS.notification(primaryStage.getTitle(), header, msg); } } } catch (Exception e1) { @@ -242,13 +242,16 @@ public class CamrecApplication extends Application { } }); - EventBusHolder.BUS.register(new Object() { - LogReaction reaction = new LogReaction(); - @Subscribe - public void modelEvent(Event e) { - reaction.reactToEvent(e); - } - }); + // EventBusHolder.BUS.register(new Object() { + // URL url = CamrecApplication.class.getResource("/Oxygen-Im-Highlight-Msg.mp3"); + // PlaySound playSound = new PlaySound(url); + // EventHandler reaction = new EventHandler(playSound); + // // LogReaction reaction = new LogReaction(); + // @Subscribe + // public void modelEvent(Event e) { + // reaction.reactToEvent(e); + // } + // }); LOG.debug("Alert System registered"); diff --git a/client/src/main/java/ctbrec/ui/controls/Wizard.java b/client/src/main/java/ctbrec/ui/controls/Wizard.java new file mode 100644 index 00000000..bc653e2e --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/Wizard.java @@ -0,0 +1,74 @@ +package ctbrec.ui.controls; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; + +public class Wizard extends BorderPane { + + private Pane[] pages; + private StackPane stack; + private Stage stage; + private int page = 0; + private Button next; + private Button prev; + private Button finish; + + public Wizard(Stage stage, Pane... pages) { + this.stage = stage; + this.pages = pages; + + if (pages.length == 0) { + throw new IllegalArgumentException("Provide at least one page"); + } + + createUi(); + } + + private void createUi() { + stack = new StackPane(); + setCenter(stack); + + next = new Button("Next"); + next.setOnAction(evt -> nextPage()); + prev = new Button("Prev"); + prev.setOnAction(evt -> prevPage()); + Button cancel = new Button("Cancel"); + cancel.setOnAction(evt -> stage.close()); + finish = new Button("Finish"); + HBox buttons = new HBox(5, prev, next, cancel, finish); + buttons.setAlignment(Pos.BASELINE_RIGHT); + setBottom(buttons); + BorderPane.setMargin(buttons, new Insets(5)); + + if (pages.length != 0) { + stack.getChildren().add(pages[0]); + } + setButtonStates(); + } + + private void prevPage() { + page = Math.max(0, --page); + stack.getChildren().clear(); + stack.getChildren().add(pages[page]); + setButtonStates(); + } + + private void nextPage() { + page = Math.min(pages.length - 1, ++page); + stack.getChildren().clear(); + stack.getChildren().add(pages[page]); + setButtonStates(); + } + + private void setButtonStates() { + prev.setDisable(page == 0); + next.setDisable(page == pages.length - 1); + finish.setDisable(page != pages.length - 1); + } +} diff --git a/client/src/main/java/ctbrec/ui/event/ModelStateNotification.java b/client/src/main/java/ctbrec/ui/event/ModelStateNotification.java new file mode 100644 index 00000000..a831b4e3 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/event/ModelStateNotification.java @@ -0,0 +1,23 @@ +package ctbrec.ui.event; + +import ctbrec.OS; +import ctbrec.event.Action; +import ctbrec.event.Event; +import ctbrec.ui.CamrecApplication; + +public class ModelStateNotification extends Action { + + private String header; + private String msg; + + public ModelStateNotification(String header, String msg) { + this.header = header; + this.msg = msg; + name = "show notification"; + } + + @Override + public void accept(Event evt) { + OS.notification(CamrecApplication.title, header, msg); + } +} diff --git a/client/src/main/java/ctbrec/ui/event/PlaySound.java b/client/src/main/java/ctbrec/ui/event/PlaySound.java new file mode 100644 index 00000000..7464bebb --- /dev/null +++ b/client/src/main/java/ctbrec/ui/event/PlaySound.java @@ -0,0 +1,23 @@ +package ctbrec.ui.event; + +import java.net.URL; + +import ctbrec.event.Action; +import ctbrec.event.Event; +import javafx.scene.media.AudioClip; + +public class PlaySound extends Action { + + private URL url; + + public PlaySound(URL url) { + this.url = url; + name = "play sound"; + } + + @Override + public void accept(Event evt) { + AudioClip clip = new AudioClip(url.toString()); + clip.play(); + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java index 08e57242..250bce86 100644 --- a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java +++ b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java @@ -1,30 +1,94 @@ package ctbrec.ui.settings; -import ctbrec.event.Event.Type; +import java.io.InputStream; + +import ctbrec.event.EventHandlerConfiguration; +import ctbrec.ui.controls.Wizard; +import javafx.event.ActionEvent; +import javafx.geometry.Insets; +import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; import javafx.scene.control.TitledPane; +import javafx.scene.image.Image; +import javafx.scene.layout.Border; +import javafx.scene.layout.BorderPane; import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.stage.Stage; public class ActionSettingsPanel extends TitledPane { + private TableView actionTable; + public ActionSettingsPanel(SettingsTab settingsTab) { - setText("Actions"); + setText("Events & Actions"); setExpanded(true); setCollapsible(false); createGui(); } private void createGui() { - GridPane mainLayout = SettingsTab.createGridLayout(); + BorderPane mainLayout = new BorderPane(); setContent(mainLayout); + actionTable = createActionTable(); + actionTable.setPrefSize(300, 200); + ScrollPane scrollPane = new ScrollPane(actionTable); + scrollPane.setFitToHeight(true); + scrollPane.setFitToWidth(true); + scrollPane.setBorder(Border.EMPTY); + mainLayout.setCenter(scrollPane); + BorderPane.setMargin(scrollPane, new Insets(5)); + + Button add = new Button("Add"); + add.setOnAction(this::add); + Button delete = new Button("Delete"); + delete.setOnAction(this::delete); + delete.setDisable(true); + HBox buttons = new HBox(10, add, delete); + mainLayout.setBottom(buttons); + BorderPane.setMargin(buttons, new Insets(5)); + } + + private void add(ActionEvent evt) { + EventHandlerConfiguration config = new EventHandlerConfiguration(); + Pane namePane = createNamePane(config); + GridPane pane2 = SettingsTab.createGridLayout(); + pane2.add(new Label("Pane 2"), 0, 0); + GridPane pane3 = SettingsTab.createGridLayout(); + pane3.add(new Label("Pane 3"), 0, 0); + Stage dialog = new Stage(); + dialog.setTitle("New Action"); + InputStream icon = getClass().getResourceAsStream("/icon.png"); + dialog.getIcons().add(new Image(icon)); + Wizard root = new Wizard(dialog, namePane, pane2, pane3); + Scene scene = new Scene(root, 640, 480); + scene.getStylesheets().addAll(getScene().getStylesheets()); + dialog.setScene(scene); + dialog.showAndWait(); + } + + private void delete(ActionEvent evt) { + + } + + private Pane createNamePane(EventHandlerConfiguration config) { + GridPane layout = SettingsTab.createGridLayout(); int row = 0; - for (Type type : Type.values()) { - Label l = new Label(type.name()); - mainLayout.add(l, 0, row); - Button b = new Button("Configure"); - mainLayout.add(b, 1, row++); - } + layout.add(new Label("Name"), 0, row); + TextField name = new TextField(); + layout.add(name, 1, row); + return layout; + } + + + private TableView createActionTable() { + TableView view = new TableView(); + return view; } } diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index 0115f0a4..b31e9202 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -130,7 +130,8 @@ public class SettingsTab extends Tab implements TabSelectionListener { //right side rightSide.getChildren().add(siteConfigAccordion); - rightSide.getChildren().add(new ActionSettingsPanel(this)); + ActionSettingsPanel actions = new ActionSettingsPanel(this); + rightSide.getChildren().add(actions); proxySettingsPane = new ProxySettingsPane(this); rightSide.getChildren().add(proxySettingsPane); for (int i = 0; i < sites.size(); i++) { @@ -141,7 +142,6 @@ public class SettingsTab extends Tab implements TabSelectionListener { siteConfigAccordion.getPanes().add(pane); } } - siteConfigAccordion.setExpandedPane(siteConfigAccordion.getPanes().get(0)); } private Node createRecordLocationPanel() { diff --git a/common/src/main/java/ctbrec/event/Action.java b/common/src/main/java/ctbrec/event/Action.java new file mode 100644 index 00000000..a71b99af --- /dev/null +++ b/common/src/main/java/ctbrec/event/Action.java @@ -0,0 +1,16 @@ +package ctbrec.event; + +import java.util.function.Consumer; + +public abstract class Action implements Consumer { + + protected String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/common/src/main/java/ctbrec/event/Event.java b/common/src/main/java/ctbrec/event/Event.java index a5477e6d..de8393ee 100644 --- a/common/src/main/java/ctbrec/event/Event.java +++ b/common/src/main/java/ctbrec/event/Event.java @@ -7,18 +7,29 @@ public abstract class Event { * This event is fired every time the OnlineMonitor sees a model online * It is also fired, if the model was online before. You can see it as a "still online ping". */ - MODEL_ONLINE, + MODEL_ONLINE("Model is online"), /** * This event is fired whenever the model's online state (Model.STATUS) changes. */ - MODEL_STATUS_CHANGED, + MODEL_STATUS_CHANGED("Model status changed"), /** * This event is fired whenever the state of a recording changes. */ - RECORDING_STATUS_CHANGED + RECORDING_STATUS_CHANGED("Recording status changed"); + + private String desc; + + Type(String desc) { + this.desc = desc; + } + + @Override + public String toString() { + return desc; + } } public abstract Type getType(); diff --git a/common/src/main/java/ctbrec/event/EventReaction.java b/common/src/main/java/ctbrec/event/EventHandler.java similarity index 55% rename from common/src/main/java/ctbrec/event/EventReaction.java rename to common/src/main/java/ctbrec/event/EventHandler.java index 8ee2df81..6621a2d8 100644 --- a/common/src/main/java/ctbrec/event/EventReaction.java +++ b/common/src/main/java/ctbrec/event/EventHandler.java @@ -1,18 +1,24 @@ package ctbrec.event; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; import java.util.function.Predicate; -public class EventReaction { +public class EventHandler { private List> predicates = new ArrayList<>(); - private Consumer action; + private List> actions; @SafeVarargs - public EventReaction(Consumer action, Predicate... predicates) { - this.action = action; + public EventHandler(Consumer action, Predicate... predicates) { + this(Collections.singletonList(action), predicates); + } + + @SafeVarargs + public EventHandler(List> actions, Predicate... predicates) { + this.actions = actions; for (Predicate predicate : predicates) { this.predicates.add(predicate); } @@ -26,7 +32,9 @@ public class EventReaction { } } if(matches) { - action.accept(evt); + for (Consumer action : actions) { + action.accept(evt); + } } } } diff --git a/common/src/main/java/ctbrec/event/EventHandlerConfiguration.java b/common/src/main/java/ctbrec/event/EventHandlerConfiguration.java new file mode 100644 index 00000000..8b2f3b15 --- /dev/null +++ b/common/src/main/java/ctbrec/event/EventHandlerConfiguration.java @@ -0,0 +1,89 @@ +package ctbrec.event; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class EventHandlerConfiguration { + + private String name; + private Event.Type event; + private List predicates = new ArrayList<>(); + private List actions = new ArrayList<>(); + + public Event.Type getEvent() { + return event; + } + + public void setEvent(Event.Type event) { + this.event = event; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getPredicates() { + return predicates; + } + + public void setPredicates(List predicates) { + this.predicates = predicates; + } + + public List getActions() { + return actions; + } + + public void setActions(List actions) { + this.actions = actions; + } + + public class PredicateConfiguration { + private String type; + private Map configuration = new HashMap<>(); + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Map getConfiguration() { + return configuration; + } + + public void setConfiguration(Map configuration) { + this.configuration = configuration; + } + + } + + public class ActionConfiguration { + private String type; + private Map configuration = new HashMap<>(); + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Map getConfiguration() { + return configuration; + } + + public void setConfiguration(Map configuration) { + this.configuration = configuration; + } + } +} diff --git a/common/src/main/java/ctbrec/event/TypePredicate.java b/common/src/main/java/ctbrec/event/EventTypePredicate.java similarity index 55% rename from common/src/main/java/ctbrec/event/TypePredicate.java rename to common/src/main/java/ctbrec/event/EventTypePredicate.java index e84be4df..63bb3965 100644 --- a/common/src/main/java/ctbrec/event/TypePredicate.java +++ b/common/src/main/java/ctbrec/event/EventTypePredicate.java @@ -4,11 +4,11 @@ import java.util.function.Predicate; import ctbrec.event.Event.Type; -public class TypePredicate implements Predicate { +public class EventTypePredicate implements Predicate { private Type type; - private TypePredicate(Type type) { + private EventTypePredicate(Type type) { this.type = type; } @@ -17,7 +17,7 @@ public class TypePredicate implements Predicate { return evt.getType() == type; } - public static TypePredicate of(Type type) { - return new TypePredicate(type); + public static EventTypePredicate of(Type type) { + return new EventTypePredicate(type); } } diff --git a/common/src/main/java/ctbrec/event/LogReaction.java b/common/src/main/java/ctbrec/event/LogReaction.java deleted file mode 100644 index ec5444f7..00000000 --- a/common/src/main/java/ctbrec/event/LogReaction.java +++ /dev/null @@ -1,15 +0,0 @@ -package ctbrec.event; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class LogReaction extends EventReaction { - - private static final transient Logger LOG = LoggerFactory.getLogger(LogReaction.class); - - public LogReaction() { - super(evt -> { - LOG.debug("LogReaction: {}", evt); - }, TypePredicate.of(Event.Type.RECORDING_STATUS_CHANGED)); - } -} diff --git a/common/src/main/java/ctbrec/event/ModelPredicate.java b/common/src/main/java/ctbrec/event/ModelPredicate.java new file mode 100644 index 00000000..e23c902c --- /dev/null +++ b/common/src/main/java/ctbrec/event/ModelPredicate.java @@ -0,0 +1,30 @@ +package ctbrec.event; + +import java.util.Objects; +import java.util.function.Predicate; + +import ctbrec.Model; + +public class ModelPredicate implements Predicate { + + private Model model; + + private ModelPredicate(Model model) { + this.model = model; + } + + @Override + public boolean test(Event evt) { + if(evt instanceof AbstractModelEvent) { + AbstractModelEvent modelEvent = (AbstractModelEvent) evt; + Model other = modelEvent.getModel(); + return Objects.equals(model, other); + } else { + return false; + } + } + + public static ModelPredicate of(Model model) { + return new ModelPredicate(model); + } +} diff --git a/common/src/main/java/ctbrec/event/ModelStatePredicate.java b/common/src/main/java/ctbrec/event/ModelStatePredicate.java new file mode 100644 index 00000000..3dad100e --- /dev/null +++ b/common/src/main/java/ctbrec/event/ModelStatePredicate.java @@ -0,0 +1,29 @@ +package ctbrec.event; + +import java.util.function.Predicate; + +import ctbrec.Model; + +public class ModelStatePredicate implements Predicate { + + private Model.State state; + + private ModelStatePredicate(Model.State state) { + this.state = state; + } + + @Override + public boolean test(Event evt) { + if(evt instanceof AbstractModelEvent) { + ModelStateChangedEvent modelEvent = (ModelStateChangedEvent) evt; + Model.State newState = modelEvent.getNewState(); + return newState == state; + } else { + return false; + } + } + + public static ModelStatePredicate of(Model.State state) { + return new ModelStatePredicate(state); + } +} From 4d55351919544aa8793a42194f8b1f258805f3ba Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 8 Dec 2018 15:34:50 +0100 Subject: [PATCH 145/231] Create controls for file, program and directory selection --- .../ui/controls/AbstractFileSelectionBox.java | 114 ++++++++++++++++++ .../ui/controls/DirectorySelectionBox.java | 17 +++ .../ctbrec/ui/controls/FileSelectionBox.java | 17 +++ .../ui/controls/ProgramSelectionBox.java | 17 +++ 4 files changed, 165 insertions(+) create mode 100644 client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java create mode 100644 client/src/main/java/ctbrec/ui/controls/DirectorySelectionBox.java create mode 100644 client/src/main/java/ctbrec/ui/controls/FileSelectionBox.java create mode 100644 client/src/main/java/ctbrec/ui/controls/ProgramSelectionBox.java diff --git a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java new file mode 100644 index 00000000..cc580ebc --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java @@ -0,0 +1,114 @@ +package ctbrec.ui.controls; + +import java.io.File; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.ui.AutosizeAlert; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ObjectPropertyBase; +import javafx.beans.value.ChangeListener; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.Border; +import javafx.scene.layout.BorderStroke; +import javafx.scene.layout.BorderStrokeStyle; +import javafx.scene.layout.BorderWidths; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.HBox; +import javafx.scene.paint.Color; +import javafx.stage.FileChooser; + +public abstract class AbstractFileSelectionBox extends HBox { + private static final transient Logger LOG = LoggerFactory.getLogger(AbstractFileSelectionBox.class); + + private ObjectProperty fileProperty = new ObjectPropertyBase() { + @Override + public Object getBean() { + return null; + } + + @Override + public String getName() { + return "file"; + } + }; + private TextField fileInput; + private Tooltip validationError = new Tooltip(); + + public AbstractFileSelectionBox() { + super(5); + fileInput = new TextField(); + fileInput.textProperty().addListener(textListener()); + fileInput.focusedProperty().addListener((obs, o, n) -> { + if(!n) { + validationError.hide(); + } + }); + Node browse = createBrowseButton(); + getChildren().addAll(fileInput, browse); + fileInput.disableProperty().bind(disableProperty()); + browse.disableProperty().bind(disableProperty()); + } + + private ChangeListener textListener() { + return (obs, o, n) -> { + String input = fileInput.getText(); + File program = new File(input); + setProgram(program); + }; + } + + private void setProgram(File program) { + String msg = validate(program); + if (msg != null) { + fileInput.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2)))); + validationError.setText(msg); + fileInput.setTooltip(validationError); + Point2D p = fileInput.localToScreen(fileInput.getTranslateY(), fileInput.getTranslateY()); + if(!validationError.isShowing()) { + validationError.show(getScene().getWindow(), p.getX(), p.getY() + fileInput.getHeight() + 4); + } + } else { + fileInput.setBorder(Border.EMPTY); + fileInput.setTooltip(null); + fileProperty.set(program); + validationError.hide(); + } + } + + protected String validate(File file) { + if (file == null || !file.exists()) { + return "File does not exist"; + } else { + return null; + } + } + + private Button createBrowseButton() { + Button button = new Button("Select"); + button.setOnAction((e) -> { + FileChooser chooser = new FileChooser(); + File program = chooser.showOpenDialog(null); + if(program != null) { + try { + fileInput.setText(program.getCanonicalPath()); + } catch (IOException e1) { + LOG.error("Couldn't determine path", e1); + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Whoopsie"); + alert.setContentText("Couldn't determine path"); + alert.showAndWait(); + } + setProgram(program); + } + }); + return button; + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/DirectorySelectionBox.java b/client/src/main/java/ctbrec/ui/controls/DirectorySelectionBox.java new file mode 100644 index 00000000..cb0758b8 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/DirectorySelectionBox.java @@ -0,0 +1,17 @@ +package ctbrec.ui.controls; + +import java.io.File; + +public class DirectorySelectionBox extends AbstractFileSelectionBox { + @Override + protected String validate(File file) { + String msg = super.validate(file); + if(msg != null) { + return msg; + } else if (!file.isDirectory()) { + return "This is not a directory"; + } else { + return null; + } + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/FileSelectionBox.java b/client/src/main/java/ctbrec/ui/controls/FileSelectionBox.java new file mode 100644 index 00000000..57b69cc5 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/FileSelectionBox.java @@ -0,0 +1,17 @@ +package ctbrec.ui.controls; + +import java.io.File; + +public class FileSelectionBox extends AbstractFileSelectionBox { + @Override + protected String validate(File file) { + String msg = super.validate(file); + if(msg != null) { + return msg; + } else if (!file.isFile()) { + return "This is not a regular file"; + } else { + return null; + } + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/ProgramSelectionBox.java b/client/src/main/java/ctbrec/ui/controls/ProgramSelectionBox.java new file mode 100644 index 00000000..23a2023f --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/ProgramSelectionBox.java @@ -0,0 +1,17 @@ +package ctbrec.ui.controls; + +import java.io.File; + +public class ProgramSelectionBox extends FileSelectionBox { + @Override + protected String validate(File file) { + String msg = super.validate(file); + if(msg != null) { + return msg; + } else if (!file.canExecute()) { + return "This is not an executable application"; + } else { + return null; + } + } +} From 016c95f7f1d003822f628167658a8f9f8d89c2c6 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 8 Dec 2018 16:06:17 +0100 Subject: [PATCH 146/231] Make file/dir selection boxes usable --- .../ui/controls/AbstractFileSelectionBox.java | 51 ++++++++++++------- .../ui/controls/DirectorySelectionBox.java | 20 ++++++++ .../ctbrec/ui/controls/FileSelectionBox.java | 7 +++ .../ui/controls/ProgramSelectionBox.java | 7 +++ 4 files changed, 66 insertions(+), 19 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java index cc580ebc..9d8d1fab 100644 --- a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java +++ b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java @@ -39,7 +39,7 @@ public abstract class AbstractFileSelectionBox extends HBox { return "file"; } }; - private TextField fileInput; + protected TextField fileInput; private Tooltip validationError = new Tooltip(); public AbstractFileSelectionBox() { @@ -57,16 +57,21 @@ public abstract class AbstractFileSelectionBox extends HBox { browse.disableProperty().bind(disableProperty()); } + public AbstractFileSelectionBox(String initialValue) { + this(); + fileInput.setText(initialValue); + } + private ChangeListener textListener() { return (obs, o, n) -> { String input = fileInput.getText(); File program = new File(input); - setProgram(program); + setFile(program); }; } - private void setProgram(File program) { - String msg = validate(program); + protected void setFile(File file) { + String msg = validate(file); if (msg != null) { fileInput.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2)))); validationError.setText(msg); @@ -78,7 +83,7 @@ public abstract class AbstractFileSelectionBox extends HBox { } else { fileInput.setBorder(Border.EMPTY); fileInput.setTooltip(null); - fileProperty.set(program); + fileProperty.set(file); validationError.hide(); } } @@ -94,21 +99,29 @@ public abstract class AbstractFileSelectionBox extends HBox { private Button createBrowseButton() { Button button = new Button("Select"); button.setOnAction((e) -> { - FileChooser chooser = new FileChooser(); - File program = chooser.showOpenDialog(null); - if(program != null) { - try { - fileInput.setText(program.getCanonicalPath()); - } catch (IOException e1) { - LOG.error("Couldn't determine path", e1); - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Whoopsie"); - alert.setContentText("Couldn't determine path"); - alert.showAndWait(); - } - setProgram(program); - } + choose(); }); return button; } + + protected void choose() { + FileChooser chooser = new FileChooser(); + File program = chooser.showOpenDialog(null); + if(program != null) { + try { + fileInput.setText(program.getCanonicalPath()); + } catch (IOException e1) { + LOG.error("Couldn't determine path", e1); + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Whoopsie"); + alert.setContentText("Couldn't determine path"); + alert.showAndWait(); + } + setFile(program); + } + } + + public ObjectProperty fileProperty() { + return fileProperty; + } } diff --git a/client/src/main/java/ctbrec/ui/controls/DirectorySelectionBox.java b/client/src/main/java/ctbrec/ui/controls/DirectorySelectionBox.java index cb0758b8..f3f1a5e5 100644 --- a/client/src/main/java/ctbrec/ui/controls/DirectorySelectionBox.java +++ b/client/src/main/java/ctbrec/ui/controls/DirectorySelectionBox.java @@ -2,7 +2,27 @@ package ctbrec.ui.controls; import java.io.File; +import javafx.stage.DirectoryChooser; + public class DirectorySelectionBox extends AbstractFileSelectionBox { + public DirectorySelectionBox(String dir) { + super(dir); + } + + @Override + protected void choose() { + DirectoryChooser chooser = new DirectoryChooser(); + File currentDir = fileProperty().get(); + if (currentDir.exists() && currentDir.isDirectory()) { + chooser.setInitialDirectory(currentDir); + } + File selectedDir = chooser.showDialog(null); + if(selectedDir != null) { + fileInput.setText(selectedDir.getAbsolutePath()); + setFile(selectedDir); + } + } + @Override protected String validate(File file) { String msg = super.validate(file); diff --git a/client/src/main/java/ctbrec/ui/controls/FileSelectionBox.java b/client/src/main/java/ctbrec/ui/controls/FileSelectionBox.java index 57b69cc5..c4f3dfe4 100644 --- a/client/src/main/java/ctbrec/ui/controls/FileSelectionBox.java +++ b/client/src/main/java/ctbrec/ui/controls/FileSelectionBox.java @@ -3,6 +3,13 @@ package ctbrec.ui.controls; import java.io.File; public class FileSelectionBox extends AbstractFileSelectionBox { + public FileSelectionBox() { + } + + public FileSelectionBox(String initialValue) { + super(initialValue); + } + @Override protected String validate(File file) { String msg = super.validate(file); diff --git a/client/src/main/java/ctbrec/ui/controls/ProgramSelectionBox.java b/client/src/main/java/ctbrec/ui/controls/ProgramSelectionBox.java index 23a2023f..1ed2b85e 100644 --- a/client/src/main/java/ctbrec/ui/controls/ProgramSelectionBox.java +++ b/client/src/main/java/ctbrec/ui/controls/ProgramSelectionBox.java @@ -3,6 +3,13 @@ package ctbrec.ui.controls; import java.io.File; public class ProgramSelectionBox extends FileSelectionBox { + public ProgramSelectionBox() { + } + + public ProgramSelectionBox(String initialValue) { + super(initialValue); + } + @Override protected String validate(File file) { String msg = super.validate(file); From 65f7c0d85ebdfe25112240eed5dddad2eb0a134e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 8 Dec 2018 16:08:00 +0100 Subject: [PATCH 147/231] Use file/dir selection boxes and remove post-processing setting --- .../java/ctbrec/ui/settings/SettingsTab.java | 233 ++---------------- 1 file changed, 24 insertions(+), 209 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index b31e9202..c20febde 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -2,11 +2,11 @@ package ctbrec.ui.settings; import static ctbrec.Settings.DirectoryStructure.*; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,18 +17,15 @@ import ctbrec.Settings.DirectoryStructure; import ctbrec.StringUtil; import ctbrec.sites.ConfigUI; import ctbrec.sites.Site; -import ctbrec.ui.AutosizeAlert; import ctbrec.ui.SiteUiFactory; import ctbrec.ui.TabSelectionListener; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; +import ctbrec.ui.controls.DirectorySelectionBox; +import ctbrec.ui.controls.ProgramSelectionBox; import javafx.collections.FXCollections; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.Accordion; -import javafx.scene.control.Alert; -import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; @@ -40,19 +37,12 @@ import javafx.scene.control.TextInputDialog; import javafx.scene.control.TitledPane; import javafx.scene.control.ToggleGroup; import javafx.scene.control.Tooltip; -import javafx.scene.layout.Border; -import javafx.scene.layout.BorderStroke; -import javafx.scene.layout.BorderStrokeStyle; -import javafx.scene.layout.BorderWidths; import javafx.scene.layout.ColumnConstraints; -import javafx.scene.layout.CornerRadii; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; -import javafx.scene.text.Font; -import javafx.stage.DirectoryChooser; -import javafx.stage.FileChooser;; +import javafx.scene.text.Font;; public class SettingsTab extends Tab implements TabSelectionListener { @@ -60,11 +50,8 @@ public class SettingsTab extends Tab implements TabSelectionListener { private static final int ONE_GiB_IN_BYTES = 1024 * 1024 * 1024; public static final int CHECKBOX_MARGIN = 6; - private TextField recordingsDirectory; - private Button recordingsDirectoryButton; - private Button postProcessingDirectoryButton; - private TextField mediaPlayer; - private TextField postProcessing; + private DirectorySelectionBox recordingsDirectory; + private ProgramSelectionBox mediaPlayer; private TextField server; private TextField port; private TextField onlineCheckIntervalInSecs; @@ -232,28 +219,22 @@ public class SettingsTab extends Tab implements TabSelectionListener { private Node createRecorderPanel() { int row = 0; GridPane layout = createGridLayout(); - layout.add(new Label("Post-Processing"), 0, row); - postProcessing = new TextField(Config.getInstance().getSettings().postProcessing); - postProcessing.focusedProperty().addListener(createPostProcessingFocusListener()); - GridPane.setFillWidth(postProcessing, true); - GridPane.setHgrow(postProcessing, Priority.ALWAYS); - GridPane.setColumnSpan(postProcessing, 2); - GridPane.setMargin(postProcessing, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(postProcessing, 1, row); - postProcessingDirectoryButton = createPostProcessingBrowseButton(); - layout.add(postProcessingDirectoryButton, 3, row++); layout.add(new Label("Recordings Directory"), 0, row); - recordingsDirectory = new TextField(Config.getInstance().getSettings().recordingsDir); - recordingsDirectory.focusedProperty().addListener(createRecordingsDirectoryFocusListener()); + recordingsDirectory = new DirectorySelectionBox(Config.getInstance().getSettings().recordingsDir); recordingsDirectory.setPrefWidth(400); + recordingsDirectory.fileProperty().addListener((obs, o, n) -> { + String path = n.getAbsolutePath(); + if(!Objects.equals(path, Config.getInstance().getSettings().recordingsDir)) { + Config.getInstance().getSettings().recordingsDir = path; + saveConfig(); + } + }); GridPane.setFillWidth(recordingsDirectory, true); GridPane.setHgrow(recordingsDirectory, Priority.ALWAYS); GridPane.setColumnSpan(recordingsDirectory, 2); GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(recordingsDirectory, 1, row); - recordingsDirectoryButton = createRecordingsBrowseButton(); - layout.add(recordingsDirectoryButton, 3, row++); + layout.add(recordingsDirectory, 1, row++); layout.add(new Label("Directory Structure"), 0, row); List options = new ArrayList<>(); @@ -363,14 +344,19 @@ public class SettingsTab extends Tab implements TabSelectionListener { int row = 0; layout.add(new Label("Player"), 0, row); - mediaPlayer = new TextField(Config.getInstance().getSettings().mediaPlayer); - mediaPlayer.focusedProperty().addListener(createMpvFocusListener()); + mediaPlayer = new ProgramSelectionBox(Config.getInstance().getSettings().mediaPlayer); + mediaPlayer.fileProperty().addListener((obs, o, n) -> { + String path = n.getAbsolutePath(); + if (!Objects.equals(path, Config.getInstance().getSettings().mediaPlayer)) { + Config.getInstance().getSettings().mediaPlayer = path; + saveConfig(); + } + }); GridPane.setFillWidth(mediaPlayer, true); GridPane.setHgrow(mediaPlayer, Priority.ALWAYS); GridPane.setColumnSpan(mediaPlayer, 2); GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(mediaPlayer, 1, row); - layout.add(createMpvBrowseButton(), 3, row++); + layout.add(mediaPlayer, 1, row++); Label l = new Label("Allow multiple players"); layout.add(l, 0, row); @@ -486,184 +472,13 @@ public class SettingsTab extends Tab implements TabSelectionListener { port.setDisable(local); secureCommunication.setDisable(local); recordingsDirectory.setDisable(!local); - recordingsDirectoryButton.setDisable(!local); splitAfter.setDisable(!local); maxResolution.setDisable(!local); - postProcessing.setDisable(!local); - postProcessingDirectoryButton.setDisable(!local); directoryStructure.setDisable(!local); onlineCheckIntervalInSecs.setDisable(!local); leaveSpaceOnDevice.setDisable(!local); } - private ChangeListener createRecordingsDirectoryFocusListener() { - return new ChangeListener() { - @Override - public void changed(ObservableValue arg0, Boolean oldPropertyValue, Boolean newPropertyValue) { - if (newPropertyValue) { - recordingsDirectory.setBorder(Border.EMPTY); - recordingsDirectory.setTooltip(null); - } else { - String input = recordingsDirectory.getText(); - File newDir = new File(input); - setRecordingsDir(newDir); - } - } - }; - } - - private ChangeListener createMpvFocusListener() { - return new ChangeListener() { - @Override - public void changed(ObservableValue arg0, Boolean oldPropertyValue, Boolean newPropertyValue) { - if (newPropertyValue) { - mediaPlayer.setBorder(Border.EMPTY); - mediaPlayer.setTooltip(null); - } else { - String input = mediaPlayer.getText(); - File program = new File(input); - setMpv(program); - } - } - }; - } - - private ChangeListener createPostProcessingFocusListener() { - return new ChangeListener() { - @Override - public void changed(ObservableValue arg0, Boolean oldPropertyValue, Boolean newPropertyValue) { - if (newPropertyValue) { - postProcessing.setBorder(Border.EMPTY); - postProcessing.setTooltip(null); - } else { - String input = postProcessing.getText(); - if(!input.trim().isEmpty()) { - File program = new File(input); - setPostProcessing(program); - } else { - Config.getInstance().getSettings().postProcessing = ""; - saveConfig(); - } - } - } - }; - } - - private void setMpv(File program) { - String msg = validateProgram(program); - if (msg != null) { - mediaPlayer.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2)))); - mediaPlayer.setTooltip(new Tooltip(msg)); - } else { - Config.getInstance().getSettings().mediaPlayer = mediaPlayer.getText(); - saveConfig(); - } - } - - private void setPostProcessing(File program) { - String msg = validateProgram(program); - if (msg != null) { - postProcessing.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2)))); - postProcessing.setTooltip(new Tooltip(msg)); - } else { - Config.getInstance().getSettings().postProcessing = postProcessing.getText(); - saveConfig(); - } - } - - private String validateProgram(File program) { - if (program == null || !program.exists()) { - return "File does not exist"; - } else if (!program.isFile() || !program.canExecute()) { - return "This is not an executable application"; - } - return null; - } - - private Button createRecordingsBrowseButton() { - Button button = new Button("Select"); - button.setOnAction((e) -> { - DirectoryChooser chooser = new DirectoryChooser(); - File currentDir = new File(Config.getInstance().getSettings().recordingsDir); - if (currentDir.exists() && currentDir.isDirectory()) { - chooser.setInitialDirectory(currentDir); - } - File selectedDir = chooser.showDialog(null); - if(selectedDir != null) { - setRecordingsDir(selectedDir); - } - }); - return button; - } - - private Node createMpvBrowseButton() { - Button button = new Button("Select"); - button.setOnAction((e) -> { - FileChooser chooser = new FileChooser(); - File program = chooser.showOpenDialog(null); - if(program != null) { - try { - mediaPlayer.setText(program.getCanonicalPath()); - } catch (IOException e1) { - LOG.error("Couldn't determine path", e1); - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Whoopsie"); - alert.setContentText("Couldn't determine path"); - alert.showAndWait(); - } - setMpv(program); - } - }); - return button; - } - - private Button createPostProcessingBrowseButton() { - Button button = new Button("Select"); - button.setOnAction((e) -> { - FileChooser chooser = new FileChooser(); - File program = chooser.showOpenDialog(null); - if(program != null) { - try { - postProcessing.setText(program.getCanonicalPath()); - } catch (IOException e1) { - LOG.error("Couldn't determine path", e1); - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Whoopsie"); - alert.setContentText("Couldn't determine path"); - alert.showAndWait(); - } - setPostProcessing(program); - } - }); - return button; - } - - private void setRecordingsDir(File dir) { - if (dir != null && dir.isDirectory()) { - try { - String path = dir.getCanonicalPath(); - Config.getInstance().getSettings().recordingsDir = path; - recordingsDirectory.setText(path); - saveConfig(); - } catch (IOException e1) { - LOG.error("Couldn't determine directory path", e1); - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Whoopsie"); - alert.setContentText("Couldn't determine directory path"); - alert.showAndWait(); - } - } else { - recordingsDirectory.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2)))); - if (!dir.isDirectory()) { - recordingsDirectory.setTooltip(new Tooltip("This is not a directory")); - } - if (!dir.exists()) { - recordingsDirectory.setTooltip(new Tooltip("Directory does not exist")); - } - - } - } - @Override public void selected() { if(startTab.getItems().isEmpty()) { From 5bb51b6a854e60d61b4c23bfc7ba4ef7388636dd Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 8 Dec 2018 16:08:14 +0100 Subject: [PATCH 148/231] Add descriptions for events and states --- common/src/main/java/ctbrec/Recording.java | 25 ++++++++++++++------ common/src/main/java/ctbrec/event/Event.java | 6 ++--- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/common/src/main/java/ctbrec/Recording.java b/common/src/main/java/ctbrec/Recording.java index e1417306..dadbe3c7 100644 --- a/common/src/main/java/ctbrec/Recording.java +++ b/common/src/main/java/ctbrec/Recording.java @@ -15,13 +15,24 @@ public class Recording { private long sizeInByte; public static enum State { - RECORDING, - GENERATING_PLAYLIST, - STOPPED, - FINISHED, - DOWNLOADING, - POST_PROCESSING, - UNKNOWN + RECORDING("recording"), + STOPPED("stopped"), + GENERATING_PLAYLIST("generating playlist"), + POST_PROCESSING("post-processing"), + FINISHED("finished"), + DOWNLOADING("downloading"), + UNKNOWN("unknown"); + + private String desc; + + State(String desc) { + this.desc = desc; + } + + @Override + public String toString() { + return desc; + } } public Recording() {} diff --git a/common/src/main/java/ctbrec/event/Event.java b/common/src/main/java/ctbrec/event/Event.java index de8393ee..9e67d032 100644 --- a/common/src/main/java/ctbrec/event/Event.java +++ b/common/src/main/java/ctbrec/event/Event.java @@ -7,18 +7,18 @@ public abstract class Event { * This event is fired every time the OnlineMonitor sees a model online * It is also fired, if the model was online before. You can see it as a "still online ping". */ - MODEL_ONLINE("Model is online"), + MODEL_ONLINE("model is online"), /** * This event is fired whenever the model's online state (Model.STATUS) changes. */ - MODEL_STATUS_CHANGED("Model status changed"), + MODEL_STATUS_CHANGED("model status changed"), /** * This event is fired whenever the state of a recording changes. */ - RECORDING_STATUS_CHANGED("Recording status changed"); + RECORDING_STATUS_CHANGED("recording status changed"); private String desc; From 509a9115fa1b1e5e7a35563d75815dfa27342b51 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 8 Dec 2018 16:49:47 +0100 Subject: [PATCH 149/231] Set user data directory for webengine --- client/src/main/java/ctbrec/ui/UpdateTab.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/UpdateTab.java b/client/src/main/java/ctbrec/ui/UpdateTab.java index c28c63a2..43eb3e61 100644 --- a/client/src/main/java/ctbrec/ui/UpdateTab.java +++ b/client/src/main/java/ctbrec/ui/UpdateTab.java @@ -3,6 +3,7 @@ package ctbrec.ui; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ctbrec.Config; import ctbrec.ui.CamrecApplication.Release; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -36,6 +37,7 @@ public class UpdateTab extends Tab { try { WebEngine webEngine = browser.getEngine(); webEngine.load("https://raw.githubusercontent.com/0xboobface/ctbrec/master/CHANGELOG.md"); + webEngine.setUserDataDirectory(Config.getInstance().getConfigDir()); vbox.getChildren().add(browser); VBox.setVgrow(browser, Priority.ALWAYS); } catch (Exception e) { From 9825383d0ca4a02c7f381ccb8310d919c7a2ed9c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 8 Dec 2018 17:13:39 +0100 Subject: [PATCH 150/231] Set textfield to grow horizontally --- .../main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java index 9d8d1fab..38a74ae8 100644 --- a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java +++ b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java @@ -22,6 +22,7 @@ import javafx.scene.layout.BorderStrokeStyle; import javafx.scene.layout.BorderWidths; import javafx.scene.layout.CornerRadii; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.paint.Color; import javafx.stage.FileChooser; @@ -55,6 +56,7 @@ public abstract class AbstractFileSelectionBox extends HBox { getChildren().addAll(fileInput, browse); fileInput.disableProperty().bind(disableProperty()); browse.disableProperty().bind(disableProperty()); + HBox.setHgrow(fileInput, Priority.ALWAYS); } public AbstractFileSelectionBox(String initialValue) { From a944117966485151f8c40ef2a1b84024729ab4fe Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 8 Dec 2018 17:13:58 +0100 Subject: [PATCH 151/231] Change layout of color settings panel --- .../ctbrec/ui/settings/ColorSettingsPane.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/settings/ColorSettingsPane.java b/client/src/main/java/ctbrec/ui/settings/ColorSettingsPane.java index fd3ac43e..3cb22604 100644 --- a/client/src/main/java/ctbrec/ui/settings/ColorSettingsPane.java +++ b/client/src/main/java/ctbrec/ui/settings/ColorSettingsPane.java @@ -3,28 +3,26 @@ package ctbrec.ui.settings; import ctbrec.Config; import javafx.scene.control.Button; import javafx.scene.control.ColorPicker; -import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; public class ColorSettingsPane extends Pane { - Label labelBaseColor = new Label("Base"); ColorPicker baseColor = new ColorPicker(); - Label labelAccentColor = new Label("Accent"); ColorPicker accentColor = new ColorPicker(); Button reset = new Button("Reset"); Pane foobar = new Pane(); public ColorSettingsPane(SettingsTab settingsTab) { - getChildren().add(labelBaseColor); getChildren().add(baseColor); - getChildren().add(labelAccentColor); getChildren().add(accentColor); getChildren().add(reset); baseColor.setValue(Color.web(Config.getInstance().getSettings().colorBase)); + baseColor.setTooltip(new Tooltip("Base Color")); accentColor.setValue(Color.web(Config.getInstance().getSettings().colorAccent)); + accentColor.setTooltip(new Tooltip("Accent Color")); baseColor.setOnAction(evt -> { Config.getInstance().getSettings().colorBase = toWeb(baseColor.getValue()); @@ -68,16 +66,12 @@ public class ColorSettingsPane extends Pane { @Override protected void layoutChildren() { - labelBaseColor.resize(32, 25); baseColor.resize(44, 25); - labelAccentColor.resize(46, 25); accentColor.resize(44, 25); reset.resize(60, 25); - labelBaseColor.setTranslateX(0); - baseColor.setTranslateX(labelBaseColor.getWidth() + 10); - labelAccentColor.setTranslateX(baseColor.getTranslateX() + baseColor.getWidth() + 15); - accentColor.setTranslateX(labelAccentColor.getTranslateX() + labelAccentColor.getWidth() + 10); - reset.setTranslateX(accentColor.getTranslateX() + accentColor.getWidth() + 50); + baseColor.setTranslateX(0); + accentColor.setTranslateX(baseColor.getTranslateX() + baseColor.getWidth() + 10); + reset.setTranslateX(accentColor.getTranslateX() + accentColor.getWidth() + 10); } } From 434001aafe274c18cfcdeb0bcb5c95c7d4917472 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 8 Dec 2018 17:14:25 +0100 Subject: [PATCH 152/231] Optimize settings tab layout --- .../java/ctbrec/ui/settings/SettingsTab.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index c20febde..f5f20dff 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -91,18 +91,26 @@ public class SettingsTab extends Tab implements TabSelectionListener { ColumnConstraints cc = new ColumnConstraints(); cc.setPercentWidth(50); mainLayout.getColumnConstraints().setAll(cc, cc); - setContent(new ScrollPane(mainLayout)); + ScrollPane scrollPane = new ScrollPane(mainLayout); + setContent(scrollPane); + GridPane.setFillHeight(scrollPane, true); + GridPane.setFillWidth(scrollPane, true); + GridPane.setHgrow(scrollPane, Priority.ALWAYS); + GridPane.setVgrow(scrollPane, Priority.ALWAYS); VBox leftSide = new VBox(15); + leftSide.setFillWidth(true); VBox rightSide = new VBox(15); + rightSide.setFillWidth(true); GridPane.setHgrow(leftSide, Priority.ALWAYS); GridPane.setHgrow(rightSide, Priority.ALWAYS); GridPane.setFillWidth(leftSide, true); GridPane.setFillWidth(rightSide, true); mainLayout.add(leftSide, 0, 1); mainLayout.add(rightSide, 1, 1); + mainLayout.prefWidthProperty().bind(scrollPane.widthProperty()); // restart info label - restartLabel = new Label("A restart is required to apply changes you made!"); + restartLabel = new Label("A restart is required to apply the changes you made!"); restartLabel.setVisible(false); restartLabel.setFont(Font.font(24)); restartLabel.setTextFill(Color.RED); @@ -222,7 +230,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { layout.add(new Label("Recordings Directory"), 0, row); recordingsDirectory = new DirectorySelectionBox(Config.getInstance().getSettings().recordingsDir); - recordingsDirectory.setPrefWidth(400); + recordingsDirectory.prefWidth(400); recordingsDirectory.fileProperty().addListener((obs, o, n) -> { String path = n.getAbsolutePath(); if(!Objects.equals(path, Config.getInstance().getSettings().recordingsDir)) { @@ -232,7 +240,6 @@ public class SettingsTab extends Tab implements TabSelectionListener { }); GridPane.setFillWidth(recordingsDirectory, true); GridPane.setHgrow(recordingsDirectory, Priority.ALWAYS); - GridPane.setColumnSpan(recordingsDirectory, 2); GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(recordingsDirectory, 1, row++); @@ -247,9 +254,9 @@ public class SettingsTab extends Tab implements TabSelectionListener { Config.getInstance().getSettings().recordingsDirStructure = directoryStructure.getValue(); saveConfig(); }); - GridPane.setColumnSpan(directoryStructure, 2); GridPane.setMargin(directoryStructure, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(directoryStructure, 1, row++); + recordingsDirectory.prefWidthProperty().bind(directoryStructure.widthProperty()); Label l = new Label("Maximum resolution (0 = unlimited)"); layout.add(l, 0, row); @@ -354,7 +361,6 @@ public class SettingsTab extends Tab implements TabSelectionListener { }); GridPane.setFillWidth(mediaPlayer, true); GridPane.setHgrow(mediaPlayer, Priority.ALWAYS); - GridPane.setColumnSpan(mediaPlayer, 2); GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(mediaPlayer, 1, row++); @@ -417,15 +423,15 @@ public class SettingsTab extends Tab implements TabSelectionListener { l = new Label("Start Tab"); layout.add(l, 0, row); startTab = new ComboBox<>(); - layout.add(startTab, 1, row++); startTab.setOnAction((e) -> { Config.getInstance().getSettings().startTab = startTab.getSelectionModel().getSelectedItem(); saveConfig(); }); + layout.add(startTab, 1, row++); GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(startTab, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - l = new Label("Colors"); + l = new Label("Colors (Base / Accent)"); layout.add(l, 0, row); ColorSettingsPane colorSettingsPane = new ColorSettingsPane(this); layout.add(colorSettingsPane, 1, row++); From be680a07f980c2d1ee66d006417becd9dfd7e4b7 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 9 Dec 2018 18:45:16 +0100 Subject: [PATCH 153/231] Map state password protected to private --- .../src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java index 73531926..e8322b8c 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java @@ -103,6 +103,7 @@ public class ChaturbateModel extends AbstractModel { break; case "private": case "hidden": + case "password protected": onlineState = PRIVATE; break; case "away": From 888046676f2125af5037f9dc8699545677840573 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 9 Dec 2018 18:46:33 +0100 Subject: [PATCH 154/231] Add first configurable version of the notification system --- .../java/ctbrec/ui/CamrecApplication.java | 53 +--- .../src/main/java/ctbrec/ui/JavaFxModel.java | 5 + .../main/java/ctbrec/ui/controls/Wizard.java | 34 ++- .../ui/event/ModelStateNotification.java | 23 -- .../main/java/ctbrec/ui/event/PlaySound.java | 10 + .../ctbrec/ui/event/ShowNotification.java | 42 +++ .../ui/settings/ActionSettingsPanel.java | 252 ++++++++++++++++-- .../ctbrec/ui/settings/ListSelectionPane.java | 121 +++++++++ .../java/ctbrec/ui/settings/SettingsTab.java | 7 +- .../src/main/java/ctbrec/AbstractModel.java | 8 + common/src/main/java/ctbrec/Model.java | 2 +- common/src/main/java/ctbrec/Settings.java | 5 +- common/src/main/java/ctbrec/event/Action.java | 9 + .../java/ctbrec/event/EventBusHolder.java | 31 ++- .../main/java/ctbrec/event/EventHandler.java | 125 ++++++++- .../event/EventHandlerConfiguration.java | 78 +++++- .../java/ctbrec/event/EventPredicate.java | 10 + .../java/ctbrec/event/EventTypePredicate.java | 15 +- .../java/ctbrec/event/ExecuteProgram.java | 53 ++++ .../java/ctbrec/event/ModelPredicate.java | 53 +++- .../ctbrec/event/ModelStatePredicate.java | 17 +- .../event/RecordingStateChangedEvent.java | 4 + .../ctbrec/event/RecordingStatePredicate.java | 31 +++ 23 files changed, 838 insertions(+), 150 deletions(-) delete mode 100644 client/src/main/java/ctbrec/ui/event/ModelStateNotification.java create mode 100644 client/src/main/java/ctbrec/ui/event/ShowNotification.java create mode 100644 client/src/main/java/ctbrec/ui/settings/ListSelectionPane.java create mode 100644 common/src/main/java/ctbrec/event/EventPredicate.java create mode 100644 common/src/main/java/ctbrec/event/ExecuteProgram.java create mode 100644 common/src/main/java/ctbrec/event/RecordingStatePredicate.java diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index df11cabc..40ca2431 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -1,9 +1,6 @@ package ctbrec.ui; -import static ctbrec.Model.State.*; -import static ctbrec.event.Event.Type.*; - import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; @@ -19,18 +16,16 @@ import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.eventbus.Subscribe; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import ctbrec.Config; -import ctbrec.Model; import ctbrec.StringUtil; import ctbrec.Version; -import ctbrec.event.Event; import ctbrec.event.EventBusHolder; -import ctbrec.event.ModelStateChangedEvent; +import ctbrec.event.EventHandler; +import ctbrec.event.EventHandlerConfiguration; import ctbrec.io.HttpClient; import ctbrec.recorder.LocalRecorder; import ctbrec.recorder.OnlineMonitor; @@ -62,7 +57,6 @@ public class CamrecApplication extends Application { static final transient Logger LOG = LoggerFactory.getLogger(CamrecApplication.class); - private Stage primaryStage; private Config config; private Recorder recorder; private OnlineMonitor onlineMonitor; @@ -75,15 +69,14 @@ public class CamrecApplication extends Application { @Override public void start(Stage primaryStage) throws Exception { - this.primaryStage = primaryStage; logEnvironment(); - registerAlertSystem(); sites.add(new BongaCams()); sites.add(new Cam4()); sites.add(new Camsoda()); sites.add(new Chaturbate()); sites.add(new MyFreeCams()); loadConfig(); + registerAlertSystem(); createHttpClient(); hostServices = getHostServices(); createRecorder(); @@ -101,7 +94,6 @@ public class CamrecApplication extends Application { } createGui(primaryStage); checkForUpdates(); - } private void logEnvironment() { @@ -134,7 +126,7 @@ public class CamrecApplication extends Application { rootPane.getTabs().add(modelsTab); RecordingsTab recordingsTab = new RecordingsTab("Recordings", recorder, config, sites); rootPane.getTabs().add(recordingsTab); - settingsTab = new SettingsTab(sites); + settingsTab = new SettingsTab(sites, recorder); rootPane.getTabs().add(settingsTab); rootPane.getTabs().add(new DonateTabFx()); @@ -222,38 +214,11 @@ public class CamrecApplication extends Application { // } catch (InterruptedException e) { // e.printStackTrace(); // } - EventBusHolder.BUS.register(new Object() { - @Subscribe - public void modelEvent(Event e) { - try { - if (e.getType() == MODEL_STATUS_CHANGED) { - ModelStateChangedEvent evt = (ModelStateChangedEvent) e; - Model model = evt.getModel(); - if (evt.getNewState() == ONLINE && primaryStage != null && primaryStage.getTitle() != null) { - String header = "Model Online"; - String msg = model.getDisplayName() + " is now online"; - LOG.debug(msg); - //OS.notification(primaryStage.getTitle(), header, msg); - } - } - } catch (Exception e1) { - LOG.error("Couldn't show notification", e1); - } - } - }); - - // EventBusHolder.BUS.register(new Object() { - // URL url = CamrecApplication.class.getResource("/Oxygen-Im-Highlight-Msg.mp3"); - // PlaySound playSound = new PlaySound(url); - // EventHandler reaction = new EventHandler(playSound); - // // LogReaction reaction = new LogReaction(); - // @Subscribe - // public void modelEvent(Event e) { - // reaction.reactToEvent(e); - // } - // }); - - + for (EventHandlerConfiguration config : Config.getInstance().getSettings().eventHandlers) { + EventHandler handler = new EventHandler(config); + EventBusHolder.register(handler); + LOG.debug("Registered event handler for {} {}", config.getEvent(), config.getName()); + } LOG.debug("Alert System registered"); } diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java index 63193d69..0f9019d4 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java @@ -207,4 +207,9 @@ public class JavaFxModel implements Model { public void setDisplayName(String name) { delegate.setDisplayName(name); } + + @Override + public int compareTo(Model o) { + return delegate.compareTo(o); + } } diff --git a/client/src/main/java/ctbrec/ui/controls/Wizard.java b/client/src/main/java/ctbrec/ui/controls/Wizard.java index bc653e2e..c065bb06 100644 --- a/client/src/main/java/ctbrec/ui/controls/Wizard.java +++ b/client/src/main/java/ctbrec/ui/controls/Wizard.java @@ -1,5 +1,8 @@ package ctbrec.ui.controls; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -11,6 +14,7 @@ import javafx.stage.Stage; public class Wizard extends BorderPane { + private static final transient Logger LOG = LoggerFactory.getLogger(Wizard.class); private Pane[] pages; private StackPane stack; private Stage stage; @@ -18,6 +22,7 @@ public class Wizard extends BorderPane { private Button next; private Button prev; private Button finish; + private boolean cancelled = true; public Wizard(Stage stage, Pane... pages) { this.stage = stage; @@ -33,42 +38,55 @@ public class Wizard extends BorderPane { private void createUi() { stack = new StackPane(); setCenter(stack); - + next = new Button("Next"); next.setOnAction(evt -> nextPage()); - prev = new Button("Prev"); + prev = new Button("Back"); prev.setOnAction(evt -> prevPage()); + prev.visibleProperty().bind(next.visibleProperty()); + next.setVisible(pages.length > 1); Button cancel = new Button("Cancel"); cancel.setOnAction(evt -> stage.close()); finish = new Button("Finish"); + finish.setOnAction(evt -> { + cancelled = false; + stage.close(); + }); HBox buttons = new HBox(5, prev, next, cancel, finish); buttons.setAlignment(Pos.BASELINE_RIGHT); setBottom(buttons); - BorderPane.setMargin(buttons, new Insets(5)); + BorderPane.setMargin(buttons, new Insets(10)); if (pages.length != 0) { - stack.getChildren().add(pages[0]); + prevPage(); } - setButtonStates(); } private void prevPage() { page = Math.max(0, --page); stack.getChildren().clear(); stack.getChildren().add(pages[page]); - setButtonStates(); + updateState(); } private void nextPage() { page = Math.min(pages.length - 1, ++page); stack.getChildren().clear(); stack.getChildren().add(pages[page]); - setButtonStates(); + updateState(); } - private void setButtonStates() { + private void updateState() { prev.setDisable(page == 0); next.setDisable(page == pages.length - 1); finish.setDisable(page != pages.length - 1); + LOG.debug("Setting border"); + pages[page].setStyle( + "-fx-background-color: -fx-inner-border, -fx-background;"+ + "-fx-background-insets: 0 0 -1 0, 0, 1, 2;"); + } + + public boolean isCancelled() { + return cancelled; } } diff --git a/client/src/main/java/ctbrec/ui/event/ModelStateNotification.java b/client/src/main/java/ctbrec/ui/event/ModelStateNotification.java deleted file mode 100644 index a831b4e3..00000000 --- a/client/src/main/java/ctbrec/ui/event/ModelStateNotification.java +++ /dev/null @@ -1,23 +0,0 @@ -package ctbrec.ui.event; - -import ctbrec.OS; -import ctbrec.event.Action; -import ctbrec.event.Event; -import ctbrec.ui.CamrecApplication; - -public class ModelStateNotification extends Action { - - private String header; - private String msg; - - public ModelStateNotification(String header, String msg) { - this.header = header; - this.msg = msg; - name = "show notification"; - } - - @Override - public void accept(Event evt) { - OS.notification(CamrecApplication.title, header, msg); - } -} diff --git a/client/src/main/java/ctbrec/ui/event/PlaySound.java b/client/src/main/java/ctbrec/ui/event/PlaySound.java index 7464bebb..c23d77cd 100644 --- a/client/src/main/java/ctbrec/ui/event/PlaySound.java +++ b/client/src/main/java/ctbrec/ui/event/PlaySound.java @@ -1,15 +1,19 @@ package ctbrec.ui.event; +import java.io.File; import java.net.URL; import ctbrec.event.Action; import ctbrec.event.Event; +import ctbrec.event.EventHandlerConfiguration.ActionConfiguration; import javafx.scene.media.AudioClip; public class PlaySound extends Action { private URL url; + public PlaySound() {} + public PlaySound(URL url) { this.url = url; name = "play sound"; @@ -20,4 +24,10 @@ public class PlaySound extends Action { AudioClip clip = new AudioClip(url.toString()); clip.play(); } + + @Override + public void configure(ActionConfiguration config) throws Exception { + File file = new File((String) config.getConfiguration().get("file")); + url = file.toURI().toURL(); + } } diff --git a/client/src/main/java/ctbrec/ui/event/ShowNotification.java b/client/src/main/java/ctbrec/ui/event/ShowNotification.java new file mode 100644 index 00000000..4d91350d --- /dev/null +++ b/client/src/main/java/ctbrec/ui/event/ShowNotification.java @@ -0,0 +1,42 @@ +package ctbrec.ui.event; + +import ctbrec.Model; +import ctbrec.OS; +import ctbrec.event.Action; +import ctbrec.event.Event; +import ctbrec.event.EventHandlerConfiguration.ActionConfiguration; +import ctbrec.event.ModelStateChangedEvent; +import ctbrec.event.RecordingStateChangedEvent; +import ctbrec.ui.CamrecApplication; + +public class ShowNotification extends Action { + + public ShowNotification() { + name = "show notification"; + } + + @Override + public void accept(Event evt) { + String header = evt.getType().toString(); + String msg; + switch(evt.getType()) { + case MODEL_STATUS_CHANGED: + ModelStateChangedEvent modelEvent = (ModelStateChangedEvent) evt; + Model m = modelEvent.getModel(); + msg = m.getDisplayName() + " is now " + modelEvent.getNewState().toString(); + break; + case RECORDING_STATUS_CHANGED: + RecordingStateChangedEvent recEvent = (RecordingStateChangedEvent) evt; + m = recEvent.getModel(); + msg = "Recording for model " + m.getDisplayName() + " is now in state " + recEvent.getState().toString(); + break; + default: + msg = evt.getDescription(); + } + OS.notification(CamrecApplication.title, header, msg); + } + + @Override + public void configure(ActionConfiguration config) throws Exception { + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java index 250bce86..a143d181 100644 --- a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java +++ b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java @@ -1,35 +1,95 @@ package ctbrec.ui.settings; +import java.io.File; import java.io.InputStream; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.OS; +import ctbrec.Recording; +import ctbrec.event.Event; +import ctbrec.event.EventBusHolder; +import ctbrec.event.EventHandler; import ctbrec.event.EventHandlerConfiguration; +import ctbrec.event.EventHandlerConfiguration.ActionConfiguration; +import ctbrec.event.EventHandlerConfiguration.PredicateConfiguration; +import ctbrec.event.ExecuteProgram; +import ctbrec.event.ModelPredicate; +import ctbrec.event.ModelStatePredicate; +import ctbrec.event.RecordingStatePredicate; +import ctbrec.recorder.Recorder; +import ctbrec.ui.CamrecApplication; +import ctbrec.ui.controls.FileSelectionBox; +import ctbrec.ui.controls.ProgramSelectionBox; import ctbrec.ui.controls.Wizard; +import ctbrec.ui.event.PlaySound; +import ctbrec.ui.event.ShowNotification; +import javafx.collections.ListChangeListener; import javafx.event.ActionEvent; import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.VPos; +import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; import javafx.scene.control.Label; +import javafx.scene.control.ListView; import javafx.scene.control.ScrollPane; -import javafx.scene.control.TableView; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.Separator; import javafx.scene.control.TextField; import javafx.scene.control.TitledPane; import javafx.scene.image.Image; -import javafx.scene.layout.Border; import javafx.scene.layout.BorderPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.stage.Modality; import javafx.stage.Stage; +import javafx.stage.Window; public class ActionSettingsPanel extends TitledPane { + private static final transient Logger LOG = LoggerFactory.getLogger(ActionSettingsPanel.class); + private ListView actionTable; - private TableView actionTable; + private TextField name = new TextField(); + private ComboBox event = new ComboBox<>(); + private ComboBox modelState = new ComboBox<>(); + private ComboBox recordingState = new ComboBox<>(); - public ActionSettingsPanel(SettingsTab settingsTab) { + private CheckBox playSound = new CheckBox("Play sound"); + private FileSelectionBox sound = new FileSelectionBox(); + private CheckBox showNotification = new CheckBox("Notify me"); + private Button testNotification = new Button("Test"); + private CheckBox executeProgram = new CheckBox("Execute program"); + private ProgramSelectionBox program = new ProgramSelectionBox(); + private ListSelectionPane modelSelectionPane; + + private Recorder recorder; + + public ActionSettingsPanel(SettingsTab settingsTab, Recorder recorder) { + this.recorder = recorder; setText("Events & Actions"); setExpanded(true); setCollapsible(false); createGui(); + loadEventHandlers(); + } + + private void loadEventHandlers() { + actionTable.getItems().addAll(Config.getInstance().getSettings().eventHandlers); } private void createGui() { @@ -37,58 +97,202 @@ public class ActionSettingsPanel extends TitledPane { setContent(mainLayout); actionTable = createActionTable(); - actionTable.setPrefSize(300, 200); ScrollPane scrollPane = new ScrollPane(actionTable); scrollPane.setFitToHeight(true); scrollPane.setFitToWidth(true); - scrollPane.setBorder(Border.EMPTY); + scrollPane.setStyle("-fx-background-color: -fx-background"); mainLayout.setCenter(scrollPane); - BorderPane.setMargin(scrollPane, new Insets(5)); Button add = new Button("Add"); add.setOnAction(this::add); Button delete = new Button("Delete"); delete.setOnAction(this::delete); delete.setDisable(true); - HBox buttons = new HBox(10, add, delete); + HBox buttons = new HBox(5, add, delete); mainLayout.setBottom(buttons); - BorderPane.setMargin(buttons, new Insets(5)); + BorderPane.setMargin(buttons, new Insets(5, 0, 0, 0)); + + actionTable.getSelectionModel().getSelectedItems().addListener(new ListChangeListener() { + @Override + public void onChanged(Change change) { + delete.setDisable(change.getList().isEmpty()); + } + }); } private void add(ActionEvent evt) { - EventHandlerConfiguration config = new EventHandlerConfiguration(); - Pane namePane = createNamePane(config); - GridPane pane2 = SettingsTab.createGridLayout(); - pane2.add(new Label("Pane 2"), 0, 0); - GridPane pane3 = SettingsTab.createGridLayout(); - pane3.add(new Label("Pane 3"), 0, 0); + Pane actionPane = createActionPane(); Stage dialog = new Stage(); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.initOwner(getScene().getWindow()); dialog.setTitle("New Action"); InputStream icon = getClass().getResourceAsStream("/icon.png"); dialog.getIcons().add(new Image(icon)); - Wizard root = new Wizard(dialog, namePane, pane2, pane3); - Scene scene = new Scene(root, 640, 480); + Wizard root = new Wizard(dialog, actionPane); + Scene scene = new Scene(root, 800, 540); scene.getStylesheets().addAll(getScene().getStylesheets()); dialog.setScene(scene); + centerOnParent(dialog); dialog.showAndWait(); + if(!root.isCancelled()) { + createEventHandler(); + } + } + + private void createEventHandler() { + EventHandlerConfiguration config = new EventHandlerConfiguration(); + config.setName(name.getText()); + config.setEvent(event.getValue()); + if(event.getValue() == Event.Type.MODEL_STATUS_CHANGED) { + PredicateConfiguration pc = new PredicateConfiguration(); + pc.setType(ModelStatePredicate.class.getName()); + pc.getConfiguration().put("state", modelState.getValue().name()); + pc.setName("state = " + modelState.getValue().toString()); + config.getPredicates().add(pc); + } else if(event.getValue() == Event.Type.RECORDING_STATUS_CHANGED) { + PredicateConfiguration pc = new PredicateConfiguration(); + pc.setType(RecordingStatePredicate.class.getName()); + pc.getConfiguration().put("state", recordingState.getValue().name()); + pc.setName("state = " + recordingState.getValue().toString()); + config.getPredicates().add(pc); + } + if(!modelSelectionPane.isAllSelected()) { + PredicateConfiguration pc = new PredicateConfiguration(); + pc.setType(ModelPredicate.class.getName()); + pc.setModels(modelSelectionPane.getSelectedItems()); + pc.setName("model is one of:" + modelSelectionPane.getSelectedItems()); + config.getPredicates().add(pc); + } + if(showNotification.isSelected()) { + ActionConfiguration ac = new ActionConfiguration(); + ac.setType(ShowNotification.class.getName()); + ac.setName("show notification"); + config.getActions().add(ac); + } + if(playSound.isSelected()) { + ActionConfiguration ac = new ActionConfiguration(); + ac.setType(PlaySound.class.getName()); + File file = sound.fileProperty().get(); + ac.getConfiguration().put("file", file.getAbsolutePath()); + ac.setName("play " + file.getName()); + config.getActions().add(ac); + } + if(executeProgram.isSelected()) { + ActionConfiguration ac = new ActionConfiguration(); + ac.setType(ExecuteProgram.class.getName()); + File file = program.fileProperty().get(); + ac.getConfiguration().put("file", file.getAbsolutePath()); + ac.setName("execute " + file.getName()); + config.getActions().add(ac); + } + + EventHandler handler = new EventHandler(config); + EventBusHolder.register(handler); + Config.getInstance().getSettings().eventHandlers.add(config); + actionTable.getItems().add(config); + LOG.debug("Registered event handler for {} {}", config.getEvent(), config.getName()); } private void delete(ActionEvent evt) { - + List selected = new ArrayList<>(actionTable.getSelectionModel().getSelectedItems()); + for (EventHandlerConfiguration config : selected) { + EventBusHolder.unregister(config.getId()); + Config.getInstance().getSettings().eventHandlers.remove(config); + actionTable.getItems().remove(config); + } } - private Pane createNamePane(EventHandlerConfiguration config) { + private Pane createActionPane() { GridPane layout = SettingsTab.createGridLayout(); + recordingState.prefWidthProperty().bind(event.widthProperty()); + modelState.prefWidthProperty().bind(event.widthProperty()); + name.prefWidthProperty().bind(event.widthProperty()); + int row = 0; layout.add(new Label("Name"), 0, row); - TextField name = new TextField(); - layout.add(name, 1, row); + layout.add(name, 1, row++); + + layout.add(new Label("Event"), 0, row); + event.getItems().add(Event.Type.MODEL_STATUS_CHANGED); + event.getItems().add(Event.Type.RECORDING_STATUS_CHANGED); + event.setOnAction(evt -> { + modelState.setVisible(event.getSelectionModel().getSelectedItem() == Event.Type.MODEL_STATUS_CHANGED); + }); + event.getSelectionModel().select(Event.Type.MODEL_STATUS_CHANGED); + layout.add(event, 1, row++); + + layout.add(new Label("State"), 0, row); + modelState.getItems().addAll(Model.State.values()); + layout.add(modelState, 1, row); + recordingState.getItems().addAll(Recording.State.values()); + layout.add(recordingState, 1, row++); + recordingState.visibleProperty().bind(modelState.visibleProperty().not()); + + layout.add(createSeparator(), 0, row++); + + Label l = new Label("Models"); + layout.add(l, 0, row); + modelSelectionPane = new ListSelectionPane(recorder.getModelsRecording(), Collections.emptyList()); + layout.add(modelSelectionPane, 1, row++); + GridPane.setValignment(l, VPos.TOP); + GridPane.setHgrow(modelSelectionPane, Priority.ALWAYS); + GridPane.setFillWidth(modelSelectionPane, true); + + layout.add(createSeparator(), 0, row++); + + layout.add(showNotification, 0, row); + layout.add(testNotification, 1, row++); + testNotification.setOnAction(evt -> { + DateTimeFormatter format = DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM); + ZonedDateTime time = ZonedDateTime.now(); + OS.notification(CamrecApplication.title, "Test Notification", "Oi, what's up! " + format.format(time)); + }); + testNotification.disableProperty().bind(showNotification.selectedProperty().not()); + + layout.add(playSound, 0, row); + layout.add(sound, 1, row++); + sound.disableProperty().bind(playSound.selectedProperty().not()); + + layout.add(executeProgram, 0, row); + layout.add(program, 1, row); + program.disableProperty().bind(executeProgram.selectedProperty().not()); + + GridPane.setFillWidth(name, true); + GridPane.setHgrow(name, Priority.ALWAYS); + GridPane.setFillWidth(sound, true); + return layout; } - - private TableView createActionTable() { - TableView view = new TableView(); + private ListView createActionTable() { + ListView view = new ListView<>(); + view.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + view.setPrefSize(300, 200); return view; } + + private Node createSeparator() { + Separator divider = new Separator(Orientation.HORIZONTAL); + GridPane.setHgrow(divider, Priority.ALWAYS); + GridPane.setFillWidth(divider, true); + GridPane.setColumnSpan(divider, 2); + int tb = 20; + int lr = 0; + GridPane.setMargin(divider, new Insets(tb, lr, tb, lr)); + return divider; + } + + private void centerOnParent(Stage dialog) { + dialog.setWidth(dialog.getScene().getWidth()); + dialog.setHeight(dialog.getScene().getHeight()); + double w = dialog.getWidth(); + double h = dialog.getHeight(); + Window p = dialog.getOwner(); + double px = p.getX(); + double py = p.getY(); + double pw = p.getWidth(); + double ph = p.getHeight(); + dialog.setX(px + (pw - w) / 2); + dialog.setY(py + (ph - h) / 2); + } } diff --git a/client/src/main/java/ctbrec/ui/settings/ListSelectionPane.java b/client/src/main/java/ctbrec/ui/settings/ListSelectionPane.java new file mode 100644 index 00000000..e5cd9dc1 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/ListSelectionPane.java @@ -0,0 +1,121 @@ +package ctbrec.ui.settings; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.control.SelectionMode; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +public class ListSelectionPane> extends GridPane { + + private ListView availableListView = new ListView<>(); + private ListView selectedListView = new ListView<>(); + private Button addModel = new Button(">"); + private Button removeModel = new Button("<"); + private CheckBox selectAll = new CheckBox("all"); + + public ListSelectionPane(List available, List selected) { + super(); + setHgap(5); + setVgap(5); + + createGui(); + fillLists(available, selected); + } + + private void fillLists(List available, List selected) { + ObservableList obsAvail = FXCollections.observableArrayList(available); + ObservableList obsSel = FXCollections.observableArrayList(selected); + for (Iterator iterator = obsAvail.iterator(); iterator.hasNext();) { + T t = iterator.next(); + if(obsSel.contains(t)) { + iterator.remove(); + } + } + Collections.sort(obsAvail); + Collections.sort(obsSel); + availableListView.setItems(obsAvail); + selectedListView.setItems(obsSel); + availableListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + selectedListView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + } + + private void createGui() { + Label labelAvailable = new Label("Available"); + Label labelSelected = new Label("Selected"); + + add(labelAvailable, 0, 0); + add(availableListView, 0, 1); + + VBox buttonBox = new VBox(5); + buttonBox.getChildren().add(addModel); + buttonBox.getChildren().add(removeModel); + buttonBox.setAlignment(Pos.CENTER); + add(buttonBox, 1, 1); + + add(labelSelected, 2, 0); + add(selectedListView, 2, 1); + + add(selectAll, 0, 2); + + GridPane.setHgrow(availableListView, Priority.ALWAYS); + GridPane.setHgrow(selectedListView, Priority.ALWAYS); + GridPane.setFillWidth(availableListView, true); + GridPane.setFillWidth(selectedListView, true); + + addModel.setOnAction(evt -> addSelectedItems()); + removeModel.setOnAction(evt -> removeSelectedItems()); + + availableListView.disableProperty().bind(selectAll.selectedProperty()); + selectedListView.disableProperty().bind(selectAll.selectedProperty()); + addModel.disableProperty().bind(selectAll.selectedProperty()); + removeModel.disableProperty().bind(selectAll.selectedProperty()); + } + + private void addSelectedItems() { + List selected = new ArrayList<>(availableListView.getSelectionModel().getSelectedItems()); + for (T t : selected) { + if(!selectedListView.getItems().contains(t)) { + selectedListView.getItems().add(t); + availableListView.getItems().remove(t); + } + } + Collections.sort(selectedListView.getItems()); + } + + private void removeSelectedItems() { + List selected = new ArrayList<>(selectedListView.getSelectionModel().getSelectedItems()); + for (T t : selected) { + if(!availableListView.getItems().contains(t)) { + availableListView.getItems().add(t); + selectedListView.getItems().remove(t); + } + } + Collections.sort(availableListView.getItems()); + } + + public List getSelectedItems() { + if(selectAll.isSelected()) { + List all = new ArrayList<>(availableListView.getItems()); + all.addAll(selectedListView.getItems()); + return all; + } else { + return selectedListView.getItems(); + } + } + + public boolean isAllSelected() { + return selectAll.isSelected(); + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index f5f20dff..b66c470f 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -15,6 +15,7 @@ import ctbrec.Config; import ctbrec.Hmac; import ctbrec.Settings.DirectoryStructure; import ctbrec.StringUtil; +import ctbrec.recorder.Recorder; import ctbrec.sites.ConfigUI; import ctbrec.sites.Site; import ctbrec.ui.SiteUiFactory; @@ -73,9 +74,11 @@ public class SettingsTab extends Tab implements TabSelectionListener { private List sites; private Label restartLabel; private Accordion siteConfigAccordion = new Accordion(); + private Recorder recorder; - public SettingsTab(List sites) { + public SettingsTab(List sites, Recorder recorder) { this.sites = sites; + this.recorder = recorder; setText("Settings"); createGui(); setClosable(false); @@ -125,7 +128,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { //right side rightSide.getChildren().add(siteConfigAccordion); - ActionSettingsPanel actions = new ActionSettingsPanel(this); + ActionSettingsPanel actions = new ActionSettingsPanel(this, recorder); rightSide.getChildren().add(actions); proxySettingsPane = new ProxySettingsPane(this); rightSide.getChildren().add(proxySettingsPane); diff --git a/common/src/main/java/ctbrec/AbstractModel.java b/common/src/main/java/ctbrec/AbstractModel.java index cd9803e7..53198b05 100644 --- a/common/src/main/java/ctbrec/AbstractModel.java +++ b/common/src/main/java/ctbrec/AbstractModel.java @@ -3,6 +3,7 @@ package ctbrec; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.ExecutionException; import com.squareup.moshi.JsonReader; @@ -162,6 +163,13 @@ public abstract class AbstractModel implements Model { return true; } + @Override + public int compareTo(Model o) { + String thisName = Optional.ofNullable(getDisplayName()).orElse("").toLowerCase(); + String otherName = Optional.ofNullable(o).map(m -> m.getDisplayName()).orElse("").toLowerCase(); + return thisName.compareTo(otherName); + } + @Override public String toString() { return getName(); diff --git a/common/src/main/java/ctbrec/Model.java b/common/src/main/java/ctbrec/Model.java index 77127903..feb73817 100644 --- a/common/src/main/java/ctbrec/Model.java +++ b/common/src/main/java/ctbrec/Model.java @@ -12,7 +12,7 @@ import com.squareup.moshi.JsonWriter; import ctbrec.recorder.download.StreamSource; import ctbrec.sites.Site; -public interface Model { +public interface Model extends Comparable { public static enum State { ONLINE("online"), diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index bb19eb34..46998661 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -4,6 +4,8 @@ import java.io.File; import java.util.ArrayList; import java.util.List; +import ctbrec.event.EventHandlerConfiguration; + public class Settings { public enum ProxyType { @@ -56,7 +58,8 @@ public class Settings { public String cam4Password; public String lastDownloadDir = ""; - public List models = new ArrayList(); + public List models = new ArrayList<>(); + public List eventHandlers = new ArrayList<>(); public boolean determineResolution = false; public boolean requireAuthentication = false; public boolean chooseStreamQuality = false; diff --git a/common/src/main/java/ctbrec/event/Action.java b/common/src/main/java/ctbrec/event/Action.java index a71b99af..67ccf08e 100644 --- a/common/src/main/java/ctbrec/event/Action.java +++ b/common/src/main/java/ctbrec/event/Action.java @@ -2,6 +2,8 @@ package ctbrec.event; import java.util.function.Consumer; +import ctbrec.event.EventHandlerConfiguration.ActionConfiguration; + public abstract class Action implements Consumer { protected String name; @@ -13,4 +15,11 @@ public abstract class Action implements Consumer { public void setName(String name) { this.name = name; } + + public abstract void configure(ActionConfiguration config) throws Exception; + + @Override + public String toString() { + return name; + } } diff --git a/common/src/main/java/ctbrec/event/EventBusHolder.java b/common/src/main/java/ctbrec/event/EventBusHolder.java index e848c892..37d7b278 100644 --- a/common/src/main/java/ctbrec/event/EventBusHolder.java +++ b/common/src/main/java/ctbrec/event/EventBusHolder.java @@ -1,16 +1,37 @@ package ctbrec.event; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.Executors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.google.common.eventbus.AsyncEventBus; import com.google.common.eventbus.EventBus; public class EventBusHolder { - - public static final String EVENT = "event"; - public static final String OLD = "old"; - public static final String STATUS = "status"; - public static final String MODEL = "model"; + private static final transient Logger LOG = LoggerFactory.getLogger(EventBusHolder.class); + private static Map handlers = new HashMap<>(); public static final EventBus BUS = new AsyncEventBus(Executors.newSingleThreadExecutor()); + + public static void register(EventHandler handler) { + if(handlers.containsKey(handler.getId())) { + LOG.warn("EventHandler with ID {} is already registered", handler.getId()); + } else { + BUS.register(handler); + handlers.put(handler.getId(), handler); + LOG.debug("EventHandler with ID {} has been added", handler.getId()); + } + } + + public static void unregister(String id) { + EventHandler handler = handlers.get(id); + if(handler != null) { + BUS.unregister(handler); + handlers.remove(id); + LOG.debug("EventHandler with ID {} has been removed", id); + } + } } diff --git a/common/src/main/java/ctbrec/event/EventHandler.java b/common/src/main/java/ctbrec/event/EventHandler.java index 6621a2d8..4afd09a2 100644 --- a/common/src/main/java/ctbrec/event/EventHandler.java +++ b/common/src/main/java/ctbrec/event/EventHandler.java @@ -6,35 +6,132 @@ import java.util.List; import java.util.function.Consumer; import java.util.function.Predicate; -public class EventHandler { +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; - private List> predicates = new ArrayList<>(); - private List> actions; +import com.google.common.eventbus.Subscribe; + +import ctbrec.event.Event.Type; +import ctbrec.event.EventHandlerConfiguration.ActionConfiguration; +import ctbrec.event.EventHandlerConfiguration.PredicateConfiguration; + +public class EventHandler { + private static final transient Logger LOG = LoggerFactory.getLogger(EventHandler.class); + + private List predicates = new ArrayList<>(); + private List actions; + private Type event; + private String id; + + public EventHandler(EventHandlerConfiguration config) { + id = config.getId(); + event = config.getEvent(); + actions = createActions(config); + predicates = createPredicates(config); + predicates.add(new EventTypePredicate(event)); + } + + public String getId() { + return id; + } @SafeVarargs - public EventHandler(Consumer action, Predicate... predicates) { + public EventHandler(Action action, EventPredicate... predicates) { this(Collections.singletonList(action), predicates); } @SafeVarargs - public EventHandler(List> actions, Predicate... predicates) { + public EventHandler(List actions, EventPredicate... predicates) { this.actions = actions; - for (Predicate predicate : predicates) { + for (EventPredicate predicate : predicates) { this.predicates.add(predicate); } } + @Subscribe public void reactToEvent(Event evt) { - boolean matches = true; - for (Predicate predicate : predicates) { - if(!predicate.test(evt)) { - matches = false; + try { + boolean matches = true; + for (Predicate predicate : predicates) { + if(!predicate.test(evt)) { + matches = false; + } } - } - if(matches) { - for (Consumer action : actions) { - action.accept(evt); + if(matches) { + for (Consumer action : actions) { + action.accept(evt); + } } + } catch(Exception e) { + LOG.error("Error while processing event", e); } } + + private List createPredicates(EventHandlerConfiguration config) { + List predicates = new ArrayList<>(config.getPredicates().size()); + for (PredicateConfiguration pc : config.getPredicates()) { + + try { + @SuppressWarnings("unchecked") + Class cls = (Class) Class.forName(pc.getType()); + if(cls == null) { + LOG.warn("Ignoring unknown action {}", cls); + continue; + } + EventPredicate predicate = cls.newInstance(); + predicate.configure(pc); + predicates.add(predicate); + } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { + LOG.warn("Error while creating action {} {}", pc.getType(), pc.getConfiguration(), e); + } + } + return predicates; + } + + private List createActions(EventHandlerConfiguration config) { + List actions = new ArrayList<>(config.getActions().size()); + for (ActionConfiguration ac : config.getActions()) { + try { + @SuppressWarnings("unchecked") + Class cls = (Class) Class.forName(ac.getType()); + if(cls == null) { + LOG.warn("Ignoring unknown action {}", cls); + continue; + } + Action action = cls.newInstance(); + action.configure(ac); + actions.add(action); + } catch (Exception e) { + LOG.warn("Error while creating action {} {}", ac.getType(), ac.getConfiguration(), e); + } + } + return actions; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + EventHandler other = (EventHandler) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + + } diff --git a/common/src/main/java/ctbrec/event/EventHandlerConfiguration.java b/common/src/main/java/ctbrec/event/EventHandlerConfiguration.java index 8b2f3b15..f3d7df81 100644 --- a/common/src/main/java/ctbrec/event/EventHandlerConfiguration.java +++ b/common/src/main/java/ctbrec/event/EventHandlerConfiguration.java @@ -4,14 +4,30 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; + +import ctbrec.Model; public class EventHandlerConfiguration { + private String id; private String name; private Event.Type event; private List predicates = new ArrayList<>(); private List actions = new ArrayList<>(); + public EventHandlerConfiguration() { + id = UUID.randomUUID().toString(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + public Event.Type getEvent() { return event; } @@ -44,10 +60,16 @@ public class EventHandlerConfiguration { this.actions = actions; } - public class PredicateConfiguration { + public static class PredicateConfiguration { + private String name; private String type; + private List models; private Map configuration = new HashMap<>(); + public void setName(String name) { + this.name = name; + } + public String getType() { return type; } @@ -64,9 +86,22 @@ public class EventHandlerConfiguration { this.configuration = configuration; } + public List getModels() { + return models; + } + + public void setModels(List models) { + this.models = models; + } + + @Override + public String toString() { + return name; + } } - public class ActionConfiguration { + public static class ActionConfiguration { + private String name; private String type; private Map configuration = new HashMap<>(); @@ -85,5 +120,44 @@ public class EventHandlerConfiguration { public void setConfiguration(Map configuration) { this.configuration = configuration; } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + @Override + public String toString() { + return name + ", when:" + predicates + " do:" + actions + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + EventHandlerConfiguration other = (EventHandlerConfiguration) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; } } diff --git a/common/src/main/java/ctbrec/event/EventPredicate.java b/common/src/main/java/ctbrec/event/EventPredicate.java new file mode 100644 index 00000000..298ffb48 --- /dev/null +++ b/common/src/main/java/ctbrec/event/EventPredicate.java @@ -0,0 +1,10 @@ +package ctbrec.event; + +import java.util.function.Predicate; + +import ctbrec.event.EventHandlerConfiguration.PredicateConfiguration; + +public abstract class EventPredicate implements Predicate { + + public abstract void configure(PredicateConfiguration pc); +} diff --git a/common/src/main/java/ctbrec/event/EventTypePredicate.java b/common/src/main/java/ctbrec/event/EventTypePredicate.java index 63bb3965..35135122 100644 --- a/common/src/main/java/ctbrec/event/EventTypePredicate.java +++ b/common/src/main/java/ctbrec/event/EventTypePredicate.java @@ -1,14 +1,16 @@ package ctbrec.event; -import java.util.function.Predicate; - import ctbrec.event.Event.Type; +import ctbrec.event.EventHandlerConfiguration.PredicateConfiguration; -public class EventTypePredicate implements Predicate { +public class EventTypePredicate extends EventPredicate { private Type type; - private EventTypePredicate(Type type) { + public EventTypePredicate() { + } + + public EventTypePredicate(Type type) { this.type = type; } @@ -17,7 +19,8 @@ public class EventTypePredicate implements Predicate { return evt.getType() == type; } - public static EventTypePredicate of(Type type) { - return new EventTypePredicate(type); + @Override + public void configure(PredicateConfiguration pc) { + type = Type.valueOf((String) pc.getConfiguration().get("type")); } } diff --git a/common/src/main/java/ctbrec/event/ExecuteProgram.java b/common/src/main/java/ctbrec/event/ExecuteProgram.java new file mode 100644 index 00000000..27c08ca1 --- /dev/null +++ b/common/src/main/java/ctbrec/event/ExecuteProgram.java @@ -0,0 +1,53 @@ +package ctbrec.event; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.OS; +import ctbrec.event.EventHandlerConfiguration.ActionConfiguration; +import ctbrec.io.StreamRedirectThread; + +public class ExecuteProgram extends Action { + + private static final transient Logger LOG = LoggerFactory.getLogger(ExecuteProgram.class); + + private String executable; + + public ExecuteProgram() {} + + public ExecuteProgram(String executable) { + this.executable = executable; + name = "execute program"; + } + + @Override + public void accept(Event evt) { + Runtime rt = Runtime.getRuntime(); + Process process = null; + try { + String[] args = {executable}; // TODO fill args array + process = rt.exec(args, OS.getEnvironment()); + + // create threads, which read stdout and stderr of the player process. these are needed, + // because otherwise the internal buffer for these streams fill up and block the process + Thread std = new Thread(new StreamRedirectThread(process.getInputStream(), System.out)); + std.setName("Player stdout pipe"); + std.setDaemon(true); + std.start(); + Thread err = new Thread(new StreamRedirectThread(process.getErrorStream(), System.err)); + err.setName("Player stderr pipe"); + err.setDaemon(true); + err.start(); + + process.waitFor(); + LOG.debug("{} finished", name); + } catch (Exception e) { + LOG.error("Error while processing {}", e); + } + } + + @Override + public void configure(ActionConfiguration config) { + executable = (String) config.getConfiguration().get("file"); + } +} diff --git a/common/src/main/java/ctbrec/event/ModelPredicate.java b/common/src/main/java/ctbrec/event/ModelPredicate.java index e23c902c..d573ab6f 100644 --- a/common/src/main/java/ctbrec/event/ModelPredicate.java +++ b/common/src/main/java/ctbrec/event/ModelPredicate.java @@ -1,30 +1,57 @@ package ctbrec.event; +import java.util.List; import java.util.Objects; import java.util.function.Predicate; import ctbrec.Model; +import ctbrec.event.EventHandlerConfiguration.PredicateConfiguration; -public class ModelPredicate implements Predicate { +public class ModelPredicate extends EventPredicate { - private Model model; + private Predicate internal; - private ModelPredicate(Model model) { - this.model = model; + public ModelPredicate() {} + + public ModelPredicate(Model model) { + internal = createFor(model); + } + + public ModelPredicate(List models) { + configure(models); + } + + private void configure(List models) { + if(models.isEmpty()) { + throw new IllegalArgumentException("List has to contain at least one model"); + } + + Predicate predicate = createFor(models.get(0)); + for (int i = 1; i < models.size(); i++) { + predicate = predicate.or(createFor(models.get(i))); + } + internal = predicate; } @Override public boolean test(Event evt) { - if(evt instanceof AbstractModelEvent) { - AbstractModelEvent modelEvent = (AbstractModelEvent) evt; - Model other = modelEvent.getModel(); - return Objects.equals(model, other); - } else { - return false; - } + return internal.test(evt); } - public static ModelPredicate of(Model model) { - return new ModelPredicate(model); + private Predicate createFor(Model model) { + return evt -> { + if(evt instanceof AbstractModelEvent) { + AbstractModelEvent modelEvent = (AbstractModelEvent) evt; + Model other = modelEvent.getModel(); + return Objects.equals(model, other); + } else { + return false; + } + }; + } + + @Override + public void configure(PredicateConfiguration pc) { + configure(pc.getModels()); } } diff --git a/common/src/main/java/ctbrec/event/ModelStatePredicate.java b/common/src/main/java/ctbrec/event/ModelStatePredicate.java index 3dad100e..9df7b97a 100644 --- a/common/src/main/java/ctbrec/event/ModelStatePredicate.java +++ b/common/src/main/java/ctbrec/event/ModelStatePredicate.java @@ -1,20 +1,22 @@ package ctbrec.event; -import java.util.function.Predicate; - import ctbrec.Model; +import ctbrec.Model.State; +import ctbrec.event.EventHandlerConfiguration.PredicateConfiguration; -public class ModelStatePredicate implements Predicate { +public class ModelStatePredicate extends EventPredicate { private Model.State state; - private ModelStatePredicate(Model.State state) { + public ModelStatePredicate() {} + + public ModelStatePredicate(Model.State state) { this.state = state; } @Override public boolean test(Event evt) { - if(evt instanceof AbstractModelEvent) { + if(evt instanceof ModelStateChangedEvent) { ModelStateChangedEvent modelEvent = (ModelStateChangedEvent) evt; Model.State newState = modelEvent.getNewState(); return newState == state; @@ -23,7 +25,8 @@ public class ModelStatePredicate implements Predicate { } } - public static ModelStatePredicate of(Model.State state) { - return new ModelStatePredicate(state); + @Override + public void configure(PredicateConfiguration pc) { + state = State.valueOf((String) pc.getConfiguration().get("state")); } } diff --git a/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java b/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java index f260552e..54c05e05 100644 --- a/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java +++ b/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java @@ -47,6 +47,10 @@ public class RecordingStateChangedEvent extends AbstractModelEvent { }; } + public State getState() { + return newState; + } + @Override public String toString() { return "RecordingStateChanged[" + newState + "," + model.getDisplayName() + "," + path + "]"; diff --git a/common/src/main/java/ctbrec/event/RecordingStatePredicate.java b/common/src/main/java/ctbrec/event/RecordingStatePredicate.java new file mode 100644 index 00000000..bad65223 --- /dev/null +++ b/common/src/main/java/ctbrec/event/RecordingStatePredicate.java @@ -0,0 +1,31 @@ +package ctbrec.event; + +import ctbrec.Recording; +import ctbrec.event.EventHandlerConfiguration.PredicateConfiguration; + +public class RecordingStatePredicate extends EventPredicate { + + private Recording.State state; + + public RecordingStatePredicate() {} + + public RecordingStatePredicate(Recording.State state) { + this.state = state; + } + + @Override + public boolean test(Event evt) { + if(evt instanceof RecordingStateChangedEvent) { + RecordingStateChangedEvent event = (RecordingStateChangedEvent) evt; + Recording.State newState = event.getState(); + return newState == state; + } else { + return false; + } + } + + @Override + public void configure(PredicateConfiguration pc) { + state = Recording.State.valueOf((String) pc.getConfiguration().get("state")); + } +} From 7c160068703936f1e659014e454f569b5551981f Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 9 Dec 2018 19:02:19 +0100 Subject: [PATCH 155/231] Set the name in the default constructor --- .../main/java/ctbrec/ui/event/PlaySound.java | 6 +++-- .../java/ctbrec/event/ExecuteProgram.java | 23 +++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/event/PlaySound.java b/client/src/main/java/ctbrec/ui/event/PlaySound.java index c23d77cd..aded0087 100644 --- a/client/src/main/java/ctbrec/ui/event/PlaySound.java +++ b/client/src/main/java/ctbrec/ui/event/PlaySound.java @@ -12,11 +12,13 @@ public class PlaySound extends Action { private URL url; - public PlaySound() {} + public PlaySound() { + name = "play sound"; + } public PlaySound(URL url) { + this(); this.url = url; - name = "play sound"; } @Override diff --git a/common/src/main/java/ctbrec/event/ExecuteProgram.java b/common/src/main/java/ctbrec/event/ExecuteProgram.java index 27c08ca1..28cb4807 100644 --- a/common/src/main/java/ctbrec/event/ExecuteProgram.java +++ b/common/src/main/java/ctbrec/event/ExecuteProgram.java @@ -1,5 +1,7 @@ package ctbrec.event; +import java.util.Arrays; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,11 +15,13 @@ public class ExecuteProgram extends Action { private String executable; - public ExecuteProgram() {} + public ExecuteProgram() { + name = "execute program"; + } public ExecuteProgram(String executable) { + this(); this.executable = executable; - name = "execute program"; } @Override @@ -25,8 +29,12 @@ public class ExecuteProgram extends Action { Runtime rt = Runtime.getRuntime(); Process process = null; try { - String[] args = {executable}; // TODO fill args array - process = rt.exec(args, OS.getEnvironment()); + String[] args = evt.getExecutionParams(); + String[] cmd = new String[args.length+1]; + cmd[0] = executable; + System.arraycopy(args, 0, cmd, 1, args.length); + LOG.debug("Executing {}", Arrays.toString(cmd)); + process = rt.exec(cmd, OS.getEnvironment()); // create threads, which read stdout and stderr of the player process. these are needed, // because otherwise the internal buffer for these streams fill up and block the process @@ -40,7 +48,7 @@ public class ExecuteProgram extends Action { err.start(); process.waitFor(); - LOG.debug("{} finished", name); + LOG.debug("executing {} finished", executable); } catch (Exception e) { LOG.error("Error while processing {}", e); } @@ -50,4 +58,9 @@ public class ExecuteProgram extends Action { public void configure(ActionConfiguration config) { executable = (String) config.getConfiguration().get("file"); } + + @Override + public String toString() { + return "execute " + executable; + } } From 86ae6602185178ef1d24ee2a422ef851f0f1a68c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 9 Dec 2018 19:02:50 +0100 Subject: [PATCH 156/231] Use the event name instead of the description in getExecutionParams --- .../src/main/java/ctbrec/event/ModelStateChangedEvent.java | 6 +++--- .../main/java/ctbrec/event/RecordingStateChangedEvent.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/ctbrec/event/ModelStateChangedEvent.java b/common/src/main/java/ctbrec/event/ModelStateChangedEvent.java index 98d681e7..1f79556b 100644 --- a/common/src/main/java/ctbrec/event/ModelStateChangedEvent.java +++ b/common/src/main/java/ctbrec/event/ModelStateChangedEvent.java @@ -32,12 +32,12 @@ public class ModelStateChangedEvent extends AbstractModelEvent { @Override public String[] getExecutionParams() { return new String[] { - getType().toString(), + getType().name(), model.getDisplayName(), model.getUrl(), model.getSite().getName(), - oldState.toString(), - newState.toString() + oldState.name(), + newState.name() }; } diff --git a/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java b/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java index 54c05e05..889017c8 100644 --- a/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java +++ b/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java @@ -37,9 +37,9 @@ public class RecordingStateChangedEvent extends AbstractModelEvent { @Override public String[] getExecutionParams() { return new String[] { - getType().toString(), + getType().name(), path.getAbsolutePath(), - newState.toString(), + newState.name(), model.getDisplayName(), model.getSite().getName(), model.getUrl(), From 878b25c55c5a7958a2eba9ea470feaf6e64ae723 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 9 Dec 2018 21:35:35 +0100 Subject: [PATCH 157/231] Add validation to actions panel --- .../main/java/ctbrec/ui/controls/Wizard.java | 12 +++++++++++- .../ui/settings/ActionSettingsPanel.java | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/controls/Wizard.java b/client/src/main/java/ctbrec/ui/controls/Wizard.java index c065bb06..301cbeb4 100644 --- a/client/src/main/java/ctbrec/ui/controls/Wizard.java +++ b/client/src/main/java/ctbrec/ui/controls/Wizard.java @@ -23,9 +23,11 @@ public class Wizard extends BorderPane { private Button prev; private Button finish; private boolean cancelled = true; + private Runnable validator; - public Wizard(Stage stage, Pane... pages) { + public Wizard(Stage stage, Runnable validator, Pane... pages) { this.stage = stage; + this.validator = validator; this.pages = pages; if (pages.length == 0) { @@ -49,6 +51,14 @@ public class Wizard extends BorderPane { cancel.setOnAction(evt -> stage.close()); finish = new Button("Finish"); finish.setOnAction(evt -> { + if(validator != null) { + try { + validator.run(); + } catch(IllegalStateException e) { + Dialogs.showError("Settings invalid", e.getMessage(), null); + return; + } + } cancelled = false; stage.close(); }); diff --git a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java index a143d181..76bd301d 100644 --- a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java +++ b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java @@ -16,6 +16,7 @@ import ctbrec.Config; import ctbrec.Model; import ctbrec.OS; import ctbrec.Recording; +import ctbrec.StringUtil; import ctbrec.event.Event; import ctbrec.event.EventBusHolder; import ctbrec.event.EventHandler; @@ -128,7 +129,7 @@ public class ActionSettingsPanel extends TitledPane { dialog.setTitle("New Action"); InputStream icon = getClass().getResourceAsStream("/icon.png"); dialog.getIcons().add(new Image(icon)); - Wizard root = new Wizard(dialog, actionPane); + Wizard root = new Wizard(dialog, this::validateSettings, actionPane); Scene scene = new Scene(root, 800, 540); scene.getStylesheets().addAll(getScene().getStylesheets()); dialog.setScene(scene); @@ -193,6 +194,21 @@ public class ActionSettingsPanel extends TitledPane { LOG.debug("Registered event handler for {} {}", config.getEvent(), config.getName()); } + private void validateSettings() { + if(StringUtil.isBlank(name.getText())) { + throw new IllegalStateException("Name cannot be empty"); + } + if(event.getValue() == Event.Type.MODEL_STATUS_CHANGED && modelState.getValue() == null) { + throw new IllegalStateException("Select a state"); + } + if(modelSelectionPane.getSelectedItems().isEmpty() && !modelSelectionPane.isAllSelected()) { + throw new IllegalStateException("Select one or more models or tick off \"all\""); + } + if(!(showNotification.isSelected() || playSound.isSelected() || executeProgram.isSelected())) { + throw new IllegalStateException("No action selected"); + } + } + private void delete(ActionEvent evt) { List selected = new ArrayList<>(actionTable.getSelectionModel().getSelectedItems()); for (EventHandlerConfiguration config : selected) { From 5b936c779db7fcb79dfd62e2bea587d3c0e6c3c1 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 9 Dec 2018 21:35:45 +0100 Subject: [PATCH 158/231] Add helper class for dialogs --- .../main/java/ctbrec/ui/ThumbOverviewTab.java | 21 +-------------- .../main/java/ctbrec/ui/controls/Dialogs.java | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 20 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/controls/Dialogs.java diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index ee38177a..5e5ec52a 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -1,5 +1,6 @@ package ctbrec.ui; +import static ctbrec.ui.controls.Dialogs.*; import java.io.IOException; import java.net.SocketTimeoutException; @@ -821,24 +822,4 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { selectedThumbCells.get(0).setSelected(false); } } - - private void showError(String header, String text, Exception e) { - Runnable r = () -> { - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Error"); - alert.setHeaderText(header); - String content = text; - if(e != null) { - content += " " + e.getLocalizedMessage(); - } - alert.setContentText(content); - alert.showAndWait(); - }; - - if(Platform.isFxApplicationThread()) { - r.run(); - } else { - Platform.runLater(r); - } - } } diff --git a/client/src/main/java/ctbrec/ui/controls/Dialogs.java b/client/src/main/java/ctbrec/ui/controls/Dialogs.java new file mode 100644 index 00000000..558f6e0f --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/Dialogs.java @@ -0,0 +1,27 @@ +package ctbrec.ui.controls; + +import ctbrec.ui.AutosizeAlert; +import javafx.application.Platform; +import javafx.scene.control.Alert; + +public class Dialogs { + public static void showError(String header, String text, Exception e) { + Runnable r = () -> { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText(header); + String content = text; + if(e != null) { + content += " " + e.getLocalizedMessage(); + } + alert.setContentText(content); + alert.showAndWait(); + }; + + if(Platform.isFxApplicationThread()) { + r.run(); + } else { + Platform.runLater(r); + } + } +} From 768507d6e5f87a917e866ccf4060fad03b11d076 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 9 Dec 2018 21:36:30 +0100 Subject: [PATCH 159/231] Increase thread pool size for event bus to 10 --- common/src/main/java/ctbrec/event/EventBusHolder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/event/EventBusHolder.java b/common/src/main/java/ctbrec/event/EventBusHolder.java index 37d7b278..6be9ce37 100644 --- a/common/src/main/java/ctbrec/event/EventBusHolder.java +++ b/common/src/main/java/ctbrec/event/EventBusHolder.java @@ -14,7 +14,7 @@ public class EventBusHolder { private static final transient Logger LOG = LoggerFactory.getLogger(EventBusHolder.class); private static Map handlers = new HashMap<>(); - public static final EventBus BUS = new AsyncEventBus(Executors.newSingleThreadExecutor()); + public static final EventBus BUS = new AsyncEventBus(Executors.newFixedThreadPool(10)); public static void register(EventHandler handler) { if(handlers.containsKey(handler.getId())) { From 316842e6903952cce6b3d4712e5de6946fcb15ea Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 9 Dec 2018 21:37:24 +0100 Subject: [PATCH 160/231] Revert removal of post-processing setting --- .../main/java/ctbrec/ui/settings/SettingsTab.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index b66c470f..05902352 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -53,6 +53,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { public static final int CHECKBOX_MARGIN = 6; private DirectorySelectionBox recordingsDirectory; private ProgramSelectionBox mediaPlayer; + private ProgramSelectionBox postProcessing; private TextField server; private TextField port; private TextField onlineCheckIntervalInSecs; @@ -305,6 +306,20 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + layout.add(new Label("Post-Processing"), 0, row); + postProcessing = new ProgramSelectionBox(Config.getInstance().getSettings().postProcessing); + postProcessing.fileProperty().addListener((obs, o, n) -> { + String path = n.getAbsolutePath(); + if(!Objects.equals(path, Config.getInstance().getSettings().postProcessing)) { + Config.getInstance().getSettings().postProcessing = path; + saveConfig(); + } + }); + GridPane.setFillWidth(postProcessing, true); + GridPane.setHgrow(postProcessing, Priority.ALWAYS); + GridPane.setMargin(postProcessing, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + layout.add(postProcessing, 1, row++); + Tooltip tt = new Tooltip("Check every x seconds, if a model came online"); l = new Label("Check online state every (seconds)"); l.setTooltip(tt); From f4e143eb7d09a9a8a52ae3579b8c06af77a2351f Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 12:54:37 +0100 Subject: [PATCH 161/231] Return a copy of models in getModelsRecording --- common/src/main/java/ctbrec/recorder/RemoteRecorder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java index dc56188e..4fafaa20 100644 --- a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -5,6 +5,7 @@ import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.time.Instant; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -135,7 +136,7 @@ public class RemoteRecorder implements Recorder { if(!lastSync.equals(Instant.EPOCH) && lastSync.isBefore(Instant.now().minusSeconds(60))) { throw new RuntimeException("Last sync was over a minute ago"); } - return models; + return new ArrayList(models); } @Override From c0442f898d6cd2efbbb66e1b0299622173cd71c4 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 12:57:33 +0100 Subject: [PATCH 162/231] Use /bin/bash as execution execution environment for the linux scripts --- client/src/assembly/ctbrec-linux-jre.sh | 2 +- client/src/assembly/ctbrec-linux.sh | 2 +- server/src/assembly/server-linux.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/assembly/ctbrec-linux-jre.sh b/client/src/assembly/ctbrec-linux-jre.sh index 6c68d4d8..e6880968 100755 --- a/client/src/assembly/ctbrec-linux-jre.sh +++ b/client/src/assembly/ctbrec-linux-jre.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash pushd $(dirname $0) JAVA=./jre/bin/java diff --git a/client/src/assembly/ctbrec-linux.sh b/client/src/assembly/ctbrec-linux.sh index df9c22eb..6ddab21e 100755 --- a/client/src/assembly/ctbrec-linux.sh +++ b/client/src/assembly/ctbrec-linux.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash pushd $(dirname $0) JAVA=java diff --git a/server/src/assembly/server-linux.sh b/server/src/assembly/server-linux.sh index 8c3658a7..498096bd 100755 --- a/server/src/assembly/server-linux.sh +++ b/server/src/assembly/server-linux.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash pushd $(dirname $0) JAVA=java From b715fba8362bc9dc4243bbd2365b2c6cdf88098f Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 14:32:33 +0100 Subject: [PATCH 163/231] Add validation for recording state --- .../src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java index 76bd301d..25d9c7a9 100644 --- a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java +++ b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java @@ -201,6 +201,9 @@ public class ActionSettingsPanel extends TitledPane { if(event.getValue() == Event.Type.MODEL_STATUS_CHANGED && modelState.getValue() == null) { throw new IllegalStateException("Select a state"); } + if(event.getValue() == Event.Type.RECORDING_STATUS_CHANGED && recordingState.getValue() == null) { + throw new IllegalStateException("Select a state"); + } if(modelSelectionPane.getSelectedItems().isEmpty() && !modelSelectionPane.isAllSelected()) { throw new IllegalStateException("Select one or more models or tick off \"all\""); } From 448cfd14b8a4a6394b4433665cdb8a1e010ec879 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 14:34:56 +0100 Subject: [PATCH 164/231] Clear state comboboxes before filling them to avoid duplicates --- .../src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java index 25d9c7a9..a163a19e 100644 --- a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java +++ b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java @@ -241,8 +241,10 @@ public class ActionSettingsPanel extends TitledPane { layout.add(event, 1, row++); layout.add(new Label("State"), 0, row); + modelState.getItems().clear(); modelState.getItems().addAll(Model.State.values()); layout.add(modelState, 1, row); + recordingState.getItems().clear(); recordingState.getItems().addAll(Recording.State.values()); layout.add(recordingState, 1, row++); recordingState.visibleProperty().bind(modelState.visibleProperty().not()); From ff539c1c2e7b10a8874dc160372f7ec0880d5845 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 14:35:21 +0100 Subject: [PATCH 165/231] Use state's name instead of description in toString --- .../src/main/java/ctbrec/event/RecordingStateChangedEvent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java b/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java index 889017c8..ad8deb00 100644 --- a/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java +++ b/common/src/main/java/ctbrec/event/RecordingStateChangedEvent.java @@ -53,7 +53,7 @@ public class RecordingStateChangedEvent extends AbstractModelEvent { @Override public String toString() { - return "RecordingStateChanged[" + newState + "," + model.getDisplayName() + "," + path + "]"; + return "RecordingStateChanged[" + newState.name() + "," + model.getDisplayName() + "," + path + "]"; } } From 5b15b77014b906a734a5da67ae1a1b2d71ccf186 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 15:18:24 +0100 Subject: [PATCH 166/231] Disable post-processing for server mode --- client/src/main/java/ctbrec/ui/settings/SettingsTab.java | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index 05902352..fc31091a 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -501,6 +501,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { directoryStructure.setDisable(!local); onlineCheckIntervalInSecs.setDisable(!local); leaveSpaceOnDevice.setDisable(!local); + postProcessing.setDisable(!local); } @Override From 1d409fa1d4b5f9a60a77abd6fca42e45006a9f2c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 15:27:56 +0100 Subject: [PATCH 167/231] Run post-processing steps in runnable in a thread pool Server and client now create a runnable for post-processing steps, which run in a thread pool. This ensures, that the steps run linearly so that RecordingStateChange events make sense, too. --- .../java/ctbrec/recorder/LocalRecorder.java | 190 +++++++----------- 1 file changed, 77 insertions(+), 113 deletions(-) diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 2cd50878..0732220b 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -24,6 +24,8 @@ import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; @@ -62,13 +64,14 @@ public class LocalRecorder implements Recorder { private Map playlistGenerators = new HashMap<>(); private Config config; private ProcessMonitor processMonitor; - private PostProcessingTrigger postProcessingTrigger; private volatile boolean recording = true; private List deleteInProgress = Collections.synchronizedList(new ArrayList<>()); private RecorderHttpClient client = new RecorderHttpClient(); private ReentrantLock lock = new ReentrantLock(); private long lastSpaceMessage = 0; + private ExecutorService ppThreadPool = Executors.newFixedThreadPool(2); + public LocalRecorder(Config config) { this.config = config; config.getSettings().models.stream().forEach((m) -> { @@ -83,12 +86,10 @@ public class LocalRecorder implements Recorder { processMonitor = new ProcessMonitor(); processMonitor.start(); - postProcessingTrigger = new PostProcessingTrigger(); - if(Config.isServerMode()) { - postProcessingTrigger.start(); - } - registerEventBusListener(); + if(Config.isServerMode()) { + processUnfinishedRecordings(); + } LOG.debug("Recorder initialized"); LOG.info("Models to record: {}", models); @@ -206,47 +207,40 @@ public class LocalRecorder implements Recorder { Download download = recordingProcesses.get(model); download.stop(); recordingProcesses.remove(model); - if(!Config.isServerMode()) { - postprocess(download); - } - fireRecordingStateChanged(download.getTarget(), FINISHED, model, download.getStartTime()); + fireRecordingStateChanged(download.getTarget(), STOPPED, model, download.getStartTime()); + ppThreadPool.submit(createPostProcessor(download)); } private void postprocess(Download download) { - if(!(download instanceof MergedHlsDownload)) { - throw new IllegalArgumentException("Download should be of type MergedHlsDownload"); - } String postProcessing = Config.getInstance().getSettings().postProcessing; if (postProcessing != null && !postProcessing.isEmpty()) { - new Thread(() -> { - Runtime rt = Runtime.getRuntime(); - try { - MergedHlsDownload d = (MergedHlsDownload) download; - String[] args = new String[] { - postProcessing, - d.getTarget().getParentFile().getAbsolutePath(), - d.getTarget().getAbsolutePath(), - d.getModel().getName(), - d.getModel().getSite().getName(), - Long.toString(download.getStartTime().getEpochSecond()) - }; - LOG.debug("Running {}", Arrays.toString(args)); - Process process = rt.exec(args, OS.getEnvironment()); - Thread std = new Thread(new StreamRedirectThread(process.getInputStream(), System.out)); - std.setName("Process stdout pipe"); - std.setDaemon(true); - std.start(); - Thread err = new Thread(new StreamRedirectThread(process.getErrorStream(), System.err)); - err.setName("Process stderr pipe"); - err.setDaemon(true); - err.start(); + Runtime rt = Runtime.getRuntime(); + try { + String[] args = new String[] { + postProcessing, + download.getTarget().getParentFile().getAbsolutePath(), + download.getTarget().getAbsolutePath(), + download.getModel().getName(), + download.getModel().getSite().getName(), + Long.toString(download.getStartTime().getEpochSecond()) + }; + LOG.debug("Running {}", Arrays.toString(args)); + Process process = rt.exec(args, OS.getEnvironment()); + // TODO maybe write these to a separate log file, e.g. recname.ts.pp.log + Thread std = new Thread(new StreamRedirectThread(process.getInputStream(), System.out)); + std.setName("Process stdout pipe"); + std.setDaemon(true); + std.start(); + Thread err = new Thread(new StreamRedirectThread(process.getErrorStream(), System.err)); + err.setName("Process stderr pipe"); + err.setDaemon(true); + err.start(); - process.waitFor(); - LOG.debug("Process finished."); - } catch (Exception e) { - LOG.error("Error in process thread", e); - } - }).start(); + process.waitFor(); + LOG.debug("Process finished."); + } catch (Exception e) { + LOG.error("Error in process thread", e); + } } } @@ -306,9 +300,9 @@ public class LocalRecorder implements Recorder { recording = false; LOG.debug("Stopping monitor threads"); processMonitor.running = false; - postProcessingTrigger.running = false; LOG.debug("Stopping all recording processes"); stopRecordingProcesses(); + ppThreadPool.shutdown(); client.shutdown(); } @@ -318,12 +312,7 @@ public class LocalRecorder implements Recorder { for (Model model : models) { Download recordingProcess = recordingProcesses.get(model); if (recordingProcess != null) { - try { - recordingProcess.stop(); - LOG.debug("Stopped recording for {}", model); - } catch (Exception e) { - LOG.error("Couldn't stop recording for model {}", model, e); - } + stopRecordingProcess(model); } } } finally { @@ -375,21 +364,14 @@ public class LocalRecorder implements Recorder { for (Iterator> iterator = recordingProcesses.entrySet().iterator(); iterator.hasNext();) { Entry entry = iterator.next(); Model m = entry.getKey(); - Download d = entry.getValue(); - if (!d.isAlive()) { + Download download = entry.getValue(); + if (!download.isAlive()) { LOG.debug("Recording terminated for model {}", m.getName()); iterator.remove(); restart.add(m); - if(Config.isServerMode()) { - try { - finishRecording(d.getTarget()); - } catch(Exception e) { - LOG.error("Error while finishing recording for model {}", m.getName(), e); - } - } else { - postprocess(d); - } - fireRecordingStateChanged(d.getTarget(), FINISHED, m, d.getStartTime()); // TODO fire all the events + fireRecordingStateChanged(download.getTarget(), STOPPED, m, download.getStartTime()); + Runnable pp = createPostProcessor(download); + ppThreadPool.submit(pp); } } for (Model m : restart) { @@ -407,20 +389,6 @@ public class LocalRecorder implements Recorder { } } - private void finishRecording(File directory) { - if(Config.isServerMode()) { - Thread t = new Thread() { - @Override - public void run() { - generatePlaylist(directory); - } - }; - t.setDaemon(true); - t.setName("Post-Processing " + directory.toString()); - t.start(); - } - } - private void generatePlaylist(File recDir) { PlaylistGenerator playlistGenerator = new PlaylistGenerator(); playlistGenerators.put(recDir, playlistGenerator); @@ -445,49 +413,32 @@ public class LocalRecorder implements Recorder { EventBusHolder.BUS.post(evt); } - private class PostProcessingTrigger extends Thread { - private volatile boolean running = false; - - public PostProcessingTrigger() { - setName("PostProcessingTrigger"); - setDaemon(true); - } - - @Override - public void run() { - running = true; - while (running) { - try { - List recs = getRecordings(); - for (Recording rec : recs) { - if (rec.getStatus() == RECORDING) { - boolean recordingProcessFound = false; - File recordingsDir = new File(config.getSettings().recordingsDir); - File recDir = new File(recordingsDir, rec.getPath()); - for (Entry download : recordingProcesses.entrySet()) { - if (download.getValue().getTarget().equals(recDir)) { - recordingProcessFound = true; - } - } - if (!recordingProcessFound) { - if (deleteInProgress.contains(recDir)) { - LOG.debug("{} is being deleted. Not going to start post-processing", recDir); - } else { - finishRecording(recDir); - } - } + /** + * This is called once at start for server mode. When the server is killed, recordings are + * left without playlist. This method creates playlists for them. + */ + private void processUnfinishedRecordings() { + try { + List recs = getRecordings(); + for (Recording rec : recs) { + if (rec.getStatus() == RECORDING) { + boolean recordingProcessFound = false; + File recordingsDir = new File(config.getSettings().recordingsDir); + File recDir = new File(recordingsDir, rec.getPath()); + for (Entry download : recordingProcesses.entrySet()) { + if (download.getValue().getTarget().equals(recDir)) { + recordingProcessFound = true; } } - - if (running) - Thread.sleep(10000); - } catch (InterruptedException e) { - LOG.error("Couldn't sleep", e); - } catch (Exception e) { - LOG.error("Unexpected error in playlist trigger thread", e); + if (!recordingProcessFound) { + ppThreadPool.submit(() -> { + generatePlaylist(recDir); + }); + } } } - LOG.debug(getName() + " terminated"); + } catch (Exception e) { + LOG.error("Unexpected error in playlist trigger", e); } } @@ -781,4 +732,17 @@ public class LocalRecorder implements Recorder { return getFreeSpaceBytes() > minimum; } } + + private Runnable createPostProcessor(Download download) { + return () -> { + LOG.debug("Starting post-processing for {}", download.getTarget()); + if(Config.isServerMode()) { + fireRecordingStateChanged(download.getTarget(), GENERATING_PLAYLIST, download.getModel(), download.getStartTime()); + generatePlaylist(download.getTarget()); + } + fireRecordingStateChanged(download.getTarget(), POST_PROCESSING, download.getModel(), download.getStartTime()); + postprocess(download); + fireRecordingStateChanged(download.getTarget(), FINISHED, download.getModel(), download.getStartTime()); + }; + } } From bcb89ef009d7017266eb0002cb99b71441b0c846 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 15:44:15 +0100 Subject: [PATCH 168/231] Add event processing system to the server Similar to the events and actions in the client you can add event listeners on the server. Easiest way to create them is to run ctbrec in standalone mode and then create the event on the settings tab. Afterwards you can copy the event handler from the client settings file to the server settings. --- .../java/ctbrec/recorder/server/HttpServer.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index ab07dabc..60c2dadc 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -17,6 +17,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Config; +import ctbrec.event.EventBusHolder; +import ctbrec.event.EventHandler; +import ctbrec.event.EventHandlerConfiguration; import ctbrec.recorder.LocalRecorder; import ctbrec.recorder.OnlineMonitor; import ctbrec.recorder.Recorder; @@ -51,6 +54,8 @@ public class HttpServer { addShutdownHook(); // for graceful termination + registerAlertSystem(); + config = Config.getInstance(); if(config.getSettings().key != null) { LOG.info("HMAC authentication is enabled"); @@ -133,6 +138,15 @@ public class HttpServer { } } + private void registerAlertSystem() { + for (EventHandlerConfiguration config : Config.getInstance().getSettings().eventHandlers) { + EventHandler handler = new EventHandler(config); + EventBusHolder.register(handler); + LOG.debug("Registered event handler for {} {}", config.getEvent(), config.getName()); + } + LOG.debug("Alert System registered"); + } + public static void main(String[] args) throws Exception { new HttpServer(); } From 2fc00404b8dbf1d5f0184f493cfa81c60d9ed151 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 16:21:34 +0100 Subject: [PATCH 169/231] Implement recording state change events in RemoteRecorder --- .../java/ctbrec/recorder/RemoteRecorder.java | 240 +++++++++++++++--- 1 file changed, 198 insertions(+), 42 deletions(-) diff --git a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java index 4fafaa20..ddd2387f 100644 --- a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -1,5 +1,6 @@ package ctbrec.recorder; +import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; @@ -7,26 +8,31 @@ import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; +import java.util.Iterator; import java.util.List; -import java.util.Map; +import java.util.concurrent.ExecutionException; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.PlaylistException; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; +import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.Hmac; import ctbrec.Model; import ctbrec.Recording; import ctbrec.event.EventBusHolder; +import ctbrec.event.RecordingStateChangedEvent; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import ctbrec.io.InstantJsonAdapter; import ctbrec.io.ModelJsonAdapter; +import ctbrec.recorder.download.StreamSource; import ctbrec.sites.Site; import okhttp3.MediaType; import okhttp3.Request; @@ -37,7 +43,6 @@ import okhttp3.Response; public class RemoteRecorder implements Recorder { private static final transient Logger LOG = LoggerFactory.getLogger(RemoteRecorder.class); - public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); private Moshi moshi = new Moshi.Builder() .add(Instant.class, new InstantJsonAdapter()) @@ -49,6 +54,7 @@ public class RemoteRecorder implements Recorder { private List models = Collections.emptyList(); private List onlineModels = Collections.emptyList(); + private List recordings = Collections.emptyList(); private List sites; private long spaceTotal = -1; private long spaceFree = -1; @@ -159,6 +165,7 @@ public class RemoteRecorder implements Recorder { syncModels(); syncOnlineModels(); syncSpace(); + syncRecordings(); sleep(); } } @@ -236,7 +243,6 @@ public class RemoteRecorder implements Recorder { if (response.isSuccessful()) { ModelListResponse resp = modelListResponseAdapter.fromJson(json); if (resp.status.equals("success")) { - List previouslyOnline = onlineModels; onlineModels = resp.models; for (Model model : models) { for (Site site : sites) { @@ -245,25 +251,49 @@ public class RemoteRecorder implements Recorder { } } } + } else { + LOG.error("Server returned error: {} - {}", resp.status, resp.msg); + } + } else { + LOG.error("Couldn't synchronize with server. HTTP status: {} - {}", response.code(), json); + } + } + } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) { + LOG.error("Couldn't synchronize with server", e); + } + } - for (Model prev : previouslyOnline) { - if(!onlineModels.contains(prev)) { - Map evt = new HashMap<>(); - evt.put("event", "model.status"); - evt.put("status", "offline"); - evt.put("model", prev); - EventBusHolder.BUS.post(evt); - } - } - for (Model model : onlineModels) { - if(!previouslyOnline.contains(model)) { - Map evt = new HashMap<>(); - evt.put("event", "model.status"); - evt.put("status", "online"); - evt.put("model", model); - EventBusHolder.BUS.post(evt); + private void syncRecordings() { + try { + String msg = "{\"action\": \"recordings\"}"; + RequestBody body = RequestBody.create(JSON, msg); + Request.Builder builder = new Request.Builder() + .url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") + .post(body); + addHmacIfNeeded(msg, builder); + Request request = builder.build(); + try (Response response = client.execute(request)) { + String json = response.body().string(); + if (response.isSuccessful()) { + RecordingListResponse resp = recordingListResponseAdapter.fromJson(json); + if (resp.status.equals("success")) { + List newRecordings = resp.recordings; + // fire changed events + for (Iterator iterator = recordings.iterator(); iterator.hasNext();) { + Recording recording = iterator.next(); + if(newRecordings.contains(recording)) { + int idx = newRecordings.indexOf(recording); + Recording newRecording = newRecordings.get(idx); + if(newRecording.getStatus() != recording.getStatus()) { + File file = new File(recording.getPath()); + Model m = new UnknownModel(); + m.setName(newRecording.getModelName()); + RecordingStateChangedEvent evt = new RecordingStateChangedEvent(file, newRecording.getStatus(), m, recording.getStartDate()); + EventBusHolder.BUS.post(evt); + } } } + recordings = newRecordings; } else { LOG.error("Server returned error: {} - {}", resp.status, resp.msg); } @@ -304,28 +334,7 @@ public class RemoteRecorder implements Recorder { @Override public List getRecordings() throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException { - String msg = "{\"action\": \"recordings\"}"; - RequestBody body = RequestBody.create(JSON, msg); - Request.Builder builder = new Request.Builder() - .url("http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/rec") - .post(body); - addHmacIfNeeded(msg, builder); - Request request = builder.build(); - try(Response response = client.execute(request)) { - String json = response.body().string(); - if(response.isSuccessful()) { - RecordingListResponse resp = recordingListResponseAdapter.fromJson(json); - if(resp.status.equals("success")) { - List recordings = resp.recordings; - return recordings; - } else { - LOG.error("Server returned error: {} - {}", resp.status, resp.msg); - } - } else { - LOG.error("Couldn't synchronize with server. HTTP status: {} - {}", response.code(), json); - } - } - return Collections.emptyList(); + return recordings; } @Override @@ -425,4 +434,151 @@ public class RemoteRecorder implements Recorder { public long getFreeSpaceBytes() { return spaceFree; } + + private static class UnknownModel extends AbstractModel { + @Override + public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { + return false; + } + + @Override + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + return Collections.emptyList(); + } + + @Override + public void invalidateCacheEntries() { + } + + @Override + public void receiveTip(int tokens) throws IOException { + } + + @Override + public int[] getStreamResolution(boolean failFast) throws ExecutionException { + return new int[2]; + } + + @Override + public boolean follow() throws IOException { + return false; + } + + @Override + public boolean unfollow() throws IOException { + return false; + } + + @Override + public Site getSite() { + return new Site() { + + @Override + public boolean supportsTips() { + return false; + } + + @Override + public boolean supportsSearch() { + return false; + } + + @Override + public boolean supportsFollow() { + return false; + } + + @Override + public void shutdown() { + } + + @Override + public void setRecorder(Recorder recorder) { + } + + @Override + public void setEnabled(boolean enabled) { + } + + @Override + public boolean searchRequiresLogin() { + return false; + } + + @Override + public List search(String q) throws IOException, InterruptedException { + return Collections.emptyList(); + } + + @Override + public boolean login() throws IOException { + return false; + } + + @Override + public boolean isSiteForModel(Model m) { + return false; + } + + @Override + public boolean isEnabled() { + return false; + } + + @Override + public void init() throws IOException { + } + + @Override + public Integer getTokenBalance() throws IOException { + return 0; + } + + @Override + public Recorder getRecorder() { + return null; + } + + @Override + public String getName() { + return "unknown"; + } + + @Override + public HttpClient getHttpClient() { + return null; + } + + @Override + public String getBuyTokensLink() { + return ""; + } + + @Override + public String getBaseUrl() { + return ""; + } + + @Override + public String getAffiliateLink() { + return ""; + } + + @Override + public boolean credentialsAvailable() { + return false; + } + + @Override + public Model createModelFromUrl(String url) { + return null; + } + + @Override + public Model createModel(String name) { + return null; + } + }; + } + } } From 4ebaab75933b0a10addffdbc92a2c5635032bef0 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 16:58:30 +0100 Subject: [PATCH 170/231] Update changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 004bfdf2..6e31a1ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +1.14.0 +======================== +* Added setting for MFC to ignore the upscaled (960p) stream +* Added event system. You can define to show a notification, + play a sound or execute a program, when the state of a model + or recording changes +* Added "follow" menu entry on the Recording tab +* Fix: Recordings change from suspended to recording by their own + when a thumbnail tab is opened and the model is showing +* Fix: Linux scripts don't work on system where bash isn't the + default shell + 1.13.0 ======================== * Added possibility to open small live previews of online models From e7f1d26a84b5382da2a0767575b53f7e1e2e7051 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 17:15:33 +0100 Subject: [PATCH 171/231] Bumb version to 1.14.0 --- client/pom.xml | 2 +- common/pom.xml | 2 +- master/pom.xml | 2 +- server/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index 7b866a81..a71d0d04 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.13.0 + 1.14.0 ../master diff --git a/common/pom.xml b/common/pom.xml index 3fef25a0..b92016d3 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.13.0 + 1.14.0 ../master diff --git a/master/pom.xml b/master/pom.xml index 8a107451..7cb07d20 100644 --- a/master/pom.xml +++ b/master/pom.xml @@ -6,7 +6,7 @@ ctbrec master pom - 1.13.0 + 1.14.0 ../common diff --git a/server/pom.xml b/server/pom.xml index 32ae4365..7cd1f4f6 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.13.0 + 1.14.0 ../master From 064efd2863cfe2c616909d0e163f4f1cef81d431 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 17:16:39 +0100 Subject: [PATCH 172/231] Register event handler 1 min after start Don't register before 1 minute has passed, because directly after the start of ctbrec, an event for every online model would be fired, which is annoying as f --- .../java/ctbrec/ui/CamrecApplication.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 40ca2431..2215460b 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -206,20 +207,21 @@ public class CamrecApplication extends Application { } private void registerAlertSystem() { - // try { - // // don't register before 1 minute has passed, because directly after - // // the start of ctbrec, an event for every online model would be fired, - // // which is annoying as f - // Thread.sleep(TimeUnit.MINUTES.toMillis(1)); - // } catch (InterruptedException e) { - // e.printStackTrace(); - // } - for (EventHandlerConfiguration config : Config.getInstance().getSettings().eventHandlers) { - EventHandler handler = new EventHandler(config); - EventBusHolder.register(handler); - LOG.debug("Registered event handler for {} {}", config.getEvent(), config.getName()); + try { + // don't register before 1 minute has passed, because directly after + // the start of ctbrec, an event for every online model would be fired, + // which is annoying as f + Thread.sleep(TimeUnit.MINUTES.toMillis(1)); + + for (EventHandlerConfiguration config : Config.getInstance().getSettings().eventHandlers) { + EventHandler handler = new EventHandler(config); + EventBusHolder.register(handler); + LOG.debug("Registered event handler for {} {}", config.getEvent(), config.getName()); + } + LOG.debug("Alert System registered"); + } catch (InterruptedException e) { + e.printStackTrace(); } - LOG.debug("Alert System registered"); } private void writeColorSchemeStyleSheet(Stage primaryStage) { From deed010a3f71c15d68335e16c451dc9045e66e69 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 17:16:50 +0100 Subject: [PATCH 173/231] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 004bfdf2..789fe20f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +1.14.0 +======================== +* Improved loading and display of resolution tags. They are not re-loaded + everytime to switch between tabs + 1.13.0 ======================== * Added possibility to open small live previews of online models From 04382dfa6e5764e9622ab664e837830e550a3296 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 17:57:12 +0100 Subject: [PATCH 174/231] Run registerAlertSystem in a thread --- .../java/ctbrec/ui/CamrecApplication.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 2215460b..d698dbdb 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -207,21 +207,23 @@ public class CamrecApplication extends Application { } private void registerAlertSystem() { - try { - // don't register before 1 minute has passed, because directly after - // the start of ctbrec, an event for every online model would be fired, - // which is annoying as f - Thread.sleep(TimeUnit.MINUTES.toMillis(1)); + new Thread(() -> { + try { + // don't register before 1 minute has passed, because directly after + // the start of ctbrec, an event for every online model would be fired, + // which is annoying as f + Thread.sleep(TimeUnit.MINUTES.toMillis(1)); - for (EventHandlerConfiguration config : Config.getInstance().getSettings().eventHandlers) { - EventHandler handler = new EventHandler(config); - EventBusHolder.register(handler); - LOG.debug("Registered event handler for {} {}", config.getEvent(), config.getName()); + for (EventHandlerConfiguration config : Config.getInstance().getSettings().eventHandlers) { + EventHandler handler = new EventHandler(config); + EventBusHolder.register(handler); + LOG.debug("Registered event handler for {} {}", config.getEvent(), config.getName()); + } + LOG.debug("Alert System registered"); + } catch (InterruptedException e) { + e.printStackTrace(); } - LOG.debug("Alert System registered"); - } catch (InterruptedException e) { - e.printStackTrace(); - } + }).start(); } private void writeColorSchemeStyleSheet(Stage primaryStage) { From bb3de834531b07ec9e5e3d19908c1ee85b7dc032 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 18:37:04 +0100 Subject: [PATCH 175/231] Logout and delete cookies when credentials are changed --- .../ctbrec/ui/sites/bonga/BongaCamsConfigUI.java | 16 +++++++++++----- .../java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java | 16 +++++++++++----- .../ctbrec/ui/sites/camsoda/CamsodaConfigUI.java | 14 ++++++++++---- .../ui/sites/chaturbate/ChaturbateConfigUi.java | 14 ++++++++++---- .../ui/sites/myfreecams/MyFreeCamsConfigUI.java | 14 ++++++++++---- .../src/main/java/ctbrec/io/CookieJarImpl.java | 4 ++++ common/src/main/java/ctbrec/io/HttpClient.java | 8 ++++++-- 7 files changed, 62 insertions(+), 24 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java index 0755f024..0214dad3 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsConfigUI.java @@ -47,8 +47,11 @@ public class BongaCamsConfigUI extends AbstractConfigUI { layout.add(new Label("BongaCams User"), 0, row); TextField username = new TextField(settings.bongaUsername); username.textProperty().addListener((ob, o, n) -> { - settings.bongaUsername = username.getText(); - save(); + if(!n.equals(Config.getInstance().getSettings().bongaUsername)) { + Config.getInstance().getSettings().bongaUsername = username.getText(); + bongaCams.getHttpClient().logout(); + save(); + } }); GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); @@ -58,9 +61,12 @@ public class BongaCamsConfigUI extends AbstractConfigUI { layout.add(new Label("BongaCams Password"), 0, row); PasswordField password = new PasswordField(); password.setText(settings.bongaPassword); - password.focusedProperty().addListener((e) -> { - settings.bongaPassword = password.getText(); - save(); + password.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().bongaPassword)) { + Config.getInstance().getSettings().bongaPassword = password.getText(); + bongaCams.getHttpClient().logout(); + save(); + } }); GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java index 46ce3490..e9062360 100644 --- a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4ConfigUI.java @@ -47,8 +47,11 @@ public class Cam4ConfigUI extends AbstractConfigUI { layout.add(new Label("Cam4 User"), 0, row); TextField username = new TextField(Config.getInstance().getSettings().cam4Username); username.textProperty().addListener((ob, o, n) -> { - Config.getInstance().getSettings().cam4Username = username.getText(); - save(); + if(!n.equals(Config.getInstance().getSettings().cam4Username)) { + Config.getInstance().getSettings().cam4Username = username.getText(); + cam4.getHttpClient().logout(); + save(); + } }); GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); @@ -58,9 +61,12 @@ public class Cam4ConfigUI extends AbstractConfigUI { layout.add(new Label("Cam4 Password"), 0, row); PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().cam4Password); - password.focusedProperty().addListener((e) -> { - Config.getInstance().getSettings().cam4Password = password.getText(); - save(); + password.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().cam4Password)) { + Config.getInstance().getSettings().cam4Password = password.getText(); + cam4.getHttpClient().logout(); + save(); + } }); GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java index 197108dc..8cdc5932 100644 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaConfigUI.java @@ -47,8 +47,11 @@ public class CamsodaConfigUI extends AbstractConfigUI { layout.add(new Label("CamSoda User"), 0, row); TextField username = new TextField(Config.getInstance().getSettings().camsodaUsername); username.textProperty().addListener((ob, o, n) -> { - Config.getInstance().getSettings().camsodaUsername = username.getText(); - save(); + if(!n.equals(Config.getInstance().getSettings().camsodaUsername)) { + Config.getInstance().getSettings().camsodaUsername = username.getText(); + camsoda.getHttpClient().logout(); + save(); + } }); GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); @@ -59,8 +62,11 @@ public class CamsodaConfigUI extends AbstractConfigUI { PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().camsodaPassword); password.textProperty().addListener((ob, o, n) -> { - Config.getInstance().getSettings().camsodaPassword = password.getText(); - save(); + if(!n.equals(Config.getInstance().getSettings().camsodaPassword)) { + Config.getInstance().getSettings().camsodaPassword = password.getText(); + camsoda.getHttpClient().logout(); + save(); + } }); GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); diff --git a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java index 8133bf90..791b1fd1 100644 --- a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java +++ b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateConfigUi.java @@ -47,8 +47,11 @@ public class ChaturbateConfigUi extends AbstractConfigUI { layout.add(new Label("Chaturbate User"), 0, row); TextField username = new TextField(Config.getInstance().getSettings().username); username.textProperty().addListener((ob, o, n) -> { - Config.getInstance().getSettings().username = username.getText(); - save(); + if(!n.equals(Config.getInstance().getSettings().username)) { + Config.getInstance().getSettings().username = n; + chaturbate.getHttpClient().logout(); + save(); + } }); GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); @@ -59,8 +62,11 @@ public class ChaturbateConfigUi extends AbstractConfigUI { PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().password); password.textProperty().addListener((ob, o, n) -> { - Config.getInstance().getSettings().password = password.getText(); - save(); + if(!n.equals(Config.getInstance().getSettings().password)) { + Config.getInstance().getSettings().password = n; + chaturbate.getHttpClient().logout(); + save(); + } }); GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java index cf258844..2c65a382 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsConfigUI.java @@ -48,8 +48,11 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { TextField username = new TextField(Config.getInstance().getSettings().mfcUsername); username.setPrefWidth(300); username.textProperty().addListener((ob, o, n) -> { - Config.getInstance().getSettings().mfcUsername = username.getText(); - save(); + if(!n.equals(Config.getInstance().getSettings().mfcUsername)) { + Config.getInstance().getSettings().mfcUsername = username.getText(); + myFreeCams.getHttpClient().logout(); + save(); + } }); GridPane.setFillWidth(username, true); GridPane.setHgrow(username, Priority.ALWAYS); @@ -60,8 +63,11 @@ public class MyFreeCamsConfigUI extends AbstractConfigUI { PasswordField password = new PasswordField(); password.setText(Config.getInstance().getSettings().mfcPassword); password.textProperty().addListener((ob, o, n) -> { - Config.getInstance().getSettings().mfcPassword = password.getText(); - save(); + if(!n.equals(Config.getInstance().getSettings().mfcPassword)) { + Config.getInstance().getSettings().mfcPassword = password.getText(); + myFreeCams.getHttpClient().logout(); + save(); + } }); GridPane.setFillWidth(password, true); GridPane.setHgrow(password, Priority.ALWAYS); diff --git a/common/src/main/java/ctbrec/io/CookieJarImpl.java b/common/src/main/java/ctbrec/io/CookieJarImpl.java index deaaab2c..69192ecb 100644 --- a/common/src/main/java/ctbrec/io/CookieJarImpl.java +++ b/common/src/main/java/ctbrec/io/CookieJarImpl.java @@ -92,4 +92,8 @@ public class CookieJarImpl implements CookieJar { public Map> getCookies() { return cookieStore; } + + public void clear() { + cookieStore.clear(); + } } diff --git a/common/src/main/java/ctbrec/io/HttpClient.java b/common/src/main/java/ctbrec/io/HttpClient.java index df03cf52..9dc81010 100644 --- a/common/src/main/java/ctbrec/io/HttpClient.java +++ b/common/src/main/java/ctbrec/io/HttpClient.java @@ -23,7 +23,6 @@ import ctbrec.Config; import ctbrec.Settings.ProxyType; import okhttp3.ConnectionPool; import okhttp3.Cookie; -import okhttp3.CookieJar; import okhttp3.Credentials; import okhttp3.OkHttpClient; import okhttp3.OkHttpClient.Builder; @@ -212,7 +211,12 @@ public abstract class HttpClient { } } - public CookieJar getCookieJar() { + public CookieJarImpl getCookieJar() { return cookieJar; } + + public void logout() { + getCookieJar().clear(); + loggedIn = false; + } } From 782c351e06797a154bbaf232c2d8152a171173b1 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 20:34:03 +0100 Subject: [PATCH 176/231] Fixed typos --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5ec4310..ea0be75a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,9 @@ * Added "follow" menu entry on the Recording tab * Fix: Recordings change from suspended to recording by their own when a thumbnail tab is opened and the model is showing -* Fix: Linux scripts don't work on system where bash isn't the default shell +* Fix: Linux scripts don't work on systems where bash isn't the default shell * Improved loading and display of resolution tags. They are not re-loaded - everytime to switch between tabs + everytime you switch between tabs 1.13.0 ======================== From 02e080b70e5db94a7d896a2a967baea83f197457 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 20:34:54 +0100 Subject: [PATCH 177/231] Update download links to 1.14.0 --- docs/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.html b/docs/index.html index c8b66352..d4d7df8f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -118,19 +118,19 @@
    - + Download for Linux! From 0fe9d9677a542377248edb492c8e61bed214e11c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 21:08:55 +0100 Subject: [PATCH 178/231] Delete recordings from synced cached recordings --- common/src/main/java/ctbrec/recorder/RemoteRecorder.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java index ddd2387f..e31650c1 100644 --- a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -352,6 +352,8 @@ public class RemoteRecorder implements Recorder { if(response.isSuccessful()) { if(!resp.status.equals("success")) { throw new IOException("Couldn't delete recording: " + resp.msg); + } else { + recordings.remove(recording); } } else { throw new IOException("Couldn't delete recording: " + resp.msg); From dd76774145b7adf345d197a2ed1b2787ba91efd4 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 10 Dec 2018 22:00:29 +0100 Subject: [PATCH 179/231] Decrease sync thread sleep time to 2 secs --- common/src/main/java/ctbrec/recorder/RemoteRecorder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java index e31650c1..38d27d99 100644 --- a/common/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/common/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -308,7 +308,7 @@ public class RemoteRecorder implements Recorder { private void sleep() { try { - Thread.sleep(10000); + Thread.sleep(2000); } catch (InterruptedException e) { // interrupted, probably by stopThread } From 2d1ac40c725c508f293248b9cd0e7de002670762 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 11 Dec 2018 15:47:19 +0100 Subject: [PATCH 180/231] Move token label and buy button to ThumbOverviewTab --- client/src/main/java/ctbrec/ui/SiteTab.java | 24 +------------------ .../main/java/ctbrec/ui/ThumbOverviewTab.java | 12 ++++++++-- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/SiteTab.java b/client/src/main/java/ctbrec/ui/SiteTab.java index 04afc41f..1b0fc0b1 100644 --- a/client/src/main/java/ctbrec/ui/SiteTab.java +++ b/client/src/main/java/ctbrec/ui/SiteTab.java @@ -1,40 +1,18 @@ package ctbrec.ui; import ctbrec.sites.Site; -import javafx.geometry.Insets; -import javafx.geometry.Pos; import javafx.scene.Scene; -import javafx.scene.control.Button; import javafx.scene.control.Tab; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; public class SiteTab extends Tab implements TabSelectionListener { - private BorderPane rootPane = new BorderPane(); - private HBox tokenPanel; private SiteTabPane siteTabPane; public SiteTab(Site site, Scene scene) { super(site.getName()); - setClosable(false); - setContent(rootPane); siteTabPane = new SiteTabPane(site, scene); - rootPane.setCenter(siteTabPane); - - if (site.supportsTips() && site.credentialsAvailable()) { - Button buyTokens = new Button("Buy Tokens"); - buyTokens.setOnAction((e) -> DesktopIntegration.open(site.getBuyTokensLink())); - TokenLabel tokenBalance = new TokenLabel(site); - tokenPanel = new HBox(5, tokenBalance, buyTokens); - tokenPanel.setAlignment(Pos.BASELINE_RIGHT); - rootPane.setTop(tokenPanel); - // HBox.setMargin(tokenBalance, new Insets(0, 5, 0, 0)); - // HBox.setMargin(buyTokens, new Insets(0, 5, 0, 0)); - tokenBalance.loadBalance(); - BorderPane.setMargin(tokenPanel, new Insets(5, 10, 0, 10)); - } + setContent(siteTabPane); } @Override diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index 5e5ec52a..f1d01ece 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -143,7 +143,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { + "Try \"1080\" or \">720\" or \"public\""); filterInput.setTooltip(filterTooltip); filterInput.getStyleClass().remove("search-box-icon"); - BorderPane.setMargin(filterInput, new Insets(5)); SearchBox searchInput = new SearchBox(); searchInput.setPromptText("Search Model"); @@ -154,7 +153,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { popover.hide(); } }); - BorderPane.setMargin(searchInput, new Insets(5)); popover = new SearchPopover(); popover.maxWidthProperty().bind(popover.minWidthProperty()); @@ -170,9 +168,19 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { HBox topBar = new HBox(5); HBox.setHgrow(filterInput, Priority.ALWAYS); topBar.getChildren().add(filterInput); + if (site.supportsTips() && site.credentialsAvailable()) { + Button buyTokens = new Button("Buy Tokens"); + buyTokens.setOnAction((e) -> DesktopIntegration.open(site.getBuyTokensLink())); + TokenLabel tokenBalance = new TokenLabel(site); + tokenBalance.setAlignment(Pos.CENTER_RIGHT); + tokenBalance.prefHeightProperty().bind(buyTokens.heightProperty()); + topBar.getChildren().addAll(tokenBalance, buyTokens); + tokenBalance.loadBalance(); + } if(site.supportsSearch()) { topBar.getChildren().add(searchInput); } + BorderPane.setMargin(topBar, new Insets(0, 5, 0, 5)); scrollPane.setContent(grid); scrollPane.setFitToHeight(true); From a5ec00c93643287893599f82a2a81884ff4cb6e6 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 11 Dec 2018 21:59:32 +0100 Subject: [PATCH 181/231] Add table for MFC which contains all models --- .../main/java/ctbrec/ui/ThumbOverviewTab.java | 2 +- .../myfreecams/MyFreeCamsTabProvider.java | 2 + .../sites/myfreecams/MyFreeCamsTableTab.java | 388 ++++++++++++++++++ .../sites/myfreecams/TableUpdateService.java | 31 ++ common/src/main/java/ctbrec/Settings.java | 5 + .../ctbrec/sites/mfc/MyFreeCamsClient.java | 6 + 6 files changed, 433 insertions(+), 1 deletion(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java create mode 100644 client/src/main/java/ctbrec/ui/sites/myfreecams/TableUpdateService.java diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index f1d01ece..d3c66c4e 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -782,7 +782,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { tokensMissing = true; } } else if(token.equals("public")) { - if(!m.getOnlineState(true).equals(token)) { + if(!m.getOnlineState(true).toString().equals(token)) { tokensMissing = true; } } else if(!searchText.toLowerCase().contains(token.toLowerCase())) { diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTabProvider.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTabProvider.java index ad5eb961..68847ed1 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTabProvider.java @@ -49,6 +49,8 @@ public class MyFreeCamsTabProvider extends TabProvider { updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); tabs.add(pop); + MyFreeCamsTableTab table = new MyFreeCamsTableTab(myFreeCams); + tabs.add(table); return tabs; } diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java new file mode 100644 index 00000000..9a01c796 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java @@ -0,0 +1,388 @@ +package ctbrec.ui.sites.myfreecams; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.StringUtil; +import ctbrec.sites.mfc.MyFreeCams; +import ctbrec.sites.mfc.SessionState; +import ctbrec.ui.TabSelectionListener; +import ctbrec.ui.controls.SearchBox; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.concurrent.Worker.State; +import javafx.concurrent.WorkerStateEvent; +import javafx.event.ActionEvent; +import javafx.geometry.Insets; +import javafx.geometry.Point2D; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.CheckMenuItem; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Tab; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableColumn.SortType; +import javafx.scene.control.TableView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.util.Duration; + +public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { + private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsTableTab.class); + private ScrollPane scrollPane = new ScrollPane(); + private TableView table = new TableView(); + private ObservableList filteredModels = FXCollections.observableArrayList(); + private ObservableList observableModels = FXCollections.observableArrayList(); + private TableUpdateService updateService; + private MyFreeCams mfc; + private ReentrantLock lock = new ReentrantLock(); + private SearchBox filterInput; + private Label count = new Label("models"); + private List> columns = new ArrayList<>(); + + public MyFreeCamsTableTab(MyFreeCams mfc) { + this.mfc = mfc; + setText("Tabular"); + setClosable(false); + initUpdateService(); + createGui(); + restoreState(); + filter(filterInput.getText()); + } + + private void initUpdateService() { + updateService = new TableUpdateService(mfc); + updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(1))); + updateService.setOnSucceeded(this::onSuccess); + updateService.setOnFailed((event) -> { + LOG.info("Couldn't update MyFreeCams model table", event.getSource().getException()); + }); + } + + private void onSuccess(WorkerStateEvent evt) { + Collection sessionStates = updateService.getValue(); + if (sessionStates == null) { + return; + } + + lock.lock(); + try { + for (SessionState updatedModel : sessionStates) { + int index = observableModels.indexOf(updatedModel); + if (index == -1) { + observableModels.add(updatedModel); + } else { + observableModels.set(index, updatedModel); + } + } + + for (Iterator iterator = observableModels.iterator(); iterator.hasNext();) { + SessionState model = iterator.next(); + if (!sessionStates.contains(model)) { + iterator.remove(); + } + } + } finally { + lock.unlock(); + } + + filteredModels.clear(); + filter(filterInput.getText()); + table.sort(); + } + + private void createGui() { + BorderPane layout = new BorderPane(); + layout.setPadding(new Insets(5, 10, 10, 10)); + + filterInput = new SearchBox(false); + filterInput.setPromptText("Filter"); + filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> { + String filter = filterInput.getText(); + Config.getInstance().getSettings().mfcModelsTableFilter = filter; + lock.lock(); + try { + filter(filter); + } finally { + lock.unlock(); + } + }); + filterInput.getStyleClass().remove("search-box-icon"); + HBox.setHgrow(filterInput, Priority.ALWAYS); + Button columnSelection = new Button("⚙"); + //Button columnSelection = new Button("⩩"); + columnSelection.setOnAction(this::showColumnSelection); + HBox topBar = new HBox(5); + topBar.getChildren().addAll(filterInput, count, columnSelection); + count.prefHeightProperty().bind(filterInput.heightProperty()); + count.setAlignment(Pos.CENTER); + layout.setTop(topBar); + BorderPane.setMargin(topBar, new Insets(0, 0, 5, 0)); + + table.setItems(observableModels); + table.getSortOrder().addListener(createSortOrderChangedListener()); + + TableColumn name = createTableColumn("Name", 200, 0); + name.setCellValueFactory(cdf -> { + return new SimpleStringProperty(Optional.ofNullable(cdf.getValue().getNm()).orElse("n/a")); + }); + addTableColumnIfEnabled(name); + + TableColumn state = createTableColumn("State", 130, 1); + state.setCellValueFactory(cdf -> { + String st = Optional.ofNullable(cdf.getValue().getVs()).map(vs -> ctbrec.sites.mfc.State.of(vs).toString()).orElse("n/a"); + return new SimpleStringProperty(st); + }); + addTableColumnIfEnabled(state); + + TableColumn camscore = createTableColumn("Score", 75, 2); + camscore.setCellValueFactory(cdf -> { + Double camScore = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getCamscore()).orElse(0d); + return new SimpleDoubleProperty(camScore); + }); + addTableColumnIfEnabled(camscore); + + TableColumn ethnic = createTableColumn("Ethnicity", 130, 3); + ethnic.setCellValueFactory(cdf -> { + String eth = Optional.ofNullable(cdf.getValue().getU()).map(u -> u.getEthnic()).orElse("n/a"); + return new SimpleStringProperty(eth); + }); + addTableColumnIfEnabled(ethnic); + + TableColumn country = createTableColumn("Country", 160, 4); + country.setCellValueFactory(cdf -> { + String c = Optional.ofNullable(cdf.getValue().getU()).map(u -> u.getCountry()).orElse("n/a"); + return new SimpleStringProperty(c); + }); + addTableColumnIfEnabled(country); + + TableColumn continent = createTableColumn("Continent", 100, 5); + continent.setCellValueFactory(cdf -> { + String c = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getContinent()).orElse("n/a"); + return new SimpleStringProperty(c); + }); + addTableColumnIfEnabled(continent); + + TableColumn occupation = createTableColumn("Occupation", 160, 6); + occupation.setCellValueFactory(cdf -> { + String occ = Optional.ofNullable(cdf.getValue().getU()).map(u -> u.getOccupation()).orElse("n/a"); + return new SimpleStringProperty(occ); + }); + addTableColumnIfEnabled(occupation); + + TableColumn tags = createTableColumn("Tags", 300, 7); + tags.setCellValueFactory(cdf -> { + Set tagSet = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getTags()).orElse(Collections.emptySet()); + if(tagSet.isEmpty()) { + return new SimpleStringProperty(""); + } else { + StringBuilder sb = new StringBuilder(); + for (String t : tagSet) { + sb.append(t).append(',').append(' '); + } + return new SimpleStringProperty(sb.substring(0, sb.length()-2)); + } + }); + addTableColumnIfEnabled(tags); + + TableColumn blurp = createTableColumn("Blurp", 300, 8); + blurp.setCellValueFactory(cdf -> { + String blrp = Optional.ofNullable(cdf.getValue().getU()).map(u -> u.getBlurb()).orElse("n/a"); + return new SimpleStringProperty(blrp); + }); + addTableColumnIfEnabled(blurp); + + TableColumn topic = createTableColumn("Topic", 600, 9); + topic.setCellValueFactory(cdf -> { + String tpc = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getTopic()).orElse("n/a"); + try { + tpc = URLDecoder.decode(tpc, "utf-8"); + } catch (UnsupportedEncodingException e) { + LOG.warn("Couldn't url decode topic", e); + } + return new SimpleStringProperty(tpc); + }); + addTableColumnIfEnabled(topic); + + + scrollPane.setFitToHeight(true); + scrollPane.setFitToWidth(true); + scrollPane.setContent(table); + scrollPane.setStyle("-fx-background-color: -fx-background"); + layout.setCenter(scrollPane); + setContent(layout); + } + + private void addTableColumnIfEnabled(TableColumn tc) { + if(isColumnEnabled(tc)) { + table.getColumns().add(tc); + } + } + + private void filter(String filter) { + lock.lock(); + try { + if (StringUtil.isBlank(filter)) { + observableModels.addAll(filteredModels); + filteredModels.clear(); + return; + } + + String[] tokens = filter.split(" "); + observableModels.addAll(filteredModels); + filteredModels.clear(); + for (int i = 0; i < table.getItems().size(); i++) { + StringBuilder sb = new StringBuilder(); + for (TableColumn tc : table.getColumns()) { + String cellData = tc.getCellData(i).toString(); + sb.append(cellData).append(' '); + } + String searchText = sb.toString(); + + boolean tokensMissing = false; + for (String token : tokens) { + if(!searchText.toLowerCase().contains(token.toLowerCase())) { + tokensMissing = true; + break; + } + } + if(tokensMissing) { + SessionState sessionState = table.getItems().get(i); + filteredModels.add(sessionState); + } + } + observableModels.removeAll(filteredModels); + } finally { + lock.unlock(); + int filtered = filteredModels.size(); + int showing = observableModels.size(); + int total = showing + filtered; + count.setText(showing + "/" + total); + } + } + + private void showColumnSelection(ActionEvent evt) { + ContextMenu menu = new ContextMenu(); + for (TableColumn tc : columns) { + CheckMenuItem item = new CheckMenuItem(tc.getText()); + item.setSelected(isColumnEnabled(tc)); + menu.getItems().add(item); + item.setOnAction(e -> { + if(item.isSelected()) { + Config.getInstance().getSettings().mfcDisabledModelsTableColumns.remove(tc.getText()); + for (int i = table.getColumns().size()-1; i>=0; i--) { + TableColumn other = table.getColumns().get(i); + int idx = (int) tc.getUserData(); + int otherIdx = (int) other.getUserData(); + if(otherIdx < idx) { + table.getColumns().add(i+1, tc); + break; + } + } + } else { + Config.getInstance().getSettings().mfcDisabledModelsTableColumns.add(tc.getText()); + table.getColumns().remove(tc); + } + }); + } + Button src = (Button) evt.getSource(); + Point2D location = src.localToScreen(src.getTranslateX(), src.getTranslateY()); + menu.show(getTabPane().getScene().getWindow(), location.getX(), location.getY() + src.getHeight() + 5); + } + + private boolean isColumnEnabled(TableColumn tc) { + return !Config.getInstance().getSettings().mfcDisabledModelsTableColumns.contains(tc.getText()); + } + + private TableColumn createTableColumn(String text, int width, int idx) { + TableColumn tc = new TableColumn<>(text); + tc.setPrefWidth(width); + tc.sortTypeProperty().addListener((obs, o, n) -> saveState()); + tc.widthProperty().addListener((obs, o, n) -> saveState()); + tc.setUserData(idx); + columns.add(tc); + return tc; + } + + @Override + public void selected() { + if(updateService != null) { + State s = updateService.getState(); + if (s != State.SCHEDULED && s != State.RUNNING) { + updateService.reset(); + updateService.restart(); + } + } + } + + @Override + public void deselected() { + if(updateService != null) { + updateService.cancel(); + } + } + + private void saveState() { + if (!table.getSortOrder().isEmpty()) { + TableColumn col = table.getSortOrder().get(0); + Config.getInstance().getSettings().mfcModelsTableSortColumn = col.getText(); + Config.getInstance().getSettings().mfcModelsTableSortType = col.getSortType().toString(); + } + double[] columnWidths = new double[table.getColumns().size()]; + for (int i = 0; i < columnWidths.length; i++) { + columnWidths[i] = table.getColumns().get(i).getWidth(); + } + Config.getInstance().getSettings().mfcModelsTableColumnWidths = columnWidths; + }; + + private void restoreState() { + String sortCol = Config.getInstance().getSettings().mfcModelsTableSortColumn; + if (StringUtil.isNotBlank(sortCol)) { + for (TableColumn col : table.getColumns()) { + if (Objects.equals(sortCol, col.getText())) { + col.setSortType(SortType.valueOf(Config.getInstance().getSettings().mfcModelsTableSortType)); + table.getSortOrder().clear(); + table.getSortOrder().add(col); + break; + } + } + } + + double[] columnWidths = Config.getInstance().getSettings().mfcModelsTableColumnWidths; + if (columnWidths != null && columnWidths.length == table.getColumns().size()) { + for (int i = 0; i < columnWidths.length; i++) { + table.getColumns().get(i).setPrefWidth(columnWidths[i]); + } + } + + filterInput.setText(Config.getInstance().getSettings().mfcModelsTableFilter); + } + + private ListChangeListener> createSortOrderChangedListener() { + return new ListChangeListener>() { + @Override + public void onChanged(Change> c) { + saveState(); + } + }; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/TableUpdateService.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/TableUpdateService.java new file mode 100644 index 00000000..0ee25c04 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/TableUpdateService.java @@ -0,0 +1,31 @@ +package ctbrec.ui.sites.myfreecams; + +import java.io.IOException; +import java.util.Collection; + +import ctbrec.sites.mfc.MyFreeCams; +import ctbrec.sites.mfc.MyFreeCamsClient; +import ctbrec.sites.mfc.SessionState; +import javafx.concurrent.ScheduledService; +import javafx.concurrent.Task; + +public class TableUpdateService extends ScheduledService> { + + private MyFreeCams mfc; + + public TableUpdateService(MyFreeCams mfc) { + this.mfc = mfc; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public Collection call() throws IOException { + MyFreeCamsClient client = mfc.getClient(); + return client.getSessionStates(); + } + }; + } + +} diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 46998661..384ea432 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -51,6 +51,11 @@ public class Settings { public String mfcUsername = ""; public String mfcPassword = ""; public String mfcBaseUrl = "https://www.myfreecams.com"; + public String mfcModelsTableSortColumn = ""; + public String mfcModelsTableSortType = ""; + public double[] mfcModelsTableColumnWidths = new double[0]; + public String mfcModelsTableFilter = ""; + public List mfcDisabledModelsTableColumns = new ArrayList<>(); public boolean mfcIgnoreUpscaled = false; public String camsodaUsername = ""; public String camsodaPassword = ""; diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index c1bf7fea..9f5c26d6 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -7,6 +7,8 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -627,4 +629,8 @@ public class MyFreeCamsClient { return result; } + + public Collection getSessionStates() { + return Collections.unmodifiableCollection(sessionStates.asMap().values()); + } } From c478f6b0f185de9e9501970c8a8704c34ea54346 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 12 Dec 2018 12:48:45 +0100 Subject: [PATCH 182/231] Make login methods synchronized Add synchronized modifier to the login methods, so that only one thread at a time tries to login. All the following threads then should be able to use the session cookies --- client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java | 2 +- client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java | 2 +- client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaSiteUi.java | 2 +- .../main/java/ctbrec/ui/sites/chaturbate/ChaturbateSiteUi.java | 2 +- .../main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsSiteUi.java | 2 +- common/src/main/java/ctbrec/sites/bonga/BongaCams.java | 2 +- common/src/main/java/ctbrec/sites/cam4/Cam4.java | 2 +- common/src/main/java/ctbrec/sites/camsoda/Camsoda.java | 2 +- common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java | 2 +- common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java index 328528bc..8e123696 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java @@ -44,7 +44,7 @@ public class BongaCamsSiteUi implements SiteUI { } @Override - public boolean login() throws IOException { + public synchronized boolean login() throws IOException { boolean automaticLogin = bongaCams.login(); if(automaticLogin) { return true; diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java index 66c4d776..8e61b411 100644 --- a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java @@ -44,7 +44,7 @@ public class Cam4SiteUi implements SiteUI { } @Override - public boolean login() throws IOException { + public synchronized boolean login() throws IOException { boolean automaticLogin = cam4.login(); if(automaticLogin) { return true; diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaSiteUi.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaSiteUi.java index a86f72e3..af1be6ab 100644 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaSiteUi.java @@ -45,7 +45,7 @@ public class CamsodaSiteUi implements SiteUI { } @Override - public boolean login() throws IOException { + public synchronized boolean login() throws IOException { boolean automaticLogin = camsoda.login(); return automaticLogin; } diff --git a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateSiteUi.java b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateSiteUi.java index fdca9ec5..575ca10f 100644 --- a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateSiteUi.java @@ -30,7 +30,7 @@ public class ChaturbateSiteUi implements SiteUI { } @Override - public boolean login() throws IOException { + public synchronized boolean login() throws IOException { return chaturbate.login(); } diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsSiteUi.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsSiteUi.java index f98528ed..59bb5829 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsSiteUi.java @@ -30,7 +30,7 @@ public class MyFreeCamsSiteUi implements SiteUI { } @Override - public boolean login() throws IOException { + public synchronized boolean login() throws IOException { return myFreeCams.login(); } diff --git a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java index fc847912..573c4d67 100644 --- a/common/src/main/java/ctbrec/sites/bonga/BongaCams.java +++ b/common/src/main/java/ctbrec/sites/bonga/BongaCams.java @@ -94,7 +94,7 @@ public class BongaCams extends AbstractSite { } @Override - public boolean login() throws IOException { + public synchronized boolean login() throws IOException { return credentialsAvailable() && getHttpClient().login(); } diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4.java b/common/src/main/java/ctbrec/sites/cam4/Cam4.java index 8c3907a0..62a1cea2 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4.java @@ -64,7 +64,7 @@ public class Cam4 extends AbstractSite { } @Override - public boolean login() throws IOException { + public synchronized boolean login() throws IOException { return credentialsAvailable() && getHttpClient().login(); } diff --git a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java index 3008f14b..10a12117 100644 --- a/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java +++ b/common/src/main/java/ctbrec/sites/camsoda/Camsoda.java @@ -82,7 +82,7 @@ public class Camsoda extends AbstractSite { } @Override - public boolean login() throws IOException { + public synchronized boolean login() throws IOException { return credentialsAvailable() && getHttpClient().login(); } diff --git a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java index 56dffb1b..d31b7983 100644 --- a/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java +++ b/common/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java @@ -105,7 +105,7 @@ public class Chaturbate extends AbstractSite { } @Override - public boolean login() throws IOException { + public synchronized boolean login() throws IOException { return credentialsAvailable() && getHttpClient().login(); } diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java index 315d040c..787fac8e 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCams.java @@ -31,7 +31,7 @@ public class MyFreeCams extends AbstractSite { } @Override - public boolean login() throws IOException { + public synchronized boolean login() throws IOException { return credentialsAvailable() && getHttpClient().login(); } From c944323aa463c57aaa641d8ae7925c200272f4ea Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 12 Dec 2018 13:25:26 +0100 Subject: [PATCH 183/231] Set min Java version to 10 and change JRE download URL --- client/pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/pom.xml b/client/pom.xml index a71d0d04..62be0878 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -118,10 +118,11 @@ false anything + https://jdk.java.net/ jre true - 1.8.0 + 10 512 From 279852bb33b282113b46b659786fd42bcffdced3 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 12 Dec 2018 13:33:24 +0100 Subject: [PATCH 184/231] Print out environment and version on start --- .../ctbrec/recorder/server/HttpServer.java | 22 +++++++++++++++++++ server/src/main/resources/version | 1 + 2 files changed, 23 insertions(+) create mode 100644 server/src/main/resources/version diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index 60c2dadc..ee57211d 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -1,6 +1,9 @@ package ctbrec.recorder.server; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.net.BindException; import java.util.ArrayList; import java.util.List; @@ -17,6 +20,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Config; +import ctbrec.Version; import ctbrec.event.EventBusHolder; import ctbrec.event.EventHandler; import ctbrec.event.EventHandlerConfiguration; @@ -40,6 +44,7 @@ public class HttpServer { private List sites = new ArrayList<>(); public HttpServer() throws Exception { + logEnvironment(); createSites(); System.setProperty("ctbrec.server.mode", "1"); if(System.getProperty("ctbrec.config") == null) { @@ -147,6 +152,23 @@ public class HttpServer { LOG.debug("Alert System registered"); } + private void logEnvironment() { + LOG.debug("OS:\t{} {}", System.getProperty("os.name"), System.getProperty("os.version")); + LOG.debug("Java:\t{} {} {}", System.getProperty("java.vendor"), System.getProperty("java.vm.name"), System.getProperty("java.version")); + try { + LOG.debug("ctbrec server {}", getVersion().toString()); + } catch (IOException e) {} + } + + private Version getVersion() throws IOException { + try (InputStream is = getClass().getClassLoader().getResourceAsStream("version")) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String versionString = reader.readLine(); + Version version = Version.of(versionString); + return version; + } + } + public static void main(String[] args) throws Exception { new HttpServer(); } diff --git a/server/src/main/resources/version b/server/src/main/resources/version new file mode 100644 index 00000000..f2ab45c3 --- /dev/null +++ b/server/src/main/resources/version @@ -0,0 +1 @@ +${project.version} \ No newline at end of file From f32990b9d7088581d1be85af6fb5d653c5f5687b Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 12 Dec 2018 16:52:29 +0100 Subject: [PATCH 185/231] Created action classes for actions on Recording tab --- .../java/ctbrec/ui/RecordedModelsTab.java | 83 ++---------- .../java/ctbrec/ui/action/FollowAction.java | 29 ++++ .../ctbrec/ui/action/ModelMassEditAction.java | 52 ++++++++ .../java/ctbrec/ui/action/PauseAction.java | 24 ++++ .../java/ctbrec/ui/action/ResumeAction.java | 24 ++++ .../ctbrec/ui/action/StopRecordingAction.java | 24 ++++ .../java/ctbrec/ui/settings/SettingsTab.java | 1 + .../sites/myfreecams/MyFreeCamsTableTab.java | 124 ++++++++++++++++-- 8 files changed, 280 insertions(+), 81 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/action/FollowAction.java create mode 100644 client/src/main/java/ctbrec/ui/action/ModelMassEditAction.java create mode 100644 client/src/main/java/ctbrec/ui/action/PauseAction.java create mode 100644 client/src/main/java/ctbrec/ui/action/ResumeAction.java create mode 100644 client/src/main/java/ctbrec/ui/action/StopRecordingAction.java diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index c4276e8a..e03695eb 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -16,7 +16,6 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -29,6 +28,10 @@ import ctbrec.Recording; import ctbrec.StringUtil; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; +import ctbrec.ui.action.FollowAction; +import ctbrec.ui.action.PauseAction; +import ctbrec.ui.action.ResumeAction; +import ctbrec.ui.action.StopRecordingAction; import ctbrec.ui.controls.AutoFillTextField; import ctbrec.ui.controls.Toast; import javafx.application.Platform; @@ -272,39 +275,11 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } private void pauseAll(ActionEvent evt) { - List models = recorder.getModelsRecording(); - Consumer action = (m) -> { - try { - recorder.suspendRecording(m); - } catch(Exception e) { - Platform.runLater(() -> - showErrorDialog(e, "Couldn't suspend recording of model", "Suspending recording of " + m.getName() + " failed")); - } - }; - massEdit(models, action); + new PauseAction(getTabPane(), recorder.getModelsRecording(), recorder).execute(); } private void resumeAll(ActionEvent evt) { - List models = recorder.getModelsRecording(); - Consumer action = (m) -> { - try { - recorder.resumeRecording(m); - } catch(Exception e) { - Platform.runLater(() -> - showErrorDialog(e, "Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed")); - } - }; - massEdit(models, action); - } - - private void massEdit(List models, Consumer action) { - table.setCursor(Cursor.WAIT); - threadPool.submit(() -> { - for (Model model : models) { - action.accept(model); - } - Platform.runLater(() -> table.setCursor(Cursor.DEFAULT)); - }); + new ResumeAction(getTabPane(), recorder.getModelsRecording(), recorder).execute(); } void initializeUpdateService() { @@ -466,16 +441,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } private void follow(ObservableList selectedModels) { - Consumer action = (m) -> { - try { - m.follow(); - } catch(Throwable e) { - LOG.error("Couldn't follow model {}", m, e); - Platform.runLater(() -> - showErrorDialog(e, "Couldn't follow model", "Following " + m.getName() + " failed: " + e.getMessage())); - } - }; - massEdit(new ArrayList(selectedModels), action); + new FollowAction(getTabPane(), new ArrayList(selectedModels)).execute(); } private void openInPlayer(JavaFxModel selectedModel) { @@ -540,45 +506,20 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } private void stopAction(List selectedModels) { - Consumer action = (m) -> { - try { - recorder.stopRecording(m); - observableModels.remove(m); - } catch(Exception e) { - Platform.runLater(() -> - showErrorDialog(e, "Couldn't stop recording", "Stopping recording of " + m.getName() + " failed")); - } - }; List models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList()); - massEdit(models, action); + new StopRecordingAction(getTabPane(), models, recorder).execute((m) -> { + observableModels.remove(m); + }); }; private void pauseRecording(List selectedModels) { - Consumer action = (m) -> { - try { - recorder.suspendRecording(m); - m.setSuspended(true); - } catch(Exception e) { - Platform.runLater(() -> - showErrorDialog(e, "Couldn't pause recording of model", "Pausing recording of " + m.getName() + " failed")); - } - }; List models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList()); - massEdit(models, action); + new PauseAction(getTabPane(), models, recorder).execute(); }; private void resumeRecording(List selectedModels) { - Consumer action = (m) -> { - try { - recorder.resumeRecording(m); - m.setSuspended(false); - } catch(Exception e) { - Platform.runLater(() -> - showErrorDialog(e, "Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed")); - } - }; List models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList()); - massEdit(models, action); + new ResumeAction(getTabPane(), models, recorder).execute(); } public void saveState() { diff --git a/client/src/main/java/ctbrec/ui/action/FollowAction.java b/client/src/main/java/ctbrec/ui/action/FollowAction.java new file mode 100644 index 00000000..d5bec925 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/FollowAction.java @@ -0,0 +1,29 @@ +package ctbrec.ui.action; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Model; +import ctbrec.ui.controls.Dialogs; +import javafx.application.Platform; +import javafx.scene.Node; + +public class FollowAction extends ModelMassEditAction { + + private static final transient Logger LOG = LoggerFactory.getLogger(FollowAction.class); + + public FollowAction(Node source, List models) { + super(source, models); + action = (m) -> { + try { + m.follow(); + } catch(Exception e) { + LOG.error("Couldn't follow model {}", m, e); + Platform.runLater(() -> + Dialogs.showError("Couldn't follow model", "Following " + m.getName() + " failed: " + e.getMessage(), e)); + } + }; + } +} diff --git a/client/src/main/java/ctbrec/ui/action/ModelMassEditAction.java b/client/src/main/java/ctbrec/ui/action/ModelMassEditAction.java new file mode 100644 index 00000000..7da778be --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/ModelMassEditAction.java @@ -0,0 +1,52 @@ +package ctbrec.ui.action; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import ctbrec.Model; +import javafx.application.Platform; +import javafx.scene.Cursor; +import javafx.scene.Node; + +public class ModelMassEditAction { + + static BlockingQueue queue = new LinkedBlockingQueue<>(); + static ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 10, TimeUnit.MINUTES, queue); + + protected List models; + protected Consumer action; + protected Node source; + + protected ModelMassEditAction(Node source, List models) { + this.source = source; + this.models = models; + } + + public ModelMassEditAction(Node source, List models, Consumer action) { + this.source = source; + this.models = models; + this.action = action; + } + + public void execute() { + execute((m) -> {}); + } + + public void execute(Consumer callback) { + Consumer cb = Objects.requireNonNull(callback); + source.setCursor(Cursor.WAIT); + threadPool.submit(() -> { + for (Model model : models) { + action.accept(model); + cb.accept(model); + } + Platform.runLater(() -> source.setCursor(Cursor.DEFAULT)); + }); + } +} diff --git a/client/src/main/java/ctbrec/ui/action/PauseAction.java b/client/src/main/java/ctbrec/ui/action/PauseAction.java new file mode 100644 index 00000000..c1aea4fb --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/PauseAction.java @@ -0,0 +1,24 @@ +package ctbrec.ui.action; + +import java.util.List; + +import ctbrec.Model; +import ctbrec.recorder.Recorder; +import ctbrec.ui.controls.Dialogs; +import javafx.application.Platform; +import javafx.scene.Node; + +public class PauseAction extends ModelMassEditAction { + + public PauseAction(Node source, List models, Recorder recorder) { + super(source, models); + action = (m) -> { + try { + recorder.suspendRecording(m); + } catch(Exception e) { + Platform.runLater(() -> + Dialogs.showError("Couldn't suspend recording of model", "Suspending recording of " + m.getName() + " failed", e)); + } + }; + } +} diff --git a/client/src/main/java/ctbrec/ui/action/ResumeAction.java b/client/src/main/java/ctbrec/ui/action/ResumeAction.java new file mode 100644 index 00000000..2215a67f --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/ResumeAction.java @@ -0,0 +1,24 @@ +package ctbrec.ui.action; + +import java.util.List; + +import ctbrec.Model; +import ctbrec.recorder.Recorder; +import ctbrec.ui.controls.Dialogs; +import javafx.application.Platform; +import javafx.scene.Node; + +public class ResumeAction extends ModelMassEditAction { + + public ResumeAction(Node source, List models, Recorder recorder) { + super(source, models); + action = (m) -> { + try { + recorder.resumeRecording(m); + } catch(Exception e) { + Platform.runLater(() -> + Dialogs.showError("Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed", e)); + } + }; + } +} diff --git a/client/src/main/java/ctbrec/ui/action/StopRecordingAction.java b/client/src/main/java/ctbrec/ui/action/StopRecordingAction.java new file mode 100644 index 00000000..a4dcab38 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/StopRecordingAction.java @@ -0,0 +1,24 @@ +package ctbrec.ui.action; + +import java.util.List; + +import ctbrec.Model; +import ctbrec.recorder.Recorder; +import ctbrec.ui.controls.Dialogs; +import javafx.application.Platform; +import javafx.scene.Node; + +public class StopRecordingAction extends ModelMassEditAction { + + public StopRecordingAction(Node source, List models, Recorder recorder) { + super(source, models); + action = (m) -> { + try { + recorder.stopRecording(m); + } catch(Exception e) { + Platform.runLater(() -> + Dialogs.showError("Couldn't stop recording", "Stopping recording of " + m.getName() + " failed", e)); + } + }; + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index fc31091a..d9630ff9 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -307,6 +307,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(new Label("Post-Processing"), 0, row); + // TODO allow empty strings to remove post-processing scripts postProcessing = new ProgramSelectionBox(Config.getInstance().getSettings().postProcessing); postProcessing.fileProperty().addListener((obs, o, n) -> { String path = n.getAbsolutePath(); diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java index 9a01c796..8f19639b 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java @@ -16,11 +16,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Config; +import ctbrec.Model; import ctbrec.StringUtil; import ctbrec.sites.mfc.MyFreeCams; +import ctbrec.sites.mfc.MyFreeCamsModel; import ctbrec.sites.mfc.SessionState; +import ctbrec.ui.DesktopIntegration; +import ctbrec.ui.Player; import ctbrec.ui.TabSelectionListener; import ctbrec.ui.controls.SearchBox; +import ctbrec.ui.controls.Toast; +import javafx.application.Platform; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; @@ -32,15 +38,21 @@ import javafx.event.ActionEvent; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.geometry.Pos; +import javafx.scene.Cursor; import javafx.scene.control.Button; import javafx.scene.control.CheckMenuItem; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn.SortType; import javafx.scene.control.TableView; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.ContextMenuEvent; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; @@ -58,6 +70,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { private SearchBox filterInput; private Label count = new Label("models"); private List> columns = new ArrayList<>(); + private ContextMenu popup; public MyFreeCamsTableTab(MyFreeCams mfc) { this.mfc = mfc; @@ -140,56 +153,84 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { table.setItems(observableModels); table.getSortOrder().addListener(createSortOrderChangedListener()); + table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { + popup = createContextMenu(); + if (popup != null) { + popup.show(table, event.getScreenX(), event.getScreenY()); + } + event.consume(); + }); + table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { + if (popup != null) { + popup.hide(); + } + }); - TableColumn name = createTableColumn("Name", 200, 0); + int idx = 0; + TableColumn name = createTableColumn("Name", 200, idx++); name.setCellValueFactory(cdf -> { return new SimpleStringProperty(Optional.ofNullable(cdf.getValue().getNm()).orElse("n/a")); }); addTableColumnIfEnabled(name); - TableColumn state = createTableColumn("State", 130, 1); + TableColumn state = createTableColumn("State", 130, idx++); state.setCellValueFactory(cdf -> { String st = Optional.ofNullable(cdf.getValue().getVs()).map(vs -> ctbrec.sites.mfc.State.of(vs).toString()).orElse("n/a"); return new SimpleStringProperty(st); }); addTableColumnIfEnabled(state); - TableColumn camscore = createTableColumn("Score", 75, 2); + TableColumn camscore = createTableColumn("Score", 75, idx++); camscore.setCellValueFactory(cdf -> { Double camScore = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getCamscore()).orElse(0d); return new SimpleDoubleProperty(camScore); }); addTableColumnIfEnabled(camscore); - TableColumn ethnic = createTableColumn("Ethnicity", 130, 3); + // this is always 0, use https://api.myfreecams.com/missmfc and https://api.myfreecams.com/missmfc/online + // TableColumn missMfc = createTableColumn("Miss MFC", 75, idx++); + // missMfc.setCellValueFactory(cdf -> { + // Integer mmfc = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getMissmfc()).orElse(-1); + // return new SimpleIntegerProperty(mmfc); + // }); + // addTableColumnIfEnabled(missMfc); + + TableColumn newModel = createTableColumn("New", 60, idx++); + newModel.setCellValueFactory(cdf -> { + Integer nu = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getNewModel()).orElse(0); + return new SimpleStringProperty(nu == 1 ? "new" : ""); + }); + addTableColumnIfEnabled(newModel); + + TableColumn ethnic = createTableColumn("Ethnicity", 130, idx++); ethnic.setCellValueFactory(cdf -> { String eth = Optional.ofNullable(cdf.getValue().getU()).map(u -> u.getEthnic()).orElse("n/a"); return new SimpleStringProperty(eth); }); addTableColumnIfEnabled(ethnic); - TableColumn country = createTableColumn("Country", 160, 4); + TableColumn country = createTableColumn("Country", 160, idx++); country.setCellValueFactory(cdf -> { String c = Optional.ofNullable(cdf.getValue().getU()).map(u -> u.getCountry()).orElse("n/a"); return new SimpleStringProperty(c); }); addTableColumnIfEnabled(country); - TableColumn continent = createTableColumn("Continent", 100, 5); + TableColumn continent = createTableColumn("Continent", 100, idx++); continent.setCellValueFactory(cdf -> { String c = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getContinent()).orElse("n/a"); return new SimpleStringProperty(c); }); addTableColumnIfEnabled(continent); - TableColumn occupation = createTableColumn("Occupation", 160, 6); + TableColumn occupation = createTableColumn("Occupation", 160, idx++); occupation.setCellValueFactory(cdf -> { String occ = Optional.ofNullable(cdf.getValue().getU()).map(u -> u.getOccupation()).orElse("n/a"); return new SimpleStringProperty(occ); }); addTableColumnIfEnabled(occupation); - TableColumn tags = createTableColumn("Tags", 300, 7); + TableColumn tags = createTableColumn("Tags", 300, idx++); tags.setCellValueFactory(cdf -> { Set tagSet = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getTags()).orElse(Collections.emptySet()); if(tagSet.isEmpty()) { @@ -204,14 +245,14 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { }); addTableColumnIfEnabled(tags); - TableColumn blurp = createTableColumn("Blurp", 300, 8); + TableColumn blurp = createTableColumn("Blurp", 300, idx++); blurp.setCellValueFactory(cdf -> { String blrp = Optional.ofNullable(cdf.getValue().getU()).map(u -> u.getBlurb()).orElse("n/a"); return new SimpleStringProperty(blrp); }); addTableColumnIfEnabled(blurp); - TableColumn topic = createTableColumn("Topic", 600, 9); + TableColumn topic = createTableColumn("Topic", 600, idx++); topic.setCellValueFactory(cdf -> { String tpc = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getTopic()).orElse("n/a"); try { @@ -232,6 +273,69 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { setContent(layout); } + private ContextMenu createContextMenu() { + ObservableList selectedStates = table.getSelectionModel().getSelectedItems(); + if (selectedStates.isEmpty()) { + return null; + } + + List selectedModels = new ArrayList<>(); + for (SessionState sessionState : selectedStates) { + if(sessionState.getNm() != null) { + MyFreeCamsModel model = mfc.createModel(sessionState.getNm()); + mfc.getClient().update(model); + selectedModels.add(model); + } + } + + MenuItem copyUrl = new MenuItem("Copy URL"); + copyUrl.setOnAction((e) -> { + Model selected = selectedModels.get(0); + final Clipboard clipboard = Clipboard.getSystemClipboard(); + final ClipboardContent content = new ClipboardContent(); + content.putString(selected.getUrl()); + clipboard.setContent(content); + }); + + // MenuItem resumeRecording = new MenuItem("Record"); + // resumeRecording.setOnAction((e) -> resumeRecording(selectedModels)); + MenuItem openInBrowser = new MenuItem("Open in Browser"); + openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModels.get(0).getUrl())); + MenuItem openInPlayer = new MenuItem("Open in Player"); + openInPlayer.setOnAction((e) -> openInPlayer(selectedModels.get(0))); + MenuItem follow = new MenuItem("Follow"); + follow.setOnAction((e) -> follow(selectedModels)); + + ContextMenu menu = new ContextMenu(); + menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, follow); + + if (selectedModels.size() > 1) { + copyUrl.setDisable(true); + openInPlayer.setDisable(true); + openInBrowser.setDisable(true); + } + + return menu; + } + + private Object follow(List selectedModels) { + // TODO Auto-generated method stub + return null; + } + + private void openInPlayer(Model selectedModel) { + table.setCursor(Cursor.WAIT); + new Thread(() -> { + boolean started = Player.play(selectedModel); + Platform.runLater(() -> { + if (started && Config.getInstance().getSettings().showPlayerStarting) { + Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500); + } + table.setCursor(Cursor.DEFAULT); + }); + }).start(); + } + private void addTableColumnIfEnabled(TableColumn tc) { if(isColumnEnabled(tc)) { table.getColumns().add(tc); From a68341de82100390505947c6886850e662c03154 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 12 Dec 2018 18:08:39 +0100 Subject: [PATCH 186/231] Add actions to MFC's table view --- .../ui/action/StartRecordingAction.java | 24 ++ .../sites/myfreecams/MyFreeCamsTableTab.java | 294 ++++++++++++------ .../ctbrec/sites/mfc/MyFreeCamsModel.java | 1 + 3 files changed, 216 insertions(+), 103 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/action/StartRecordingAction.java diff --git a/client/src/main/java/ctbrec/ui/action/StartRecordingAction.java b/client/src/main/java/ctbrec/ui/action/StartRecordingAction.java new file mode 100644 index 00000000..56e3e67c --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/StartRecordingAction.java @@ -0,0 +1,24 @@ +package ctbrec.ui.action; + +import java.util.List; + +import ctbrec.Model; +import ctbrec.recorder.Recorder; +import ctbrec.ui.controls.Dialogs; +import javafx.application.Platform; +import javafx.scene.Node; + +public class StartRecordingAction extends ModelMassEditAction { + + public StartRecordingAction(Node source, List models, Recorder recorder) { + super(source, models); + action = (m) -> { + try { + recorder.startRecording(m); + } catch(Exception e) { + Platform.runLater(() -> + Dialogs.showError("Couldn't start recording", "Starting recording of " + m.getName() + " failed", e)); + } + }; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java index 8f19639b..b83b8551 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java @@ -24,11 +24,15 @@ import ctbrec.sites.mfc.SessionState; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.Player; import ctbrec.ui.TabSelectionListener; +import ctbrec.ui.action.FollowAction; +import ctbrec.ui.action.StartRecordingAction; import ctbrec.ui.controls.SearchBox; import ctbrec.ui.controls.Toast; import javafx.application.Platform; +import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -45,6 +49,7 @@ import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; +import javafx.scene.control.SelectionMode; import javafx.scene.control.Tab; import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn.SortType; @@ -61,15 +66,15 @@ import javafx.util.Duration; public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsTableTab.class); private ScrollPane scrollPane = new ScrollPane(); - private TableView table = new TableView(); - private ObservableList filteredModels = FXCollections.observableArrayList(); - private ObservableList observableModels = FXCollections.observableArrayList(); + private TableView table = new TableView(); + private ObservableList filteredModels = FXCollections.observableArrayList(); + private ObservableList observableModels = FXCollections.observableArrayList(); private TableUpdateService updateService; private MyFreeCams mfc; private ReentrantLock lock = new ReentrantLock(); private SearchBox filterInput; private Label count = new Label("models"); - private List> columns = new ArrayList<>(); + private List> columns = new ArrayList<>(); private ContextMenu popup; public MyFreeCamsTableTab(MyFreeCams mfc) { @@ -100,17 +105,25 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { lock.lock(); try { for (SessionState updatedModel : sessionStates) { - int index = observableModels.indexOf(updatedModel); + ModelTableRow row = new ModelTableRow(updatedModel); + int index = observableModels.indexOf(row); if (index == -1) { - observableModels.add(updatedModel); + observableModels.add(row); } else { - observableModels.set(index, updatedModel); + observableModels.get(index).update(updatedModel); } } - for (Iterator iterator = observableModels.iterator(); iterator.hasNext();) { - SessionState model = iterator.next(); - if (!sessionStates.contains(model)) { + for (Iterator iterator = observableModels.iterator(); iterator.hasNext();) { + ModelTableRow model = iterator.next(); + boolean found = false; + for (SessionState sessionState : sessionStates) { + if(Objects.equals(sessionState.getUid(), model.uid)) { + found = true; + break; + } + } + if(!found) { iterator.remove(); } } @@ -152,6 +165,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { BorderPane.setMargin(topBar, new Insets(0, 0, 5, 0)); table.setItems(observableModels); + table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); table.getSortOrder().addListener(createSortOrderChangedListener()); table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { popup = createContextMenu(); @@ -167,24 +181,16 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { }); int idx = 0; - TableColumn name = createTableColumn("Name", 200, idx++); - name.setCellValueFactory(cdf -> { - return new SimpleStringProperty(Optional.ofNullable(cdf.getValue().getNm()).orElse("n/a")); - }); + TableColumn name = createTableColumn("Name", 200, idx++); + name.setCellValueFactory(cdf -> cdf.getValue().nameProperty()); addTableColumnIfEnabled(name); - TableColumn state = createTableColumn("State", 130, idx++); - state.setCellValueFactory(cdf -> { - String st = Optional.ofNullable(cdf.getValue().getVs()).map(vs -> ctbrec.sites.mfc.State.of(vs).toString()).orElse("n/a"); - return new SimpleStringProperty(st); - }); + TableColumn state = createTableColumn("State", 130, idx++); + state.setCellValueFactory(cdf -> cdf.getValue().stateProperty()); addTableColumnIfEnabled(state); - TableColumn camscore = createTableColumn("Score", 75, idx++); - camscore.setCellValueFactory(cdf -> { - Double camScore = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getCamscore()).orElse(0d); - return new SimpleDoubleProperty(camScore); - }); + TableColumn camscore = createTableColumn("Score", 75, idx++); + camscore.setCellValueFactory(cdf -> cdf.getValue().camScoreProperty()); addTableColumnIfEnabled(camscore); // this is always 0, use https://api.myfreecams.com/missmfc and https://api.myfreecams.com/missmfc/online @@ -195,76 +201,38 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { // }); // addTableColumnIfEnabled(missMfc); - TableColumn newModel = createTableColumn("New", 60, idx++); - newModel.setCellValueFactory(cdf -> { - Integer nu = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getNewModel()).orElse(0); - return new SimpleStringProperty(nu == 1 ? "new" : ""); - }); + TableColumn newModel = createTableColumn("New", 60, idx++); + newModel.setCellValueFactory(cdf -> cdf.getValue().newModelProperty()); addTableColumnIfEnabled(newModel); - TableColumn ethnic = createTableColumn("Ethnicity", 130, idx++); - ethnic.setCellValueFactory(cdf -> { - String eth = Optional.ofNullable(cdf.getValue().getU()).map(u -> u.getEthnic()).orElse("n/a"); - return new SimpleStringProperty(eth); - }); + TableColumn ethnic = createTableColumn("Ethnicity", 130, idx++); + ethnic.setCellValueFactory(cdf -> cdf.getValue().ethnicityProperty()); addTableColumnIfEnabled(ethnic); - TableColumn country = createTableColumn("Country", 160, idx++); - country.setCellValueFactory(cdf -> { - String c = Optional.ofNullable(cdf.getValue().getU()).map(u -> u.getCountry()).orElse("n/a"); - return new SimpleStringProperty(c); - }); + TableColumn country = createTableColumn("Country", 160, idx++); + country.setCellValueFactory(cdf -> cdf.getValue().countryProperty()); addTableColumnIfEnabled(country); - TableColumn continent = createTableColumn("Continent", 100, idx++); - continent.setCellValueFactory(cdf -> { - String c = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getContinent()).orElse("n/a"); - return new SimpleStringProperty(c); - }); + TableColumn continent = createTableColumn("Continent", 100, idx++); + continent.setCellValueFactory(cdf -> cdf.getValue().continentProperty()); addTableColumnIfEnabled(continent); - TableColumn occupation = createTableColumn("Occupation", 160, idx++); - occupation.setCellValueFactory(cdf -> { - String occ = Optional.ofNullable(cdf.getValue().getU()).map(u -> u.getOccupation()).orElse("n/a"); - return new SimpleStringProperty(occ); - }); + TableColumn occupation = createTableColumn("Occupation", 160, idx++); + occupation.setCellValueFactory(cdf -> cdf.getValue().occupationProperty()); addTableColumnIfEnabled(occupation); - TableColumn tags = createTableColumn("Tags", 300, idx++); - tags.setCellValueFactory(cdf -> { - Set tagSet = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getTags()).orElse(Collections.emptySet()); - if(tagSet.isEmpty()) { - return new SimpleStringProperty(""); - } else { - StringBuilder sb = new StringBuilder(); - for (String t : tagSet) { - sb.append(t).append(',').append(' '); - } - return new SimpleStringProperty(sb.substring(0, sb.length()-2)); - } - }); + TableColumn tags = createTableColumn("Tags", 300, idx++); + tags.setCellValueFactory(cdf -> cdf.getValue().tagsProperty()); addTableColumnIfEnabled(tags); - TableColumn blurp = createTableColumn("Blurp", 300, idx++); - blurp.setCellValueFactory(cdf -> { - String blrp = Optional.ofNullable(cdf.getValue().getU()).map(u -> u.getBlurb()).orElse("n/a"); - return new SimpleStringProperty(blrp); - }); + TableColumn blurp = createTableColumn("Blurp", 300, idx++); + blurp.setCellValueFactory(cdf -> cdf.getValue().blurpProperty()); addTableColumnIfEnabled(blurp); - TableColumn topic = createTableColumn("Topic", 600, idx++); - topic.setCellValueFactory(cdf -> { - String tpc = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getTopic()).orElse("n/a"); - try { - tpc = URLDecoder.decode(tpc, "utf-8"); - } catch (UnsupportedEncodingException e) { - LOG.warn("Couldn't url decode topic", e); - } - return new SimpleStringProperty(tpc); - }); + TableColumn topic = createTableColumn("Topic", 600, idx++); + topic.setCellValueFactory(cdf -> cdf.getValue().topicProperty()); addTableColumnIfEnabled(topic); - scrollPane.setFitToHeight(true); scrollPane.setFitToWidth(true); scrollPane.setContent(table); @@ -274,15 +242,15 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { } private ContextMenu createContextMenu() { - ObservableList selectedStates = table.getSelectionModel().getSelectedItems(); + ObservableList selectedStates = table.getSelectionModel().getSelectedItems(); if (selectedStates.isEmpty()) { return null; } List selectedModels = new ArrayList<>(); - for (SessionState sessionState : selectedStates) { - if(sessionState.getNm() != null) { - MyFreeCamsModel model = mfc.createModel(sessionState.getNm()); + for (ModelTableRow sessionState : selectedStates) { + if(sessionState.name.get() != null) { + MyFreeCamsModel model = mfc.createModel(sessionState.name.get()); mfc.getClient().update(model); selectedModels.add(model); } @@ -297,17 +265,17 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { clipboard.setContent(content); }); - // MenuItem resumeRecording = new MenuItem("Record"); - // resumeRecording.setOnAction((e) -> resumeRecording(selectedModels)); + MenuItem startRecording = new MenuItem("Record"); + startRecording.setOnAction((e) -> startRecording(selectedModels)); MenuItem openInBrowser = new MenuItem("Open in Browser"); openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModels.get(0).getUrl())); MenuItem openInPlayer = new MenuItem("Open in Player"); openInPlayer.setOnAction((e) -> openInPlayer(selectedModels.get(0))); MenuItem follow = new MenuItem("Follow"); - follow.setOnAction((e) -> follow(selectedModels)); + follow.setOnAction((e) -> new FollowAction(getTabPane(), selectedModels).execute()); ContextMenu menu = new ContextMenu(); - menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, follow); + menu.getItems().addAll(startRecording, copyUrl, openInPlayer, openInBrowser, follow); if (selectedModels.size() > 1) { copyUrl.setDisable(true); @@ -318,9 +286,8 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { return menu; } - private Object follow(List selectedModels) { - // TODO Auto-generated method stub - return null; + private void startRecording(List selectedModels) { + new StartRecordingAction(getTabPane(), selectedModels, mfc.getRecorder()).execute(); } private void openInPlayer(Model selectedModel) { @@ -336,7 +303,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { }).start(); } - private void addTableColumnIfEnabled(TableColumn tc) { + private void addTableColumnIfEnabled(TableColumn tc) { if(isColumnEnabled(tc)) { table.getColumns().add(tc); } @@ -356,7 +323,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { filteredModels.clear(); for (int i = 0; i < table.getItems().size(); i++) { StringBuilder sb = new StringBuilder(); - for (TableColumn tc : table.getColumns()) { + for (TableColumn tc : table.getColumns()) { String cellData = tc.getCellData(i).toString(); sb.append(cellData).append(' '); } @@ -370,7 +337,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { } } if(tokensMissing) { - SessionState sessionState = table.getItems().get(i); + ModelTableRow sessionState = table.getItems().get(i); filteredModels.add(sessionState); } } @@ -386,7 +353,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { private void showColumnSelection(ActionEvent evt) { ContextMenu menu = new ContextMenu(); - for (TableColumn tc : columns) { + for (TableColumn tc : columns) { CheckMenuItem item = new CheckMenuItem(tc.getText()); item.setSelected(isColumnEnabled(tc)); menu.getItems().add(item); @@ -394,7 +361,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { if(item.isSelected()) { Config.getInstance().getSettings().mfcDisabledModelsTableColumns.remove(tc.getText()); for (int i = table.getColumns().size()-1; i>=0; i--) { - TableColumn other = table.getColumns().get(i); + TableColumn other = table.getColumns().get(i); int idx = (int) tc.getUserData(); int otherIdx = (int) other.getUserData(); if(otherIdx < idx) { @@ -413,12 +380,12 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { menu.show(getTabPane().getScene().getWindow(), location.getX(), location.getY() + src.getHeight() + 5); } - private boolean isColumnEnabled(TableColumn tc) { + private boolean isColumnEnabled(TableColumn tc) { return !Config.getInstance().getSettings().mfcDisabledModelsTableColumns.contains(tc.getText()); } - private TableColumn createTableColumn(String text, int width, int idx) { - TableColumn tc = new TableColumn<>(text); + private TableColumn createTableColumn(String text, int width, int idx) { + TableColumn tc = new TableColumn<>(text); tc.setPrefWidth(width); tc.sortTypeProperty().addListener((obs, o, n) -> saveState()); tc.widthProperty().addListener((obs, o, n) -> saveState()); @@ -447,7 +414,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { private void saveState() { if (!table.getSortOrder().isEmpty()) { - TableColumn col = table.getSortOrder().get(0); + TableColumn col = table.getSortOrder().get(0); Config.getInstance().getSettings().mfcModelsTableSortColumn = col.getText(); Config.getInstance().getSettings().mfcModelsTableSortType = col.getSortType().toString(); } @@ -461,7 +428,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { private void restoreState() { String sortCol = Config.getInstance().getSettings().mfcModelsTableSortColumn; if (StringUtil.isNotBlank(sortCol)) { - for (TableColumn col : table.getColumns()) { + for (TableColumn col : table.getColumns()) { if (Objects.equals(sortCol, col.getText())) { col.setSortType(SortType.valueOf(Config.getInstance().getSettings().mfcModelsTableSortType)); table.getSortOrder().clear(); @@ -481,12 +448,133 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { filterInput.setText(Config.getInstance().getSettings().mfcModelsTableFilter); } - private ListChangeListener> createSortOrderChangedListener() { - return new ListChangeListener>() { + private ListChangeListener> createSortOrderChangedListener() { + return new ListChangeListener>() { @Override - public void onChanged(Change> c) { + public void onChanged(Change> c) { saveState(); } }; } -} + + private static class ModelTableRow { + private Integer uid; + private StringProperty name = new SimpleStringProperty(); + private StringProperty state = new SimpleStringProperty(); + private DoubleProperty camScore = new SimpleDoubleProperty(); + private StringProperty newModel = new SimpleStringProperty(); + private StringProperty ethnic = new SimpleStringProperty(); + private StringProperty country = new SimpleStringProperty(); + private StringProperty continent = new SimpleStringProperty(); + private StringProperty occupation = new SimpleStringProperty(); + private StringProperty tags = new SimpleStringProperty(); + private StringProperty blurp = new SimpleStringProperty(); + private StringProperty topic = new SimpleStringProperty(); + + public ModelTableRow(SessionState st) { + update(st); + } + + public void update(SessionState st) { + uid = st.getUid(); + name.set(Optional.ofNullable(st.getNm()).orElse("n/a")); + state.set(Optional.ofNullable(st.getVs()).map(vs -> ctbrec.sites.mfc.State.of(vs).toString()).orElse("n/a")); + camScore.set(Optional.ofNullable(st.getM()).map(m -> m.getCamscore()).orElse(0d)); + Integer nu = Optional.ofNullable(st.getM()).map(m -> m.getNewModel()).orElse(0); + newModel.set(nu == 1 ? "new" : ""); + ethnic.set(Optional.ofNullable(st.getU()).map(u -> u.getEthnic()).orElse("n/a")); + country.set(Optional.ofNullable(st.getU()).map(u -> u.getCountry()).orElse("n/a")); + continent.set(Optional.ofNullable(st.getM()).map(m -> m.getContinent()).orElse("n/a")); + occupation.set(Optional.ofNullable(st.getU()).map(u -> u.getOccupation()).orElse("n/a")); + Set tagSet = Optional.ofNullable(st.getM()).map(m -> m.getTags()).orElse(Collections.emptySet()); + if(tagSet.isEmpty()) { + tags.set(""); + } else { + StringBuilder sb = new StringBuilder(); + for (String t : tagSet) { + sb.append(t).append(',').append(' '); + } + tags.set(sb.substring(0, sb.length()-2)); + } + blurp.set(Optional.ofNullable(st.getU()).map(u -> u.getBlurb()).orElse("n/a")); + String tpc = Optional.ofNullable(st.getM()).map(m -> m.getTopic()).orElse("n/a"); + try { + tpc = URLDecoder.decode(tpc, "utf-8"); + } catch (UnsupportedEncodingException e) { + LOG.warn("Couldn't url decode topic", e); + } + topic.set(tpc); + } + + public StringProperty nameProperty() { + return name; + }; + + public StringProperty stateProperty() { + return state; + }; + + public DoubleProperty camScoreProperty() { + return camScore; + }; + + public StringProperty newModelProperty() { + return newModel; + }; + + public StringProperty ethnicityProperty() { + return ethnic; + }; + + public StringProperty countryProperty() { + return country; + }; + + public StringProperty continentProperty() { + return continent; + }; + + public StringProperty occupationProperty() { + return occupation; + }; + + public StringProperty tagsProperty() { + return tags; + }; + + public StringProperty blurpProperty() { + return blurp; + }; + + public StringProperty topicProperty() { + return topic; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((uid == null) ? 0 : uid.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ModelTableRow other = (ModelTableRow) obj; + if (uid == null) { + if (other.uid != null) + return false; + } else if (!uid.equals(other.uid)) + return false; + return true; + }; + + + } +} \ No newline at end of file diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index 4e07f0cd..768ef5df 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -87,6 +87,7 @@ public class MyFreeCamsModel extends AbstractModel { return ctbrec.Model.State.GROUP; case OFFLINE: case CAMOFF: + case UNKNOWN: return ctbrec.Model.State.OFFLINE; default: LOG.debug("State {} is not mapped", this.state); From 60da66139ebfac7ab4cb34cef1efd4308d6fba5c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 12 Dec 2018 18:16:49 +0100 Subject: [PATCH 187/231] Make use of new actions --- .../java/ctbrec/ui/RecordedModelsTab.java | 21 ++---------- client/src/main/java/ctbrec/ui/ThumbCell.java | 13 ++------ .../java/ctbrec/ui/action/PlayAction.java | 33 +++++++++++++++++++ .../ui/controls/SearchPopoverTreeList.java | 14 ++------ .../sites/myfreecams/MyFreeCamsTableTab.java | 16 ++------- 5 files changed, 41 insertions(+), 56 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/action/PlayAction.java diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index e03695eb..ae41af76 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -8,13 +8,10 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; -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.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; @@ -30,11 +27,10 @@ import ctbrec.recorder.Recorder; import ctbrec.sites.Site; import ctbrec.ui.action.FollowAction; import ctbrec.ui.action.PauseAction; +import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.ResumeAction; import ctbrec.ui.action.StopRecordingAction; import ctbrec.ui.controls.AutoFillTextField; -import ctbrec.ui.controls.Toast; -import javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -42,7 +38,6 @@ import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.geometry.Insets; -import javafx.scene.Cursor; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ContextMenu; @@ -72,9 +67,6 @@ import javafx.util.Duration; public class RecordedModelsTab extends Tab implements TabSelectionListener { private static final transient Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class); - static BlockingQueue queue = new LinkedBlockingQueue<>(); - static ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 10, TimeUnit.MINUTES, queue); - private ScheduledService> updateService; private Recorder recorder; private List sites; @@ -445,16 +437,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } private void openInPlayer(JavaFxModel selectedModel) { - table.setCursor(Cursor.WAIT); - new Thread(() -> { - boolean started = Player.play(selectedModel); - Platform.runLater(() -> { - if (started && Config.getInstance().getSettings().showPlayerStarting) { - Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500); - } - table.setCursor(Cursor.DEFAULT); - }); - }).start(); + new PlayAction(getTabPane(), selectedModel).execute(); } private void switchStreamSource(JavaFxModel fxModel) { diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index 914f2916..215559e5 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -21,7 +21,7 @@ import ctbrec.Config; import ctbrec.Model; import ctbrec.io.HttpException; import ctbrec.recorder.Recorder; -import ctbrec.ui.controls.Toast; +import ctbrec.ui.action.PlayAction; import javafx.animation.FadeTransition; import javafx.animation.FillTransition; import javafx.animation.ParallelTransition; @@ -341,16 +341,7 @@ public class ThumbCell extends StackPane { } void startPlayer() { - setCursor(Cursor.WAIT); - new Thread(() -> { - boolean started = Player.play(model); - Platform.runLater(() -> { - setCursor(Cursor.DEFAULT); - if (started && Config.getInstance().getSettings().showPlayerStarting) { - Toast.makeText(getScene(), "Starting Player", 2000, 500, 500); - } - }); - }).start(); + new PlayAction(this, model).execute(); } private void setRecording(boolean recording) { diff --git a/client/src/main/java/ctbrec/ui/action/PlayAction.java b/client/src/main/java/ctbrec/ui/action/PlayAction.java new file mode 100644 index 00000000..06f9cc6b --- /dev/null +++ b/client/src/main/java/ctbrec/ui/action/PlayAction.java @@ -0,0 +1,33 @@ +package ctbrec.ui.action; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.ui.Player; +import ctbrec.ui.controls.Toast; +import javafx.application.Platform; +import javafx.scene.Cursor; +import javafx.scene.Node; + +public class PlayAction { + + private Model selectedModel; + private Node source; + + public PlayAction(Node source, Model selectedModel) { + this.source = source; + this.selectedModel = selectedModel; + } + + public void execute() { + source.setCursor(Cursor.WAIT); + new Thread(() -> { + boolean started = Player.play(selectedModel); + Platform.runLater(() -> { + if (started && Config.getInstance().getSettings().showPlayerStarting) { + Toast.makeText(source.getScene(), "Starting Player", 2000, 500, 500); + } + source.setCursor(Cursor.DEFAULT); + }); + }).start(); + } +} diff --git a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java index 5b58e3a4..6cda44a6 100644 --- a/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java +++ b/client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java @@ -38,10 +38,9 @@ import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ctbrec.Config; import ctbrec.Model; import ctbrec.recorder.Recorder; -import ctbrec.ui.Player; +import ctbrec.ui.action.PlayAction; import javafx.application.Platform; import javafx.concurrent.Task; import javafx.event.EventHandler; @@ -83,16 +82,7 @@ public class SearchPopoverTreeList extends PopoverTreeList implements Pop return; } - setCursor(Cursor.WAIT); - new Thread(() -> { - Platform.runLater(() -> { - boolean started = Player.play(model); - if(started && Config.getInstance().getSettings().showPlayerStarting) { - Toast.makeText(getScene(), "Starting Player", 2000, 500, 500); - } - setCursor(Cursor.DEFAULT); - }); - }).start(); + new PlayAction(this, model).execute(); } @Override diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java index b83b8551..9a1501f0 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java @@ -22,13 +22,11 @@ import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.mfc.MyFreeCamsModel; import ctbrec.sites.mfc.SessionState; import ctbrec.ui.DesktopIntegration; -import ctbrec.ui.Player; import ctbrec.ui.TabSelectionListener; import ctbrec.ui.action.FollowAction; +import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.StartRecordingAction; import ctbrec.ui.controls.SearchBox; -import ctbrec.ui.controls.Toast; -import javafx.application.Platform; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleStringProperty; @@ -42,7 +40,6 @@ import javafx.event.ActionEvent; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.geometry.Pos; -import javafx.scene.Cursor; import javafx.scene.control.Button; import javafx.scene.control.CheckMenuItem; import javafx.scene.control.ContextMenu; @@ -291,16 +288,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { } private void openInPlayer(Model selectedModel) { - table.setCursor(Cursor.WAIT); - new Thread(() -> { - boolean started = Player.play(selectedModel); - Platform.runLater(() -> { - if (started && Config.getInstance().getSettings().showPlayerStarting) { - Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500); - } - table.setCursor(Cursor.DEFAULT); - }); - }).start(); + new PlayAction(getTabPane(), selectedModel).execute(); } private void addTableColumnIfEnabled(TableColumn tc) { From 672c133ee4fec30178102da17c4eb50ef1260254 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 12 Dec 2018 18:29:34 +0100 Subject: [PATCH 188/231] Rename "Record" to "Start Recording" ... to be consistent with other views --- .../java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java index 9a1501f0..2c5fda0f 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java @@ -262,7 +262,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { clipboard.setContent(content); }); - MenuItem startRecording = new MenuItem("Record"); + MenuItem startRecording = new MenuItem("Start Recording"); startRecording.setOnAction((e) -> startRecording(selectedModels)); MenuItem openInBrowser = new MenuItem("Open in Browser"); openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModels.get(0).getUrl())); From 5c4125c03e7f5e86093a77e045756d62a76718c9 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 12 Dec 2018 18:30:50 +0100 Subject: [PATCH 189/231] Fix: selection overlay did not show The selection overlay did not show when a tab was opened the first time --- client/src/main/java/ctbrec/ui/ThumbCell.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index 215559e5..898f3516 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -166,6 +166,8 @@ public class ThumbCell extends StackPane { selectionOverlay = new Rectangle(); selectionOverlay.setOpacity(0); + selectionOverlay.widthProperty().bind(widthProperty()); + selectionOverlay.heightProperty().bind(heightProperty()); StackPane.setAlignment(selectionOverlay, Pos.TOP_LEFT); getChildren().add(selectionOverlay); @@ -578,8 +580,6 @@ public class ThumbCell extends StackPane { int margin = 4; topic.maxWidth(w-margin*2); topic.setWrappingWidth(w-margin*2); - selectionOverlay.setWidth(w); - selectionOverlay.setHeight(getHeight()); Rectangle clip = new Rectangle(w, h); clip.setArcWidth(10); From 8039359455c19c7605ee09303fde1b3c52c0a7ed Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 12 Dec 2018 22:05:46 +0100 Subject: [PATCH 190/231] Remove playlistUrl check from isOnline isOnline contained a check for playlistUrl != null, because sometimes the playlistUrl is null even though the model is online, but it prevents the followed tab from working correctly --- common/src/main/java/ctbrec/sites/cam4/Cam4Model.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java index 4b9c4842..2acb4382 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java @@ -28,7 +28,6 @@ import com.iheartradio.m3u8.data.PlaylistData; import ctbrec.AbstractModel; import ctbrec.Config; -import ctbrec.StringUtil; import ctbrec.io.HtmlParser; import ctbrec.io.HttpException; import ctbrec.recorder.download.StreamSource; @@ -53,7 +52,8 @@ public class Cam4Model extends AbstractModel { return false; } } - return onlineState == ONLINE && StringUtil.isNotBlank(playlistUrl) && !privateRoom; + LOG.debug("{} state:{} playlistUrl:{} private:{}", getName(), onlineState, playlistUrl != null, privateRoom); + return onlineState == ONLINE && !privateRoom; } private void loadModelDetails() throws IOException, ModelDetailsEmptyException { From 168ad694ae2b5dca734b4293aa83b94499ad89f5 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 01:11:22 +0100 Subject: [PATCH 191/231] Remove debug log message --- common/src/main/java/ctbrec/sites/cam4/Cam4Model.java | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java index 2acb4382..0c40a424 100644 --- a/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java +++ b/common/src/main/java/ctbrec/sites/cam4/Cam4Model.java @@ -52,7 +52,6 @@ public class Cam4Model extends AbstractModel { return false; } } - LOG.debug("{} state:{} playlistUrl:{} private:{}", getName(), onlineState, playlistUrl != null, privateRoom); return onlineState == ONLINE && !privateRoom; } From 9f287b6d8190b29da374623e9b8eaae74832f5b2 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 02:35:03 +0100 Subject: [PATCH 192/231] Fix possible IndexOutOfBoundsException in the follow animation --- client/src/main/java/ctbrec/ui/SiteUiFactory.java | 2 +- client/src/main/java/ctbrec/ui/ThumbOverviewTab.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/SiteUiFactory.java b/client/src/main/java/ctbrec/ui/SiteUiFactory.java index 94352c1e..8ef694d1 100644 --- a/client/src/main/java/ctbrec/ui/SiteUiFactory.java +++ b/client/src/main/java/ctbrec/ui/SiteUiFactory.java @@ -20,7 +20,7 @@ public class SiteUiFactory { private static ChaturbateSiteUi ctbSiteUi; private static MyFreeCamsSiteUi mfcSiteUi; - public static SiteUI getUi(Site site) { + public static synchronized SiteUI getUi(Site site) { if (site instanceof BongaCams) { if (bongaSiteUi == null) { bongaSiteUi = new BongaCamsSiteUi((BongaCams) site); diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index d3c66c4e..aa06a2b7 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -617,7 +617,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { private double getFollowedTabYPosition(Tab followedTab) { TabPane tabPane = getTabPane(); - int idx = tabPane.getTabs().indexOf(followedTab); + int idx = Math.max(0, tabPane.getTabs().indexOf(followedTab)); for (Node node : tabPane.getChildrenUnmodifiable()) { Parent p = (Parent) node; for (Node child : p.getChildrenUnmodifiable()) { From fb54f464ab7cf99a80be95bda4c8f6399f7947b4 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 15:21:09 +0100 Subject: [PATCH 193/231] Remove irrelevant TODO comments --- client/src/main/java/ctbrec/ui/controls/Popover.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/controls/Popover.java b/client/src/main/java/ctbrec/ui/controls/Popover.java index 4b16a172..d738877b 100644 --- a/client/src/main/java/ctbrec/ui/controls/Popover.java +++ b/client/src/main/java/ctbrec/ui/controls/Popover.java @@ -98,9 +98,6 @@ public class Popover extends Region implements EventHandler{ }; public Popover() { - // TODO Could pagesPane be a region instead? I need to draw some opaque background. Right now when - // TODO animating from one page to another you can see the background "shine through" because the - // TODO group background is transparent. That can't be good for performance either. getStyleClass().setAll("popover"); frameBorder.getStyleClass().setAll("popover-frame"); frameBorder.setMouseTransparent(true); From 10c0728e059dbaf4a29fc75fa067cbb86213de89 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 15:21:31 +0100 Subject: [PATCH 194/231] Avoid NPE when is not yet loaded --- .../main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java index 38a74ae8..556c1393 100644 --- a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java +++ b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java @@ -79,7 +79,7 @@ public abstract class AbstractFileSelectionBox extends HBox { validationError.setText(msg); fileInput.setTooltip(validationError); Point2D p = fileInput.localToScreen(fileInput.getTranslateY(), fileInput.getTranslateY()); - if(!validationError.isShowing()) { + if(!validationError.isShowing() && getScene() != null) { validationError.show(getScene().getWindow(), p.getX(), p.getY() + fileInput.getHeight() + 4); } } else { From 540e8a84662782f13cf3aaec13d89888f21bc4f2 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 16:15:08 +0100 Subject: [PATCH 195/231] Fix JSON parsing for CamSoda --- .../sites/camsoda/CamsodaUpdateService.java | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java index 3e3ff047..75f827a6 100644 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java @@ -4,6 +4,8 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; import java.util.stream.Collectors; import org.json.JSONArray; @@ -56,28 +58,27 @@ public class CamsodaUpdateService extends PaginatedScheduledService { if (response.isSuccessful()) { JSONObject json = new JSONObject(response.body().string()); if(json.has("status") && json.getBoolean("status")) { + JSONArray template = json.getJSONArray("template"); JSONArray results = json.getJSONArray("results"); for (int i = 0; i < results.length(); i++) { JSONObject result = results.getJSONObject(i); if(result.has("tpl")) { JSONArray tpl = result.getJSONArray("tpl"); - String name = tpl.getString(0); - String displayName = tpl.getString(1); + String name = tpl.getString(getTemplateIndex(template, "username")); + String displayName = tpl.getString(getTemplateIndex(template, "display_name")); // int connections = tpl.getInt(2); - String streamName = tpl.getString(5); - String tsize = tpl.getString(6); - String serverPrefix = tpl.getString(7); + String streamName = tpl.getString(getTemplateIndex(template, "stream_name")); + String tsize = tpl.getString(getTemplateIndex(template, "tsize")); + String serverPrefix = tpl.getString(getTemplateIndex(template, "server_prefix")); CamsodaModel model = (CamsodaModel) camsoda.createModel(name); - model.setDescription(tpl.getString(4)); - model.setSortOrder(tpl.getFloat(3)); + model.setDescription(tpl.getString(getTemplateIndex(template, "subject_html"))); + model.setSortOrder(tpl.getFloat(getTemplateIndex(template, "sort_value"))); long unixtime = System.currentTimeMillis() / 1000; String preview = "https://thumbs-orig.camsoda.com/thumbs/" + streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime; model.setPreview(preview); - if(result.has("edge_servers")) { - JSONArray edgeServers = result.getJSONArray("edge_servers"); - model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); - } + JSONArray edgeServers = tpl.getJSONArray(getTemplateIndex(template, "edge_servers")); + model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); model.setDisplayName(displayName); models.add(model); } else { @@ -125,6 +126,16 @@ public class CamsodaUpdateService extends PaginatedScheduledService { } } } + + private int getTemplateIndex(JSONArray template, String string) { + for (int i = 0; i < template.length(); i++) { + String s = template.getString(i); + if(Objects.equals(s, string)) { + return i; + } + } + throw new NoSuchElementException(string + " not found in template: " + template.toString()); + } }; } From 066cb52106d3c6452945417219957662871a8250 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 16:16:46 +0100 Subject: [PATCH 196/231] Fix JSON parsing for BongaCams --- .../main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java index d7eebd8d..e7845956 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java @@ -58,7 +58,6 @@ public class BongaCamsUpdateService extends PaginatedScheduledService { JSONObject m = _models.getJSONObject(i); String name = m.getString("username"); BongaCamsModel model = (BongaCamsModel) bongaCams.createModel(name); - model.setUserId(m.getInt("user_id")); boolean away = m.optBoolean("is_away"); boolean online = m.optBoolean("online"); model.setOnline(online); From 723909086582c1220c896a87cfd7e80aa805ca72 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 16:23:12 +0100 Subject: [PATCH 197/231] Update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea0be75a..9235ca88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +1.15.0 +======================== +* Fix: BongaCams overview didn't work anymore +* Fix: CamSoda overview didn't work anymore +* Fix: Multi selection of thumbnails didn't work when a tab was opened the + first time +* Fix: Cam4 online detection was to restrictive +* Added tabular view for MFC, which shows all online models + 1.14.0 ======================== * Added setting for MFC to ignore the upscaled (960p) stream From 9d86a0531ce19302a64f939176dc2d8199b45a84 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 16:24:45 +0100 Subject: [PATCH 198/231] Bump version to 1.15.0 --- client/pom.xml | 2 +- common/pom.xml | 2 +- master/pom.xml | 2 +- server/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index 62be0878..9ea0e01e 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.14.0 + 1.15.0 ../master diff --git a/common/pom.xml b/common/pom.xml index b92016d3..4f20e4fa 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.14.0 + 1.15.0 ../master diff --git a/master/pom.xml b/master/pom.xml index 7cb07d20..78ea5606 100644 --- a/master/pom.xml +++ b/master/pom.xml @@ -6,7 +6,7 @@ ctbrec master pom - 1.14.0 + 1.15.0 ../common diff --git a/server/pom.xml b/server/pom.xml index 7cd1f4f6..24075158 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.14.0 + 1.15.0 ../master From dc8a4d419043c5cb26f3c1dafa871a1b6f900fe3 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 17:09:40 +0100 Subject: [PATCH 199/231] Don't add model to models or update it, if uid is not set --- common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index 9f5c26d6..50ac1bbc 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -420,6 +420,11 @@ public class MyFreeCamsClient { return; } + // uid not set, we can't identify this model + if(state.getUid() == null || state.getUid() <= 0) { + return; + } + MyFreeCamsModel model = models.getIfPresent(state.getUid()); if(model == null) { model = mfc.createModel(state.getNm()); From 560e73c1dd85c20413097be40316a6abcfbab8ac Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 20:51:14 +0100 Subject: [PATCH 200/231] Reduce log level for unused message types --- common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index 50ac1bbc..cc4a3e06 100644 --- a/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -277,7 +277,7 @@ public class MyFreeCamsClient { case ROOMDATA: LOG.debug("ROOMDATA: {}", message); case UEOPT: - LOG.debug("UEOPT: {}", message); + LOG.trace("UEOPT: {}", message); break; case SLAVEVSHARE: // LOG.debug("SLAVEVSHARE {}", message); @@ -295,7 +295,7 @@ public class MyFreeCamsClient { } break; default: - LOG.debug("Unknown message {}", message); + LOG.trace("Unknown message {}", message); break; } } From ceb7c07aa8b115b395a4abaf91dd2cc34d65b7a4 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 20:54:10 +0100 Subject: [PATCH 201/231] Add setting for minimum recording length If a recording is shorter than x seconds, it gets deleted --- .../java/ctbrec/ui/settings/SettingsTab.java | 21 ++++++ common/src/main/java/ctbrec/MpegUtil.java | 73 +++++++++++++++++++ common/src/main/java/ctbrec/Settings.java | 1 + .../java/ctbrec/recorder/LocalRecorder.java | 73 +++++++++++++++++++ .../ctbrec/recorder/PlaylistGenerator.java | 55 +------------- 5 files changed, 171 insertions(+), 52 deletions(-) create mode 100644 common/src/main/java/ctbrec/MpegUtil.java diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index d9630ff9..c06733b2 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -58,6 +58,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private TextField port; private TextField onlineCheckIntervalInSecs; private TextField leaveSpaceOnDevice; + private TextField minimumLengthInSecs; private CheckBox loadResolution; private CheckBox secureCommunication = new CheckBox(); private CheckBox chooseStreamQuality = new CheckBox(); @@ -360,6 +361,26 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(leaveSpaceOnDevice, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(leaveSpaceOnDevice, 1, row++); + tt = new Tooltip("Delete recordings, which are shorter than x seconds. 0 to disable."); + l = new Label("Delete recordings shorter than (secs)"); + l.setTooltip(tt); + layout.add(l, 0, row); + int minimumLengthInSeconds = Config.getInstance().getSettings().minimumLengthInSeconds; + minimumLengthInSecs = new TextField(Integer.toString(minimumLengthInSeconds)); + minimumLengthInSecs.setTooltip(tt); + minimumLengthInSecs.textProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue.matches("\\d*")) { + minimumLengthInSecs.setText(newValue.replaceAll("[^\\d]", "")); + } + if(!minimumLengthInSecs.getText().isEmpty()) { + int minimumLength = Integer.parseInt(minimumLengthInSecs.getText()); + Config.getInstance().getSettings().minimumLengthInSeconds = minimumLength; + saveConfig(); + } + }); + GridPane.setMargin(minimumLengthInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + layout.add(minimumLengthInSecs, 1, row++); + TitledPane locations = new TitledPane("Recorder", layout); locations.setCollapsible(false); return locations; diff --git a/common/src/main/java/ctbrec/MpegUtil.java b/common/src/main/java/ctbrec/MpegUtil.java new file mode 100644 index 00000000..4ef37a53 --- /dev/null +++ b/common/src/main/java/ctbrec/MpegUtil.java @@ -0,0 +1,73 @@ +package ctbrec; + +import java.io.File; +import java.io.IOException; +import java.nio.channels.ReadableByteChannel; +import java.time.Duration; +import java.util.Set; + +import org.jcodec.common.Demuxer; +import org.jcodec.common.DemuxerTrack; +import org.jcodec.common.TrackType; +import org.jcodec.common.Tuple; +import org.jcodec.common.Tuple._2; +import org.jcodec.common.io.FileChannelWrapper; +import org.jcodec.common.io.NIOUtils; +import org.jcodec.common.model.Packet; +import org.jcodec.containers.mps.MPSDemuxer; +import org.jcodec.containers.mps.MTSDemuxer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MpegUtil { + private static final transient Logger LOG = LoggerFactory.getLogger(MpegUtil.class); + + public static void main(String[] args) throws IOException { + readFile(new File("../../test-recs/ff.ts")); + } + + public static void readFile(File file) throws IOException { + System.out.println(file.getCanonicalPath()); + double duration = MpegUtil.getFileDuration(file); + System.out.println(Duration.ofSeconds((long) duration)); + } + + public static double getFileDuration(File file) throws IOException { + try(FileChannelWrapper ch = NIOUtils.readableChannel(file)) { + _2 m2tsDemuxer = createM2TSDemuxer(ch, TrackType.VIDEO); + Demuxer demuxer = m2tsDemuxer.v1; + DemuxerTrack videoDemux = demuxer.getTracks().get(0); + Packet videoFrame = null; + double totalDuration = 0; + while( (videoFrame = videoDemux.nextFrame()) != null) { + totalDuration += videoFrame.getDurationD(); + } + return totalDuration; + } + } + + public static _2 createM2TSDemuxer(FileChannelWrapper ch, TrackType targetTrack) throws IOException { + MTSDemuxer mts = new MTSDemuxer(ch); + Set programs = mts.getPrograms(); + if (programs.size() == 0) { + LOG.error("The MPEG TS stream contains no programs"); + return null; + } + Tuple._2 found = null; + for (Integer pid : programs) { + ReadableByteChannel program = mts.getProgram(pid); + if (found != null) { + program.close(); + continue; + } + MPSDemuxer demuxer = new MPSDemuxer(program); + if (targetTrack == TrackType.AUDIO && demuxer.getAudioTracks().size() > 0 + || targetTrack == TrackType.VIDEO && demuxer.getVideoTracks().size() > 0) { + found = org.jcodec.common.Tuple._2(pid, (Demuxer) demuxer); + } else { + program.close(); + } + } + return found; + } +} diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 384ea432..043b55af 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -41,6 +41,7 @@ public class Settings { public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec"; public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT; public long minimumSpaceLeftInBytes = 0; + public int minimumLengthInSeconds = 0; public String mediaPlayer = "/usr/bin/mpv"; public String postProcessing = ""; public String username = ""; // chaturbate username TODO maybe rename this onetime diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 0732220b..6e16c809 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -4,6 +4,8 @@ import static ctbrec.Recording.State.*; import static ctbrec.event.Event.Type.*; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FilenameFilter; import java.io.IOException; import java.nio.file.FileStore; @@ -11,6 +13,7 @@ import java.nio.file.Files; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; +import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; @@ -34,11 +37,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.eventbus.Subscribe; +import com.iheartradio.m3u8.Encoding; +import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.ParsingMode; import com.iheartradio.m3u8.PlaylistException; +import com.iheartradio.m3u8.PlaylistParser; +import com.iheartradio.m3u8.data.MediaPlaylist; +import com.iheartradio.m3u8.data.Playlist; +import com.iheartradio.m3u8.data.TrackData; import ctbrec.Config; import ctbrec.Model; +import ctbrec.MpegUtil; import ctbrec.OS; import ctbrec.Recording; import ctbrec.Recording.State; @@ -740,9 +751,71 @@ public class LocalRecorder implements Recorder { fireRecordingStateChanged(download.getTarget(), GENERATING_PLAYLIST, download.getModel(), download.getStartTime()); generatePlaylist(download.getTarget()); } + boolean deleted = deleteIfTooShort(download); + if(deleted) { + // recording was too short. stop here and don't do post-processing + return; + } fireRecordingStateChanged(download.getTarget(), POST_PROCESSING, download.getModel(), download.getStartTime()); postprocess(download); fireRecordingStateChanged(download.getTarget(), FINISHED, download.getModel(), download.getStartTime()); }; } + + + // TODO maybe get file size and bitrate and check, if the values are plausible + // we could also compare the length with the time elapsed since starting the recording + private boolean deleteIfTooShort(Download download) { + long minimumLengthInSeconds = Config.getInstance().getSettings().minimumLengthInSeconds; + if(minimumLengthInSeconds <= 0) { + return false; + } + + try { + LOG.debug("Determining video length for {}", download.getTarget()); + File target = download.getTarget(); + double duration = 0; + if(target.isDirectory()) { + File playlist = new File(target, "playlist.m3u8"); + duration = getPlaylistLength(playlist); + } else { + duration = MpegUtil.getFileDuration(target); + } + Duration minLength = Duration.ofSeconds(minimumLengthInSeconds); + Duration videoLength = Duration.ofSeconds((long) duration); + LOG.debug("Recording started at:{}. Video length is {}", download.getStartTime(), videoLength); + if(videoLength.minus(minLength).isNegative()) { + LOG.debug("Video too short {} {}", videoLength, download.getTarget()); + LOG.debug("Deleting {}", target); + if(target.isDirectory()) { + deleteDirectory(target); + deleteEmptyParents(target); + } else { + Files.delete(target.toPath()); + deleteEmptyParents(target.getParentFile()); + } + return true; + } else { + return false; + } + } catch (Exception e) { + LOG.error("Couldn't check video length", e); + return false; + } + } + + private double getPlaylistLength(File playlist) throws IOException, ParseException, PlaylistException { + if(playlist.exists()) { + PlaylistParser playlistParser = new PlaylistParser(new FileInputStream(playlist), Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); + Playlist m3u = playlistParser.parse(); + MediaPlaylist mediaPlaylist = m3u.getMediaPlaylist(); + double length = 0; + for (TrackData trackData : mediaPlaylist.getTracks()) { + length += trackData.getTrackInfo().duration; + } + return length; + } else { + throw new FileNotFoundException(playlist.getAbsolutePath() + " does not exist"); + } + } } diff --git a/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java b/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java index 2fec113c..a4180765 100644 --- a/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java +++ b/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java @@ -6,24 +6,12 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; -import java.nio.channels.ReadableByteChannel; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.jcodec.common.Demuxer; -import org.jcodec.common.DemuxerTrack; -import org.jcodec.common.TrackType; -import org.jcodec.common.Tuple; -import org.jcodec.common.Tuple._2; -import org.jcodec.common.io.FileChannelWrapper; -import org.jcodec.common.io.NIOUtils; -import org.jcodec.common.model.Packet; -import org.jcodec.containers.mps.MPSDemuxer; -import org.jcodec.containers.mps.MTSDemuxer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +28,8 @@ import com.iheartradio.m3u8.data.PlaylistType; import com.iheartradio.m3u8.data.TrackData; import com.iheartradio.m3u8.data.TrackInfo; +import ctbrec.MpegUtil; + public class PlaylistGenerator { private static final transient Logger LOG = LoggerFactory.getLogger(PlaylistGenerator.class); @@ -72,7 +62,7 @@ public class PlaylistGenerator { try { track.add(new TrackData.Builder() .withUri(file.getName()) - .withTrackInfo(new TrackInfo((float) getFileDuration(file), file.getName())) + .withTrackInfo(new TrackInfo((float) MpegUtil.getFileDuration(file), file.getName())) .build()); } catch(Exception e) { LOG.warn("Couldn't determine duration for {}. Skipping this file.", file.getName()); @@ -141,45 +131,6 @@ public class PlaylistGenerator { return targetDuration; } - private double getFileDuration(File file) throws IOException { - try(FileChannelWrapper ch = NIOUtils.readableChannel(file)) { - _2 m2tsDemuxer = createM2TSDemuxer(ch, TrackType.VIDEO); - Demuxer demuxer = m2tsDemuxer.v1; - DemuxerTrack videoDemux = demuxer.getTracks().get(0); - Packet videoFrame = null; - double totalDuration = 0; - while( (videoFrame = videoDemux.nextFrame()) != null) { - totalDuration += videoFrame.getDurationD(); - } - return totalDuration; - } - } - - public static _2 createM2TSDemuxer(FileChannelWrapper ch, TrackType targetTrack) throws IOException { - MTSDemuxer mts = new MTSDemuxer(ch); - Set programs = mts.getPrograms(); - if (programs.size() == 0) { - LOG.error("The MPEG TS stream contains no programs"); - return null; - } - Tuple._2 found = null; - for (Integer pid : programs) { - ReadableByteChannel program = mts.getProgram(pid); - if (found != null) { - program.close(); - continue; - } - MPSDemuxer demuxer = new MPSDemuxer(program); - if (targetTrack == TrackType.AUDIO && demuxer.getAudioTracks().size() > 0 - || targetTrack == TrackType.VIDEO && demuxer.getVideoTracks().size() > 0) { - found = org.jcodec.common.Tuple._2(pid, (Demuxer) demuxer); - } else { - program.close(); - } - } - return found; - } - public void addProgressListener(ProgressListener l) { listeners.add(l); } From 150af23d1475df9ebb1b0bac4e253d22496bdf11 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 20:54:26 +0100 Subject: [PATCH 202/231] Fix log messages --- common/src/main/java/ctbrec/event/ExecuteProgram.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/ctbrec/event/ExecuteProgram.java b/common/src/main/java/ctbrec/event/ExecuteProgram.java index 28cb4807..5bb2b321 100644 --- a/common/src/main/java/ctbrec/event/ExecuteProgram.java +++ b/common/src/main/java/ctbrec/event/ExecuteProgram.java @@ -48,9 +48,9 @@ public class ExecuteProgram extends Action { err.start(); process.waitFor(); - LOG.debug("executing {} finished", executable); + LOG.debug("Executing {} finished", executable); } catch (Exception e) { - LOG.error("Error while processing {}", e); + LOG.error("Error while executing {}", executable, e); } } From 52cdf8d60127fd47c05b3afe3412cd20e04287e1 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 13 Dec 2018 23:48:16 +0100 Subject: [PATCH 203/231] Add classes and first code for Streamate --- .../java/ctbrec/ui/CamrecApplication.java | 2 + .../main/java/ctbrec/ui/SiteUiFactory.java | 8 + .../ui/sites/streamate/StreamateSiteUi.java | 33 +++ .../sites/streamate/StreamateTabProvider.java | 62 +++++ .../streamate/StreamateUpdateService.java | 95 +++++++ .../ctbrec/ui/sites/streamate/girls.sml | 18 ++ .../main/java/ctbrec/io/XmlParserUtils.java | 117 ++++++++ .../ctbrec/sites/streamate/Streamate.java | 193 ++++++++++++++ .../sites/streamate/StreamateHttpClient.java | 75 ++++++ .../sites/streamate/StreamateModel.java | 251 ++++++++++++++++++ 10 files changed, 854 insertions(+) create mode 100644 client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java create mode 100644 client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java create mode 100644 client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java create mode 100644 client/src/main/resources/ctbrec/ui/sites/streamate/girls.sml create mode 100644 common/src/main/java/ctbrec/io/XmlParserUtils.java create mode 100644 common/src/main/java/ctbrec/sites/streamate/Streamate.java create mode 100644 common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java create mode 100644 common/src/main/java/ctbrec/sites/streamate/StreamateModel.java diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index d698dbdb..d91a9d5e 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -38,6 +38,7 @@ import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.mfc.MyFreeCams; +import ctbrec.sites.streamate.Streamate; import ctbrec.ui.settings.SettingsTab; import javafx.application.Application; import javafx.application.HostServices; @@ -76,6 +77,7 @@ public class CamrecApplication extends Application { sites.add(new Camsoda()); sites.add(new Chaturbate()); sites.add(new MyFreeCams()); + sites.add(new Streamate()); loadConfig(); registerAlertSystem(); createHttpClient(); diff --git a/client/src/main/java/ctbrec/ui/SiteUiFactory.java b/client/src/main/java/ctbrec/ui/SiteUiFactory.java index 8ef694d1..7475868c 100644 --- a/client/src/main/java/ctbrec/ui/SiteUiFactory.java +++ b/client/src/main/java/ctbrec/ui/SiteUiFactory.java @@ -6,11 +6,13 @@ import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.mfc.MyFreeCams; +import ctbrec.sites.streamate.Streamate; import ctbrec.ui.sites.bonga.BongaCamsSiteUi; import ctbrec.ui.sites.cam4.Cam4SiteUi; import ctbrec.ui.sites.camsoda.CamsodaSiteUi; import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi; import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi; +import ctbrec.ui.sites.streamate.StreamateSiteUi; public class SiteUiFactory { @@ -19,6 +21,7 @@ public class SiteUiFactory { private static CamsodaSiteUi camsodaSiteUi; private static ChaturbateSiteUi ctbSiteUi; private static MyFreeCamsSiteUi mfcSiteUi; + private static StreamateSiteUi streamateSiteUi; public static synchronized SiteUI getUi(Site site) { if (site instanceof BongaCams) { @@ -46,6 +49,11 @@ public class SiteUiFactory { mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site); } return mfcSiteUi; + } else if (site instanceof Streamate) { + if (streamateSiteUi == null) { + streamateSiteUi = new StreamateSiteUi((Streamate) site); + } + return streamateSiteUi; } throw new RuntimeException("Unknown site " + site.getName()); } diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java new file mode 100644 index 00000000..8d31d020 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java @@ -0,0 +1,33 @@ +package ctbrec.ui.sites.streamate; + +import java.io.IOException; + +import ctbrec.sites.ConfigUI; +import ctbrec.sites.streamate.Streamate; +import ctbrec.ui.SiteUI; +import ctbrec.ui.TabProvider; + +public class StreamateSiteUi implements SiteUI { + + private StreamateTabProvider tabProvider; + + public StreamateSiteUi(Streamate streamate) { + tabProvider = new StreamateTabProvider(streamate); + } + + @Override + public TabProvider getTabProvider() { + return tabProvider; + } + + @Override + public ConfigUI getConfigUI() { + return null; + } + + @Override + public boolean login() throws IOException { + return false; + } + +} diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java new file mode 100644 index 00000000..5c74b825 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java @@ -0,0 +1,62 @@ +package ctbrec.ui.sites.streamate; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.recorder.Recorder; +import ctbrec.sites.streamate.Streamate; +import ctbrec.ui.TabProvider; +import ctbrec.ui.ThumbOverviewTab; +import javafx.scene.Scene; +import javafx.scene.control.Tab; + +public class StreamateTabProvider extends TabProvider { + private static final transient Logger LOG = LoggerFactory.getLogger(StreamateTabProvider.class); + private Streamate streamate; + private Recorder recorder; + + public StreamateTabProvider(Streamate streamate) { + this.streamate = streamate; + this.recorder = streamate.getRecorder(); + } + + @Override + public List getTabs(Scene scene) { + List tabs = new ArrayList<>(); + try { + tabs.add(createTab("Girls", "/ctbrec/ui/sites/streamate/girls.sml")); + } catch (IOException e) { + LOG.error("Couldn't create streamate tab", e); + } + return tabs; + } + + @Override + public Tab getFollowedTab() { + return null; + } + + private Tab createTab(String title, String queryFile) throws IOException { + StreamateUpdateService updateService = new StreamateUpdateService(loadQuery(queryFile), streamate); + ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, streamate); + tab.setRecorder(recorder); + return tab; + } + + private String loadQuery(String file) throws IOException { + InputStream is = getClass().getResourceAsStream(file); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] b = new byte[1024]; + int len = -1; + while( (len = is.read(b)) >= 0) { + bos.write(b, 0, len); + } + return new String(bos.toByteArray(), "utf-8"); + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java new file mode 100644 index 00000000..1c594e93 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java @@ -0,0 +1,95 @@ +package ctbrec.ui.sites.streamate; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPathExpressionException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.XmlParserUtils; +import ctbrec.sites.streamate.Streamate; +import ctbrec.sites.streamate.StreamateModel; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class StreamateUpdateService extends PaginatedScheduledService { + + private static final transient Logger LOG = LoggerFactory.getLogger(StreamateUpdateService.class); + + private static final String URL = "http://affiliate.streamate.com/SMLive/SMLResult.xml"; + private Streamate streamate; + private String query; + + public StreamateUpdateService(String query, Streamate streamate) { + this.query = query; + this.streamate = streamate; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException, SAXException, ParserConfigurationException, XPathExpressionException { + LOG.debug("Fetching page {}", URL); + String q = query + .replace("{maxresults}", "50") + .replace("{pagenum}", Integer.toString(page)); + //LOG.debug("Query:\n{}", q); + RequestBody body = RequestBody.create(MediaType.parse("text/xml"), q); + Request request = new Request.Builder() + .url(URL) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "text/xml, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", streamate.getBaseUrl()) + .post(body) + .build(); + Response response = streamate.getHttpClient().execute(request); + if (response.isSuccessful()) { + List models = new ArrayList<>(); + String content = response.body().string(); + LOG.debug(content); + ByteArrayInputStream in = new ByteArrayInputStream(content.getBytes("utf-8")); + Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in); + NodeList performers = doc.getElementsByTagName("Performer"); + for (int i = 0; i < performers.getLength(); i++) { + Node performer = performers.item(i); + String name = performer.getAttributes().getNamedItem("Name").getNodeValue(); + String id = performer.getAttributes().getNamedItem("Id").getNodeValue(); + String GoldShow = performer.getAttributes().getNamedItem("GoldShow").getNodeValue(); + String PreGoldShow = performer.getAttributes().getNamedItem("PreGoldShow").getNodeValue(); + String PartyChat = performer.getAttributes().getNamedItem("PartyChat").getNodeValue(); + StreamateModel model = (StreamateModel) streamate.createModel(name); + model.setId(id); + models.add(model); + Node pic = XmlParserUtils.getNodeWithXpath(performer, "Media/Pic/Full"); + String previewUrl = "https:" + pic.getAttributes().getNamedItem("Src").getNodeValue(); + model.setPreview(previewUrl); + LOG.debug("Name {} - {}{}{}", name, PartyChat, PreGoldShow, GoldShow); + } + return models; + } else { + int code = response.code(); + response.close(); + throw new IOException("HTTP status " + code); + } + } + }; + } +} diff --git a/client/src/main/resources/ctbrec/ui/sites/streamate/girls.sml b/client/src/main/resources/ctbrec/ui/sites/streamate/girls.sml new file mode 100644 index 00000000..f7841ae4 --- /dev/null +++ b/client/src/main/resources/ctbrec/ui/sites/streamate/girls.sml @@ -0,0 +1,18 @@ + + + + + + + + biopic, staticbiopic + + + + live,recorded + + + + \ No newline at end of file diff --git a/common/src/main/java/ctbrec/io/XmlParserUtils.java b/common/src/main/java/ctbrec/io/XmlParserUtils.java new file mode 100644 index 00000000..a1ac9cf6 --- /dev/null +++ b/common/src/main/java/ctbrec/io/XmlParserUtils.java @@ -0,0 +1,117 @@ +package ctbrec.io; + +import java.io.IOException; +import java.io.StringReader; +import java.util.List; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public class XmlParserUtils { + + public static Document parse(String xml) throws ParserConfigurationException, SAXException, IOException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(new InputSource(new StringReader(xml))); + } + + public static Node getFirstElementByTagName(Document doc, String tagName) { + NodeList list = doc.getElementsByTagName(tagName); + if (list.getLength() > 0) { + return list.item(0); + } else { + return null; + } + } + + public static String getTextContent(Document doc, String tagName) { + Node node = getFirstElementByTagName(doc, tagName); + if (node != null) { + return node.getTextContent(); + } else { + return null; + } + } + + public static String getTextContent(Node parent, String tagName) { + Node node = findChildWithTagName(parent, tagName); + if (node != null) { + return node.getTextContent(); + } else { + return null; + } + } + + public static Node findChildWithTagName(Node parent, String tagName) { + if (parent == null) { + return null; + } + + NodeList childs = parent.getChildNodes(); + for (int i = 0; i < childs.getLength(); i++) { + Node child = childs.item(i); + if (child.getNodeName().equals(tagName)) { + return child; + } else if (child.hasChildNodes()) { + Node result = findChildWithTagName(child, tagName); + if (result != null) { + return result; + } + } + } + + return null; + } + + public static void getElementsByTagName(Node parent, String tagName, List result) { + if (parent == null) { + return; + } + + NodeList childs = parent.getChildNodes(); + for (int i = 0; i < childs.getLength(); i++) { + Node child = childs.item(i); + if (child.getNodeName().equals(tagName)) { + result.add(child); + } else if (child.hasChildNodes()) { + getElementsByTagName(child, tagName, result); + } + } + } + + public static String getStringWithXpath(String xml, String xpath) throws XPathExpressionException { + XPath xp = XPathFactory.newInstance().newXPath(); + return xp.evaluate(xpath, new InputSource(new StringReader(xml))); + } + + public static String getStringWithXpath(Node node, String xpath) throws XPathExpressionException { + XPath xp = XPathFactory.newInstance().newXPath(); + return xp.evaluate(xpath, node); + } + + public static Node getNodeWithXpath(String xml, String xpath) throws XPathExpressionException { + XPath xp = XPathFactory.newInstance().newXPath(); + return (Node) xp.evaluate(xpath, new InputSource(new StringReader(xml)), XPathConstants.NODE); + } + + public static Node getNodeWithXpath(Node node, String xpath) throws XPathExpressionException { + XPath xp = XPathFactory.newInstance().newXPath(); + return (Node) xp.evaluate(xpath, node, XPathConstants.NODE); + } + + public static NodeList getNodeListWithXpath(String xml, String xpath) throws XPathExpressionException { + XPath xp = XPathFactory.newInstance().newXPath(); + return (NodeList) xp.evaluate(xpath, new InputSource(new StringReader(xml)), XPathConstants.NODESET); + } +} diff --git a/common/src/main/java/ctbrec/sites/streamate/Streamate.java b/common/src/main/java/ctbrec/sites/streamate/Streamate.java new file mode 100644 index 00000000..e0da236b --- /dev/null +++ b/common/src/main/java/ctbrec/sites/streamate/Streamate.java @@ -0,0 +1,193 @@ +package ctbrec.sites.streamate; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Model; +import ctbrec.io.HttpClient; +import ctbrec.sites.AbstractSite; + +public class Streamate extends AbstractSite { + + private static final transient Logger LOG = LoggerFactory.getLogger(Streamate.class); + + public static final String BASE_URL = "https://www.streamate.com"; + + private StreamateHttpClient httpClient; + + @Override + public String getName() { + return "Streamate"; + } + + @Override + public String getBaseUrl() { + return BASE_URL; + } + + @Override + public String getAffiliateLink() { + return BASE_URL + "/landing/click/?AFNO=2-11330.2"; + } + + @Override + public Model createModel(String name) { + StreamateModel model = new StreamateModel(); + model.setName(name); + model.setUrl(BASE_URL + "/cam/" + name); + model.setDescription(""); + model.setSite(this); + return model; + } + + @Override + public Integer getTokenBalance() throws IOException { + // int userId = ((StreamateHttpClient)getHttpClient()).getUserId(); + // String url = Streamate.BASE_URL + "/tools/amf.php"; + // RequestBody body = new FormBody.Builder() + // .add("method", "ping") + // .add("args[]", Integer.toString(userId)) + // .build(); + // 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", Streamate.BASE_URL) + // .addHeader("X-Requested-With", "XMLHttpRequest") + // .post(body) + // .build(); + // try(Response response = getHttpClient().execute(request)) { + // if(response.isSuccessful()) { + // JSONObject json = new JSONObject(response.body().string()); + // if(json.optString("status").equals("online")) { + // JSONObject userData = json.getJSONObject("userData"); + // return userData.getInt("balance"); + // } else { + // throw new IOException("Request was not successful: " + json.toString(2)); + // } + // } else { + // throw new HttpException(response.code(), response.message()); + // } + // } + return 0; + } + + @Override + public String getBuyTokensLink() { + return getAffiliateLink(); + } + + @Override + public synchronized boolean login() throws IOException { + return credentialsAvailable() && getHttpClient().login(); + } + + @Override + public HttpClient getHttpClient() { + if(httpClient == null) { + httpClient = new StreamateHttpClient(); + } + return httpClient; + } + + @Override + public void init() throws IOException { + } + + @Override + public void shutdown() { + if(httpClient != null) { + httpClient.shutdown(); + } + } + + @Override + public boolean supportsTips() { + return false; + } + + @Override + public boolean supportsFollow() { + return false; + } + + @Override + public boolean supportsSearch() { + return false; + } + + @Override + public boolean searchRequiresLogin() { + return true; + } + + @Override + public List search(String q) throws IOException, InterruptedException { + // String url = BASE_URL + "/tools/listing_v3.php?offset=0&model_search[display_name][text]=" + URLEncoder.encode(q, "utf-8"); + // Request req = new Request.Builder() + // .url(url) + // .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + // .addHeader("Accept", "application/json, text/javascript, */*") + // .addHeader("Accept-Language", "en") + // .addHeader("Referer", Streamate.BASE_URL) + // .addHeader("X-Requested-With", "XMLHttpRequest") + // .build(); + // try(Response response = getHttpClient().execute(req)) { + // if(response.isSuccessful()) { + // String body = response.body().string(); + // JSONObject json = new JSONObject(body); + // if(json.optString("status").equals("success")) { + // List models = new ArrayList<>(); + // JSONArray results = json.getJSONArray("models"); + // for (int i = 0; i < results.length(); i++) { + // JSONObject result = results.getJSONObject(i); + // Model model = createModel(result.getString("username")); + // String thumb = result.getString("thumb_image"); + // if(thumb != null) { + // model.setPreview("https:" + thumb); + // } + // if(result.has("display_name")) { + // model.setDisplayName(result.getString("display_name")); + // } + // models.add(model); + // } + // return models; + // } else { + // LOG.warn("Search result: " + json.toString(2)); + // return Collections.emptyList(); + // } + // } else { + // throw new HttpException(response.code(), response.message()); + // } + // } + return Collections.emptyList(); + } + + @Override + public boolean isSiteForModel(Model m) { + return m instanceof StreamateModel; + } + + @Override + public boolean credentialsAvailable() { + return false; + } + + @Override + public Model createModelFromUrl(String url) { + Matcher m = Pattern.compile("https?://.*?streamate.com/cam/([^/]*?)/?").matcher(url); + if(m.matches()) { + String modelName = m.group(1); + return createModel(modelName); + } else { + return super.createModelFromUrl(url); + } + } +} diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java b/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java new file mode 100644 index 00000000..6772eadd --- /dev/null +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java @@ -0,0 +1,75 @@ +package ctbrec.sites.streamate; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.io.HttpClient; + +public class StreamateHttpClient extends HttpClient { + + private static final transient Logger LOG = LoggerFactory.getLogger(StreamateHttpClient.class); + + public StreamateHttpClient() { + super("streamate"); + } + + @Override + public synchronized boolean login() throws IOException { + if(loggedIn) { + return true; + } + + boolean cookiesWorked = checkLoginSuccess(); + if(cookiesWorked) { + loggedIn = true; + LOG.debug("Logged in with cookies"); + return true; + } + + return false; + } + + /** + * Check, if the login worked + * @throws IOException + */ + public boolean checkLoginSuccess() throws IOException { + return false; + // String modelName = getAnyModelName(); + // // we request the roomData of a random model, because it contains + // // user data, if the user is logged in, which we can use to verify, that the login worked + // String url = Streamate.BASE_URL + "/tools/amf.php"; + // RequestBody body = new FormBody.Builder() + // .add("method", "getRoomData") + // .add("args[]", modelName) + // .add("args[]", "false") + // //.add("method", "ping") // TODO alternative request, but + // //.add("args[]", ) // where to get the userId + // .build(); + // 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", Streamate.BASE_URL) + // .addHeader("X-Requested-With", "XMLHttpRequest") + // .post(body) + // .build(); + // try(Response response = execute(request)) { + // if(response.isSuccessful()) { + // JSONObject json = new JSONObject(response.body().string()); + // if(json.optString("status").equals("success")) { + // JSONObject userData = json.getJSONObject("userData"); + // userId = userData.optInt("userId"); + // return userId > 0; + // } else { + // throw new IOException("Request was not successful: " + json.toString(2)); + // } + // } else { + // throw new HttpException(response.code(), response.message()); + // } + // } + } +} diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java new file mode 100644 index 00000000..20c80be9 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java @@ -0,0 +1,251 @@ +package ctbrec.sites.streamate; + +import static ctbrec.Model.State.*; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.iheartradio.m3u8.Encoding; +import com.iheartradio.m3u8.Format; +import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.ParsingMode; +import com.iheartradio.m3u8.PlaylistException; +import com.iheartradio.m3u8.PlaylistParser; +import com.iheartradio.m3u8.data.MasterPlaylist; +import com.iheartradio.m3u8.data.Playlist; +import com.iheartradio.m3u8.data.PlaylistData; +import com.iheartradio.m3u8.data.StreamInfo; + +import ctbrec.AbstractModel; +import ctbrec.Config; +import ctbrec.io.HttpException; +import ctbrec.recorder.download.StreamSource; +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class StreamateModel extends AbstractModel { + + private static final transient Logger LOG = LoggerFactory.getLogger(StreamateModel.class); + + private boolean online = false; + private List streamSources = new ArrayList<>(); + private int[] resolution; + private String id; + + @Override + public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { + if(ignoreCache) { + String url = getStreamUrl(); + Request req = new Request.Builder().url(url).build(); + try(Response resp = site.getHttpClient().execute(req)) { + online = resp.isSuccessful(); + } + } + return online; + } + + private JSONObject getRoomData() throws IOException { + String url = Streamate.BASE_URL + "/tools/amf.php"; + RequestBody body = new FormBody.Builder() + .add("method", "getRoomData") + .add("args[]", getName()) + .add("args[]", "false") + .build(); + 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", Streamate.BASE_URL) + .addHeader("X-Requested-With", "XMLHttpRequest") + .post(body) + .build(); + try(Response response = site.getHttpClient().execute(request)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + return json; + } else { + throw new IOException(response.code() + " " + response.message()); + } + } + } + + public void setOnline(boolean online) { + this.online = online; + } + + @Override + public State getOnlineState(boolean failFast) throws IOException, ExecutionException { + if(failFast) { + return onlineState; + } else { + if(onlineState == UNKNOWN) { + return online ? ONLINE : OFFLINE; + } + return onlineState; + } + } + + @Override + public void setOnlineState(State onlineState) { + this.onlineState = onlineState; + } + + @Override + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + String streamUrl = getStreamUrl(); + if (streamUrl == null) { + return Collections.emptyList(); + } + Request req = new Request.Builder().url(streamUrl).build(); + try(Response response = site.getHttpClient().execute(req)) { + if(response.isSuccessful()) { + InputStream inputStream = response.body().byteStream(); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); + Playlist playlist = parser.parse(); + MasterPlaylist master = playlist.getMasterPlaylist(); + streamSources.clear(); + for (PlaylistData playlistData : master.getPlaylists()) { + StreamSource streamsource = new StreamSource(); + streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", playlistData.getUri()); + if (playlistData.hasStreamInfo()) { + StreamInfo info = playlistData.getStreamInfo(); + streamsource.bandwidth = info.getBandwidth(); + streamsource.width = info.hasResolution() ? info.getResolution().width : 0; + streamsource.height = info.hasResolution() ? info.getResolution().height : 0; + } else { + streamsource.bandwidth = 0; + streamsource.width = 0; + streamsource.height = 0; + } + streamSources.add(streamsource); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + return streamSources; + } + + private String getStreamUrl() throws IOException { + String url = "https://hybridclient.naiadsystems.com/api/v1/config/?sabasic=&sakey=&sk=www.streamate.com&userid=0&version=6.3.16&ajax=1&name=" + getName(); + Request req = new Request.Builder() + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", Streamate.BASE_URL + '/' + getName()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .url(url) + .build(); + try(Response response = site.getHttpClient().execute(req)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + JSONObject performer = json.getJSONObject("performer"); + id = performer.getString("id"); + JSONObject stream = json.getJSONObject("stream"); + String sserver = stream.getString("serverId"); + String streamId = stream.getString("streamId"); + String wsHost = stream.getString("nodeHost"); + LOG.debug(json.toString(2)); + + String wsUrl = wsHost + "/socket.io/?" + + "performerid=" + id + + "&sserver=" + sserver + + "&streamid=" + streamId + + "&sakey=&sessiontype=preview&perfdiscountid=0&minduration=0&goldshowid=0&version=7&referrer=hybrid.client.6.3.16/avchat.swf&usertype=false&lang=en&EIO=3&transport=websocket"; + } else { + throw new IOException(response.code() + ' ' + response.message()); + } + } + return ""; + } + + @Override + public void invalidateCacheEntries() { + resolution = null; + } + + @Override + public void receiveTip(int tokens) throws IOException { + // String url = Streamate.BASE_URL + "/chat-ajax-amf-service?" + System.currentTimeMillis(); + // int userId = ((StreamateHttpClient)site.getHttpClient()).getUserId(); + // RequestBody body = new FormBody.Builder() + // .add("method", "tipModel") + // .add("args[]", getName()) + // .add("args[]", Integer.toString(tokens)) + // .add("args[]", Integer.toString(userId)) + // .add("args[3]", "") + // .build(); + // 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", Streamate.BASE_URL + '/' + getName()) + // .addHeader("X-Requested-With", "XMLHttpRequest") + // .post(body) + // .build(); + // try(Response response = site.getHttpClient().execute(request)) { + // if(response.isSuccessful()) { + // JSONObject json = new JSONObject(response.body().string()); + // if(!json.optString("status").equals("success")) { + // LOG.error("Sending tip failed {}", json.toString(2)); + // throw new IOException("Sending tip failed"); + // } + // } else { + // throw new IOException(response.code() + ' ' + response.message()); + // } + // } + } + + @Override + public int[] getStreamResolution(boolean failFast) throws ExecutionException { + if(resolution == null) { + if(failFast) { + return new int[2]; + } + try { + if(!isOnline()) { + return new int[2]; + } + List streamSources = getStreamSources(); + Collections.sort(streamSources); + StreamSource best = streamSources.get(streamSources.size()-1); + resolution = new int[] {best.width, best.height}; + } catch (ExecutionException | IOException | ParseException | PlaylistException | InterruptedException e) { + LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); + } + return resolution; + } else { + return resolution; + } + } + + @Override + public boolean follow() throws IOException { + return false; + } + + @Override + public boolean unfollow() throws IOException { + return false; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} From 64c60eaeaabec6a94db32ddd2d68563c8330618d Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 14 Dec 2018 02:39:07 +0100 Subject: [PATCH 204/231] Add determination of stream url and stream sources --- .../streamate/StreamateUpdateService.java | 3 +- .../src/main/java/ctbrec/io/HttpClient.java | 7 + .../sites/streamate/StreamateModel.java | 130 ++++++++++++------ .../streamate/StreamateWebsocketClient.java | 74 ++++++++++ 4 files changed, 167 insertions(+), 47 deletions(-) create mode 100644 common/src/main/java/ctbrec/sites/streamate/StreamateWebsocketClient.java diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java index 1c594e93..7f9816c6 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java @@ -64,7 +64,6 @@ public class StreamateUpdateService extends PaginatedScheduledService { if (response.isSuccessful()) { List models = new ArrayList<>(); String content = response.body().string(); - LOG.debug(content); ByteArrayInputStream in = new ByteArrayInputStream(content.getBytes("utf-8")); Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in); NodeList performers = doc.getElementsByTagName("Performer"); @@ -81,7 +80,7 @@ public class StreamateUpdateService extends PaginatedScheduledService { Node pic = XmlParserUtils.getNodeWithXpath(performer, "Media/Pic/Full"); String previewUrl = "https:" + pic.getAttributes().getNamedItem("Src").getNodeValue(); model.setPreview(previewUrl); - LOG.debug("Name {} - {}{}{}", name, PartyChat, PreGoldShow, GoldShow); + //LOG.debug("Name {} - {}{}{}", name, PartyChat, PreGoldShow, GoldShow); } return models; } else { diff --git a/common/src/main/java/ctbrec/io/HttpClient.java b/common/src/main/java/ctbrec/io/HttpClient.java index 9dc81010..5b2d8d9c 100644 --- a/common/src/main/java/ctbrec/io/HttpClient.java +++ b/common/src/main/java/ctbrec/io/HttpClient.java @@ -29,6 +29,8 @@ import okhttp3.OkHttpClient.Builder; import okhttp3.Request; import okhttp3.Response; import okhttp3.Route; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; public abstract class HttpClient { private static final transient Logger LOG = LoggerFactory.getLogger(HttpClient.class); @@ -219,4 +221,9 @@ public abstract class HttpClient { getCookieJar().clear(); loggedIn = false; } + + public WebSocket newWebSocket(String url, WebSocketListener l) { + Request request = new Request.Builder().url(url).build(); + return client.newWebSocket(request, l); + } } diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java index 20c80be9..cecf91da 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java @@ -4,11 +4,13 @@ import static ctbrec.Model.State.*; import java.io.IOException; import java.io.InputStream; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; +import org.json.JSONArray; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,9 +30,7 @@ import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.io.HttpException; import ctbrec.recorder.download.StreamSource; -import okhttp3.FormBody; import okhttp3.Request; -import okhttp3.RequestBody; import okhttp3.Response; public class StreamateModel extends AbstractModel { @@ -54,32 +54,6 @@ public class StreamateModel extends AbstractModel { return online; } - private JSONObject getRoomData() throws IOException { - String url = Streamate.BASE_URL + "/tools/amf.php"; - RequestBody body = new FormBody.Builder() - .add("method", "getRoomData") - .add("args[]", getName()) - .add("args[]", "false") - .build(); - 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", Streamate.BASE_URL) - .addHeader("X-Requested-With", "XMLHttpRequest") - .post(body) - .build(); - try(Response response = site.getHttpClient().execute(request)) { - if(response.isSuccessful()) { - JSONObject json = new JSONObject(response.body().string()); - return json; - } else { - throw new IOException(response.code() + " " + response.message()); - } - } - } - public void setOnline(boolean online) { this.online = online; } @@ -107,6 +81,7 @@ public class StreamateModel extends AbstractModel { if (streamUrl == null) { return Collections.emptyList(); } + LOG.debug(streamUrl); Request req = new Request.Builder().url(streamUrl).build(); try(Response response = site.getHttpClient().execute(req)) { if(response.isSuccessful()) { @@ -117,7 +92,7 @@ public class StreamateModel extends AbstractModel { streamSources.clear(); for (PlaylistData playlistData : master.getPlaylists()) { StreamSource streamsource = new StreamSource(); - streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", playlistData.getUri()); + streamsource.mediaPlaylistUrl = playlistData.getUri(); if (playlistData.hasStreamInfo()) { StreamInfo info = playlistData.getStreamInfo(); streamsource.bandwidth = info.getBandwidth(); @@ -138,6 +113,85 @@ public class StreamateModel extends AbstractModel { } private String getStreamUrl() throws IOException { + JSONObject json = getRoomInfo(); + JSONObject performer = json.getJSONObject("performer"); + id = Long.toString(performer.getLong("id")); + JSONObject stream = json.getJSONObject("stream"); + String sserver = stream.getString("serverId"); + String streamId = stream.getString("streamId"); + String wsHost = stream.getString("nodeHost"); + JSONObject liveservices = json.getJSONObject("liveservices"); + String streamHost = liveservices.getString("host").replace("wss", "https"); + + String roomId; + try { + roomId = getRoomId(wsHost, sserver, streamId); + LOG.debug("room id: {}", roomId); + } catch (InterruptedException e) { + throw new IOException("Couldn't get room id", e); + } + + String streamFormatUrl = getStreamFormatUrl(streamHost, roomId); + return getMasterPlaylistUrl(streamFormatUrl); + } + + private String getMasterPlaylistUrl(String url) throws IOException { + LOG.debug(url); + Request req = new Request.Builder() + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "*/*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", Streamate.BASE_URL + '/' + getName()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .url(url) + .build(); + try(Response response = site.getHttpClient().execute(req)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + JSONObject formats = json.getJSONObject("formats"); + JSONObject hls = formats.getJSONObject("mp4-hls"); + return hls.getString("manifest"); + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private String getStreamFormatUrl(String streamHost, String roomId) throws IOException { + String url = streamHost + "/videourl?payload=" + + URLEncoder.encode("{\"puserid\":" + id + ",\"roomid\":\"" + roomId + "\",\"showtype\":1,\"nginx\":1}", "utf-8"); + LOG.debug(url); + Request req = new Request.Builder() + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "*/*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", Streamate.BASE_URL + '/' + getName()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .url(url) + .build(); + try(Response response = site.getHttpClient().execute(req)) { + if(response.isSuccessful()) { + JSONArray streamConfig = new JSONArray(response.body().string()); + JSONObject obj = streamConfig.getJSONObject(0); + return obj.getString("url"); + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private String getRoomId(String wsHost, String sserver, String streamId) throws InterruptedException { + String wsUrl = wsHost + "/socket.io/?" + + "performerid=" + id + + "&sserver=" + sserver + + "&streamid=" + streamId + + "&sakey=&sessiontype=preview&perfdiscountid=0&minduration=0&goldshowid=0&version=7&referrer=hybrid.client.6.3.16/avchat.swf&usertype=false&lang=en&EIO=3&transport=websocket"; + + StreamateWebsocketClient wsClient = new StreamateWebsocketClient(wsUrl, site.getHttpClient()); + return wsClient.getRoomId(); + } + + private JSONObject getRoomInfo() throws IOException { String url = "https://hybridclient.naiadsystems.com/api/v1/config/?sabasic=&sakey=&sk=www.streamate.com&userid=0&version=6.3.16&ajax=1&name=" + getName(); Request req = new Request.Builder() .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) @@ -149,25 +203,11 @@ public class StreamateModel extends AbstractModel { .build(); try(Response response = site.getHttpClient().execute(req)) { if(response.isSuccessful()) { - JSONObject json = new JSONObject(response.body().string()); - JSONObject performer = json.getJSONObject("performer"); - id = performer.getString("id"); - JSONObject stream = json.getJSONObject("stream"); - String sserver = stream.getString("serverId"); - String streamId = stream.getString("streamId"); - String wsHost = stream.getString("nodeHost"); - LOG.debug(json.toString(2)); - - String wsUrl = wsHost + "/socket.io/?" - + "performerid=" + id - + "&sserver=" + sserver - + "&streamid=" + streamId - + "&sakey=&sessiontype=preview&perfdiscountid=0&minduration=0&goldshowid=0&version=7&referrer=hybrid.client.6.3.16/avchat.swf&usertype=false&lang=en&EIO=3&transport=websocket"; + return new JSONObject(response.body().string()); } else { - throw new IOException(response.code() + ' ' + response.message()); + throw new HttpException(response.code(), response.message()); } } - return ""; } @Override diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateWebsocketClient.java b/common/src/main/java/ctbrec/sites/streamate/StreamateWebsocketClient.java new file mode 100644 index 00000000..d13cde56 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateWebsocketClient.java @@ -0,0 +1,74 @@ +package ctbrec.sites.streamate; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.io.HttpClient; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +public class StreamateWebsocketClient { + + private static final transient Logger LOG = LoggerFactory.getLogger(StreamateWebsocketClient.class); + private String url; + private HttpClient client; + + public StreamateWebsocketClient(String url, HttpClient client) { + this.url = url; + this.client = client; + } + + String roomId = ""; + public String getRoomId() throws InterruptedException { + LOG.debug("Connecting to {}", url); + Object monitor = new Object(); + client.newWebSocket(url, new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, Response response) { + response.close(); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + if(text.contains("NaiadAuthorized")) { + Matcher m = Pattern.compile("\"roomid\":\"(.*?)\"").matcher(text); + if(m.find()) { + roomId = m.group(1); + webSocket.close(1000, ""); + } + } + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + LOG.debug("ws btxt {}", bytes.toString()); + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + synchronized (monitor) { + monitor.notify(); + } + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + LOG.debug("ws failure", t); + response.close(); + synchronized (monitor) { + monitor.notify(); + } + } + }); + synchronized (monitor) { + monitor.wait(); + } + return roomId; + } +} + From 70f4fa930f810dbd390cd207457fbd31b8de1353 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 14 Dec 2018 13:42:20 +0100 Subject: [PATCH 205/231] Implement search for Streamate --- .../ctbrec/sites/streamate/Streamate.java | 156 +++++++++--------- .../sites/streamate/StreamateHttpClient.java | 11 ++ .../sites/streamate/StreamateModel.java | 9 +- 3 files changed, 96 insertions(+), 80 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/streamate/Streamate.java b/common/src/main/java/ctbrec/sites/streamate/Streamate.java index e0da236b..12cf63c7 100644 --- a/common/src/main/java/ctbrec/sites/streamate/Streamate.java +++ b/common/src/main/java/ctbrec/sites/streamate/Streamate.java @@ -1,17 +1,25 @@ package ctbrec.sites.streamate; import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +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.io.HttpException; import ctbrec.sites.AbstractSite; +import okhttp3.Request; +import okhttp3.Response; public class Streamate extends AbstractSite { @@ -33,7 +41,8 @@ public class Streamate extends AbstractSite { @Override public String getAffiliateLink() { - return BASE_URL + "/landing/click/?AFNO=2-11330.2"; + return BASE_URL + "/landing/click/?AFNO=2-11329.1"; + // return BASE_URL + "/landing/click/?AFNO=2-11330.2"; } @Override @@ -48,34 +57,34 @@ public class Streamate extends AbstractSite { @Override public Integer getTokenBalance() throws IOException { - // int userId = ((StreamateHttpClient)getHttpClient()).getUserId(); - // String url = Streamate.BASE_URL + "/tools/amf.php"; - // RequestBody body = new FormBody.Builder() - // .add("method", "ping") - // .add("args[]", Integer.toString(userId)) - // .build(); - // 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", Streamate.BASE_URL) - // .addHeader("X-Requested-With", "XMLHttpRequest") - // .post(body) - // .build(); - // try(Response response = getHttpClient().execute(request)) { - // if(response.isSuccessful()) { - // JSONObject json = new JSONObject(response.body().string()); - // if(json.optString("status").equals("online")) { - // JSONObject userData = json.getJSONObject("userData"); - // return userData.getInt("balance"); - // } else { - // throw new IOException("Request was not successful: " + json.toString(2)); - // } - // } else { - // throw new HttpException(response.code(), response.message()); - // } - // } + // int userId = ((StreamateHttpClient)getHttpClient()).getUserId(); + // String url = Streamate.BASE_URL + "/tools/amf.php"; + // RequestBody body = new FormBody.Builder() + // .add("method", "ping") + // .add("args[]", Integer.toString(userId)) + // .build(); + // 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", Streamate.BASE_URL) + // .addHeader("X-Requested-With", "XMLHttpRequest") + // .post(body) + // .build(); + // try(Response response = getHttpClient().execute(request)) { + // if(response.isSuccessful()) { + // JSONObject json = new JSONObject(response.body().string()); + // if(json.optString("status").equals("online")) { + // JSONObject userData = json.getJSONObject("userData"); + // return userData.getInt("balance"); + // } else { + // throw new IOException("Request was not successful: " + json.toString(2)); + // } + // } else { + // throw new HttpException(response.code(), response.message()); + // } + // } return 0; } @@ -91,7 +100,7 @@ public class Streamate extends AbstractSite { @Override public HttpClient getHttpClient() { - if(httpClient == null) { + if (httpClient == null) { httpClient = new StreamateHttpClient(); } return httpClient; @@ -103,7 +112,7 @@ public class Streamate extends AbstractSite { @Override public void shutdown() { - if(httpClient != null) { + if (httpClient != null) { httpClient.shutdown(); } } @@ -120,54 +129,51 @@ public class Streamate extends AbstractSite { @Override public boolean supportsSearch() { - return false; - } - - @Override - public boolean searchRequiresLogin() { return true; } + @Override + public boolean searchRequiresLogin() { + return false; + } + @Override public List search(String q) throws IOException, InterruptedException { - // String url = BASE_URL + "/tools/listing_v3.php?offset=0&model_search[display_name][text]=" + URLEncoder.encode(q, "utf-8"); - // Request req = new Request.Builder() - // .url(url) - // .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) - // .addHeader("Accept", "application/json, text/javascript, */*") - // .addHeader("Accept-Language", "en") - // .addHeader("Referer", Streamate.BASE_URL) - // .addHeader("X-Requested-With", "XMLHttpRequest") - // .build(); - // try(Response response = getHttpClient().execute(req)) { - // if(response.isSuccessful()) { - // String body = response.body().string(); - // JSONObject json = new JSONObject(body); - // if(json.optString("status").equals("success")) { - // List models = new ArrayList<>(); - // JSONArray results = json.getJSONArray("models"); - // for (int i = 0; i < results.length(); i++) { - // JSONObject result = results.getJSONObject(i); - // Model model = createModel(result.getString("username")); - // String thumb = result.getString("thumb_image"); - // if(thumb != null) { - // model.setPreview("https:" + thumb); - // } - // if(result.has("display_name")) { - // model.setDisplayName(result.getString("display_name")); - // } - // models.add(model); - // } - // return models; - // } else { - // LOG.warn("Search result: " + json.toString(2)); - // return Collections.emptyList(); - // } - // } else { - // throw new HttpException(response.code(), response.message()); - // } - // } - return Collections.emptyList(); + String url = BASE_URL + "/api/search/autocomplete?exact=false&skin_search_kids=0&results_per_page=10&query=" + URLEncoder.encode(q, "utf-8"); + Request req = new Request.Builder().url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", Streamate.BASE_URL) + .addHeader("X-Requested-With", "XMLHttpRequest").build(); + try (Response response = getHttpClient().execute(req)) { + if (response.isSuccessful()) { + String body = response.body().string(); + JSONObject json = new JSONObject(body); + if (json.optString("status").equals("SM_OK")) { + List models = new ArrayList<>(); + JSONObject results = json.getJSONObject("results"); + JSONArray nickname = results.getJSONArray("nickname"); + for (int i = 0; i < nickname.length(); i++) { + JSONObject result = nickname.getJSONObject(i); + StreamateModel model = (StreamateModel) createModel(result.getString("nickname")); + model.setId(result.getString("performerId")); + String thumb = result.getString("thumbnail"); + if (thumb != null) { + model.setPreview(thumb); + } + model.setOnline(result.optString("liveStatus").equals("live")); + models.add(model); + } + return models; + } else { + LOG.warn("Search result: " + json.toString(2)); + return Collections.emptyList(); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } } @Override @@ -183,7 +189,7 @@ public class Streamate extends AbstractSite { @Override public Model createModelFromUrl(String url) { Matcher m = Pattern.compile("https?://.*?streamate.com/cam/([^/]*?)/?").matcher(url); - if(m.matches()) { + if (m.matches()) { String modelName = m.group(1); return createModel(modelName); } else { diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java b/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java index 6772eadd..aa64b61f 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java @@ -1,11 +1,14 @@ package ctbrec.sites.streamate; import java.io.IOException; +import java.util.Collections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.io.HttpClient; +import okhttp3.Cookie; +import okhttp3.HttpUrl; public class StreamateHttpClient extends HttpClient { @@ -13,6 +16,14 @@ public class StreamateHttpClient extends HttpClient { public StreamateHttpClient() { super("streamate"); + + // this cookie is needed for the search + Cookie searchCookie = new Cookie.Builder() + .domain("streamate.com") + .name("Xld_rct") + .value("1") + .build(); + getCookieJar().saveFromResponse(HttpUrl.parse(Streamate.BASE_URL), Collections.singletonList(searchCookie)); } @Override diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java index cecf91da..425009d7 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java @@ -45,11 +45,10 @@ public class StreamateModel extends AbstractModel { @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { if(ignoreCache) { - String url = getStreamUrl(); - Request req = new Request.Builder().url(url).build(); - try(Response resp = site.getHttpClient().execute(req)) { - online = resp.isSuccessful(); - } + JSONObject roomInfo = getRoomInfo(); + JSONObject stream = roomInfo.getJSONObject("stream"); + String serverId = stream.optString("serverId"); + online = !serverId.equals("0"); } return online; } From 461e65ed84c4aac989b8776c3543bdfb9b4c0ad6 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 14 Dec 2018 14:58:12 +0100 Subject: [PATCH 206/231] Switch to much simpler JSON api --- .../sites/streamate/StreamateTabProvider.java | 27 ++- .../streamate/StreamateUpdateService.java | 77 ++++---- .../ctbrec/ui/sites/streamate/girls.sml | 18 -- .../sites/streamate/StreamateModel.java | 168 ++++-------------- 4 files changed, 79 insertions(+), 211 deletions(-) delete mode 100644 client/src/main/resources/ctbrec/ui/sites/streamate/girls.sml diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java index 5c74b825..137854a5 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java @@ -1,8 +1,6 @@ package ctbrec.ui.sites.streamate; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -30,7 +28,15 @@ public class StreamateTabProvider extends TabProvider { public List getTabs(Scene scene) { List tabs = new ArrayList<>(); try { - tabs.add(createTab("Girls", "/ctbrec/ui/sites/streamate/girls.sml")); + tabs.add(createTab("Girls", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:f")); + tabs.add(createTab("Guys", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:m")); + tabs.add(createTab("Couples", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:mf")); + tabs.add(createTab("Lesbian", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:ff")); + tabs.add(createTab("Gay", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:mm")); + tabs.add(createTab("Groups", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:g")); + tabs.add(createTab("Trans female", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:tm2f")); + tabs.add(createTab("Trans male", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:tf2m")); + tabs.add(createTab("New", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=new:true")); } catch (IOException e) { LOG.error("Couldn't create streamate tab", e); } @@ -42,21 +48,10 @@ public class StreamateTabProvider extends TabProvider { return null; } - private Tab createTab(String title, String queryFile) throws IOException { - StreamateUpdateService updateService = new StreamateUpdateService(loadQuery(queryFile), streamate); + private Tab createTab(String title, String url) throws IOException { + StreamateUpdateService updateService = new StreamateUpdateService(streamate, url); ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, streamate); tab.setRecorder(recorder); return tab; } - - private String loadQuery(String file) throws IOException { - InputStream is = getClass().getResourceAsStream(file); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - byte[] b = new byte[1024]; - int len = -1; - while( (len = is.read(b)) >= 0) { - bos.write(b, 0, len); - } - return new String(bos.toByteArray(), "utf-8"); - } } diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java index 7f9816c6..0703e662 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java @@ -1,44 +1,39 @@ package ctbrec.ui.sites.streamate; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathExpressionException; +import org.json.JSONArray; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import ctbrec.Config; import ctbrec.Model; -import ctbrec.io.XmlParserUtils; +import ctbrec.io.HttpException; import ctbrec.sites.streamate.Streamate; import ctbrec.sites.streamate.StreamateModel; import ctbrec.ui.PaginatedScheduledService; import javafx.concurrent.Task; -import okhttp3.MediaType; import okhttp3.Request; -import okhttp3.RequestBody; import okhttp3.Response; public class StreamateUpdateService extends PaginatedScheduledService { private static final transient Logger LOG = LoggerFactory.getLogger(StreamateUpdateService.class); - private static final String URL = "http://affiliate.streamate.com/SMLive/SMLResult.xml"; + private static final int MODELS_PER_PAGE = 48; private Streamate streamate; - private String query; + private String url; - public StreamateUpdateService(String query, Streamate streamate) { - this.query = query; + public StreamateUpdateService(Streamate streamate, String url) { this.streamate = streamate; + this.url = url; } @Override @@ -46,47 +41,35 @@ public class StreamateUpdateService extends PaginatedScheduledService { return new Task>() { @Override public List call() throws IOException, SAXException, ParserConfigurationException, XPathExpressionException { - LOG.debug("Fetching page {}", URL); - String q = query - .replace("{maxresults}", "50") - .replace("{pagenum}", Integer.toString(page)); - //LOG.debug("Query:\n{}", q); - RequestBody body = RequestBody.create(MediaType.parse("text/xml"), q); + int from = (page - 1) * MODELS_PER_PAGE; + String _url = url + "&from=" + from + "&size=" + MODELS_PER_PAGE; + LOG.debug("Fetching page {}", _url); Request request = new Request.Builder() - .url(URL) + .url(_url) .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) - .addHeader("Accept", "text/xml, */*") + .addHeader("Accept", "application/json, */*") .addHeader("Accept-Language", "en") .addHeader("Referer", streamate.getBaseUrl()) - .post(body) .build(); - Response response = streamate.getHttpClient().execute(request); - if (response.isSuccessful()) { - List models = new ArrayList<>(); - String content = response.body().string(); - ByteArrayInputStream in = new ByteArrayInputStream(content.getBytes("utf-8")); - Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(in); - NodeList performers = doc.getElementsByTagName("Performer"); - for (int i = 0; i < performers.getLength(); i++) { - Node performer = performers.item(i); - String name = performer.getAttributes().getNamedItem("Name").getNodeValue(); - String id = performer.getAttributes().getNamedItem("Id").getNodeValue(); - String GoldShow = performer.getAttributes().getNamedItem("GoldShow").getNodeValue(); - String PreGoldShow = performer.getAttributes().getNamedItem("PreGoldShow").getNodeValue(); - String PartyChat = performer.getAttributes().getNamedItem("PartyChat").getNodeValue(); - StreamateModel model = (StreamateModel) streamate.createModel(name); - model.setId(id); - models.add(model); - Node pic = XmlParserUtils.getNodeWithXpath(performer, "Media/Pic/Full"); - String previewUrl = "https:" + pic.getAttributes().getNamedItem("Src").getNodeValue(); - model.setPreview(previewUrl); - //LOG.debug("Name {} - {}{}{}", name, PartyChat, PreGoldShow, GoldShow); + try(Response response = streamate.getHttpClient().execute(request)) { + if (response.isSuccessful()) { + List models = new ArrayList<>(); + String content = response.body().string(); + JSONObject json = new JSONObject(content); + JSONArray performers = json.getJSONArray("performers"); + for (int i = 0; i < performers.length(); i++) { + JSONObject p = performers.getJSONObject(i); + String nickname = p.getString("nickname"); + StreamateModel model = (StreamateModel) streamate.createModel(nickname); + model.setId(Long.toString(p.getLong("id"))); + model.setPreview(p.getString("thumbnail")); + model.setOnline(p.optBoolean("online")); + models.add(model); + } + return models; + } else { + throw new HttpException(response.code(), response.message()); } - return models; - } else { - int code = response.code(); - response.close(); - throw new IOException("HTTP status " + code); } } }; diff --git a/client/src/main/resources/ctbrec/ui/sites/streamate/girls.sml b/client/src/main/resources/ctbrec/ui/sites/streamate/girls.sml deleted file mode 100644 index f7841ae4..00000000 --- a/client/src/main/resources/ctbrec/ui/sites/streamate/girls.sml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - biopic, staticbiopic - - - - live,recorded - - - - \ No newline at end of file diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java index 425009d7..66e58376 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java @@ -3,8 +3,6 @@ package ctbrec.sites.streamate; import static ctbrec.Model.State.*; import java.io.IOException; -import java.io.InputStream; -import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -15,16 +13,8 @@ import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.iheartradio.m3u8.Encoding; -import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; -import com.iheartradio.m3u8.ParsingMode; import com.iheartradio.m3u8.PlaylistException; -import com.iheartradio.m3u8.PlaylistParser; -import com.iheartradio.m3u8.data.MasterPlaylist; -import com.iheartradio.m3u8.data.Playlist; -import com.iheartradio.m3u8.data.PlaylistData; -import com.iheartradio.m3u8.data.StreamInfo; import ctbrec.AbstractModel; import ctbrec.Config; @@ -45,10 +35,17 @@ public class StreamateModel extends AbstractModel { @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { if(ignoreCache) { - JSONObject roomInfo = getRoomInfo(); - JSONObject stream = roomInfo.getJSONObject("stream"); - String serverId = stream.optString("serverId"); - online = !serverId.equals("0"); + String url = "https://sea1c-ls.naiadsystems.com/sea1c-edge-ls/80/live/s:" + getName() + ".json"; + Request req = new Request.Builder().url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "*/*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", Streamate.BASE_URL + '/' + getName()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + try(Response response = site.getHttpClient().execute(req)) { + online = response.isSuccessful(); + } } return online; } @@ -76,137 +73,48 @@ public class StreamateModel extends AbstractModel { @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { - String streamUrl = getStreamUrl(); - if (streamUrl == null) { - return Collections.emptyList(); - } - LOG.debug(streamUrl); - Request req = new Request.Builder().url(streamUrl).build(); - try(Response response = site.getHttpClient().execute(req)) { - if(response.isSuccessful()) { - InputStream inputStream = response.body().byteStream(); - PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); - Playlist playlist = parser.parse(); - MasterPlaylist master = playlist.getMasterPlaylist(); - streamSources.clear(); - for (PlaylistData playlistData : master.getPlaylists()) { - StreamSource streamsource = new StreamSource(); - streamsource.mediaPlaylistUrl = playlistData.getUri(); - if (playlistData.hasStreamInfo()) { - StreamInfo info = playlistData.getStreamInfo(); - streamsource.bandwidth = info.getBandwidth(); - streamsource.width = info.hasResolution() ? info.getResolution().width : 0; - streamsource.height = info.hasResolution() ? info.getResolution().height : 0; - } else { - streamsource.bandwidth = 0; - streamsource.width = 0; - streamsource.height = 0; - } - streamSources.add(streamsource); - } - } else { - throw new HttpException(response.code(), response.message()); - } - } - return streamSources; - } - - private String getStreamUrl() throws IOException { - JSONObject json = getRoomInfo(); - JSONObject performer = json.getJSONObject("performer"); - id = Long.toString(performer.getLong("id")); - JSONObject stream = json.getJSONObject("stream"); - String sserver = stream.getString("serverId"); - String streamId = stream.getString("streamId"); - String wsHost = stream.getString("nodeHost"); - JSONObject liveservices = json.getJSONObject("liveservices"); - String streamHost = liveservices.getString("host").replace("wss", "https"); - - String roomId; - try { - roomId = getRoomId(wsHost, sserver, streamId); - LOG.debug("room id: {}", roomId); - } catch (InterruptedException e) { - throw new IOException("Couldn't get room id", e); - } - - String streamFormatUrl = getStreamFormatUrl(streamHost, roomId); - return getMasterPlaylistUrl(streamFormatUrl); - } - - private String getMasterPlaylistUrl(String url) throws IOException { - LOG.debug(url); - Request req = new Request.Builder() + String url = "https://sea1c-ls.naiadsystems.com/sea1c-edge-ls/80/live/s:" + getName() + ".json"; + Request req = new Request.Builder().url(url) .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) .addHeader("Accept", "*/*") .addHeader("Accept-Language", "en") .addHeader("Referer", Streamate.BASE_URL + '/' + getName()) .addHeader("X-Requested-With", "XMLHttpRequest") - .url(url) .build(); try(Response response = site.getHttpClient().execute(req)) { if(response.isSuccessful()) { JSONObject json = new JSONObject(response.body().string()); JSONObject formats = json.getJSONObject("formats"); + JSONObject ws = formats.getJSONObject("mp4-ws"); JSONObject hls = formats.getJSONObject("mp4-hls"); - return hls.getString("manifest"); - } else { - throw new HttpException(response.code(), response.message()); - } - } - } - - private String getStreamFormatUrl(String streamHost, String roomId) throws IOException { - String url = streamHost + "/videourl?payload=" - + URLEncoder.encode("{\"puserid\":" + id + ",\"roomid\":\"" + roomId + "\",\"showtype\":1,\"nginx\":1}", "utf-8"); - LOG.debug(url); - Request req = new Request.Builder() - .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) - .addHeader("Accept", "*/*") - .addHeader("Accept-Language", "en") - .addHeader("Referer", Streamate.BASE_URL + '/' + getName()) - .addHeader("X-Requested-With", "XMLHttpRequest") - .url(url) - .build(); - try(Response response = site.getHttpClient().execute(req)) { - if(response.isSuccessful()) { - JSONArray streamConfig = new JSONArray(response.body().string()); - JSONObject obj = streamConfig.getJSONObject(0); - return obj.getString("url"); - } else { - throw new HttpException(response.code(), response.message()); - } - } - } - - private String getRoomId(String wsHost, String sserver, String streamId) throws InterruptedException { - String wsUrl = wsHost + "/socket.io/?" - + "performerid=" + id - + "&sserver=" + sserver - + "&streamid=" + streamId - + "&sakey=&sessiontype=preview&perfdiscountid=0&minduration=0&goldshowid=0&version=7&referrer=hybrid.client.6.3.16/avchat.swf&usertype=false&lang=en&EIO=3&transport=websocket"; - - StreamateWebsocketClient wsClient = new StreamateWebsocketClient(wsUrl, site.getHttpClient()); - return wsClient.getRoomId(); - } - - private JSONObject getRoomInfo() throws IOException { - String url = "https://hybridclient.naiadsystems.com/api/v1/config/?sabasic=&sakey=&sk=www.streamate.com&userid=0&version=6.3.16&ajax=1&name=" + getName(); - Request req = new Request.Builder() - .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) - .addHeader("Accept", "application/json, text/javascript, */*") - .addHeader("Accept-Language", "en") - .addHeader("Referer", Streamate.BASE_URL + '/' + getName()) - .addHeader("X-Requested-With", "XMLHttpRequest") - .url(url) - .build(); - try(Response response = site.getHttpClient().execute(req)) { - if(response.isSuccessful()) { - return new JSONObject(response.body().string()); + + // add encodings + JSONArray encodings = hls.getJSONArray("encodings"); + streamSources.clear(); + for (int i = 0; i < encodings.length(); i++) { + JSONObject encoding = encodings.getJSONObject(i); + StreamSource src = new StreamSource(); + src.mediaPlaylistUrl = encoding.getString("location"); + src.width = encoding.optInt("videoWidth"); + src.height = encoding.optInt("videoHeight"); + src.bandwidth = (encoding.optInt("videoKbps") + encoding.optInt("audioKbps")) * 1024; + streamSources.add(src); + } + + // add raw source stream + JSONObject origin = hls.getJSONObject("origin"); + StreamSource src = new StreamSource(); + src.mediaPlaylistUrl = origin.getString("location"); + origin = ws.getJSONObject("origin"); // switch to web socket origin, because it has width, height and bitrates + src.width = origin.optInt("videoWidth"); + src.height = origin.optInt("videoHeight"); + src.bandwidth = (origin.optInt("videoKbps") + origin.optInt("audioKbps")) * 1024; + streamSources.add(src); } else { throw new HttpException(response.code(), response.message()); } } + return streamSources; } @Override From 6b52906811529f6d55340b69e98369ed7a8a412a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 14 Dec 2018 15:51:15 +0100 Subject: [PATCH 207/231] Add configuration ui for the credentials --- .../ui/sites/streamate/StreamateConfigUI.java | 86 +++++++++++++++++++ .../ui/sites/streamate/StreamateSiteUi.java | 4 +- .../streamate/StreamateUpdateService.java | 9 ++ common/src/main/java/ctbrec/Settings.java | 6 +- 4 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/streamate/StreamateConfigUI.java diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateConfigUI.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateConfigUI.java new file mode 100644 index 00000000..d338cab1 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateConfigUI.java @@ -0,0 +1,86 @@ +package ctbrec.ui.sites.streamate; + +import ctbrec.Config; +import ctbrec.Settings; +import ctbrec.sites.streamate.Streamate; +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 StreamateConfigUI extends AbstractConfigUI { + private Streamate streamate; + + public StreamateConfigUI(Streamate streamate) { + this.streamate = streamate; + } + + @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(streamate.getName())); + enabled.setOnAction((e) -> { + if(enabled.isSelected()) { + settings.disabledSites.remove(streamate.getName()); + } else { + settings.disabledSites.add(streamate.getName()); + } + save(); + }); + GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + layout.add(enabled, 1, row++); + + layout.add(new Label("Streamate User"), 0, row); + TextField username = new TextField(settings.streamateUsername); + username.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().streamateUsername)) { + Config.getInstance().getSettings().streamateUsername = username.getText(); + streamate.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("Streamate Password"), 0, row); + PasswordField password = new PasswordField(); + password.setText(settings.streamatePassword); + password.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().streamatePassword)) { + Config.getInstance().getSettings().streamatePassword = password.getText(); + streamate.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(streamate.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/streamate/StreamateSiteUi.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java index 8d31d020..bd29af2b 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java @@ -10,9 +10,11 @@ import ctbrec.ui.TabProvider; public class StreamateSiteUi implements SiteUI { private StreamateTabProvider tabProvider; + private StreamateConfigUI configUi; public StreamateSiteUi(Streamate streamate) { tabProvider = new StreamateTabProvider(streamate); + configUi = new StreamateConfigUI(streamate); } @Override @@ -22,7 +24,7 @@ public class StreamateSiteUi implements SiteUI { @Override public ConfigUI getConfigUI() { - return null; + return configUi; } @Override diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java index 0703e662..37ea045c 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java @@ -64,6 +64,15 @@ public class StreamateUpdateService extends PaginatedScheduledService { model.setId(Long.toString(p.getLong("id"))); model.setPreview(p.getString("thumbnail")); model.setOnline(p.optBoolean("online")); + // TODO figure out, what all the states mean + // liveState {…} + // exclusiveShow false + // goldShow true + // onBreak false + // partyChat true + // preGoldShow true + // privateChat false + // specialShow false models.add(model); } return models; diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 043b55af..168f2e25 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -60,8 +60,10 @@ public class Settings { public boolean mfcIgnoreUpscaled = false; public String camsodaUsername = ""; public String camsodaPassword = ""; - public String cam4Username; - public String cam4Password; + public String cam4Username = ""; + public String cam4Password = ""; + public String streamateUsername = ""; + public String streamatePassword = ""; public String lastDownloadDir = ""; public List models = new ArrayList<>(); From c7e07b4b261cf0e793a99d39d77755539880c95d Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 14 Dec 2018 17:36:24 +0100 Subject: [PATCH 208/231] Implement login and favorites tab --- .../streamate/StreamateFollowedService.java | 94 +++++++++++++++++++ .../sites/streamate/StreamateFollowedTab.java | 77 +++++++++++++++ .../ui/sites/streamate/StreamateSiteUi.java | 4 +- .../sites/streamate/StreamateTabProvider.java | 7 +- .../ctbrec/sites/streamate/Streamate.java | 5 +- .../sites/streamate/StreamateHttpClient.java | 51 +++++++++- 6 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java create mode 100644 client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedTab.java diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java new file mode 100644 index 00000000..6aab7d5b --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java @@ -0,0 +1,94 @@ +package ctbrec.ui.sites.streamate; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPathExpressionException; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.SAXException; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HttpException; +import ctbrec.sites.streamate.Streamate; +import ctbrec.sites.streamate.StreamateHttpClient; +import ctbrec.sites.streamate.StreamateModel; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import okhttp3.Response; + +public class StreamateFollowedService extends PaginatedScheduledService { + + private static final transient Logger LOG = LoggerFactory.getLogger(StreamateFollowedService.class); + + private static final int MODELS_PER_PAGE = 48; + private Streamate streamate; + private StreamateHttpClient httpClient; + private String url; + private boolean showOnline = true; + + public StreamateFollowedService(Streamate streamate) { + this.streamate = streamate; + this.httpClient = (StreamateHttpClient) streamate.getHttpClient(); + this.url = streamate.getBaseUrl() + "/api/search/v1/favorites?host=streamate.com&domain=streamate.com"; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException, SAXException, ParserConfigurationException, XPathExpressionException { + httpClient.login(); + String saKey = httpClient.getSaKey(); + String userId = httpClient.getUserId(); + String _url = url + "&page_number=" + page + "&results_per_page=" + MODELS_PER_PAGE + "&sakey=" + saKey + "&userid=" + userId; + LOG.debug("Fetching page {}", _url); + Request request = new Request.Builder() + .url(_url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "application/json, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", streamate.getBaseUrl()) + .build(); + try(Response response = streamate.getHttpClient().execute(request)) { + if (response.isSuccessful()) { + List models = new ArrayList<>(); + String content = response.body().string(); + JSONObject json = new JSONObject(content); + if(json.optString("status").equals("SM_OK")) { + JSONArray performers = json.getJSONArray("Results"); + for (int i = 0; i < performers.length(); i++) { + JSONObject p = performers.getJSONObject(i); + String nickname = p.getString("Nickname"); + StreamateModel model = (StreamateModel) streamate.createModel(nickname); + model.setId(Long.toString(p.getLong("PerformerId"))); + model.setPreview("https://m1.nsimg.net/biopic/320x240/" + model.getId()); + boolean online = p.optString("LiveStatus").equals("live"); + model.setOnline(online); + if(online == showOnline) { + models.add(model); + } + } + } else { + throw new IOException("Status: " + json.optString("status")); + } + return models; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + }; + } + + public void setOnline(boolean online) { + this.showOnline = online; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedTab.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedTab.java new file mode 100644 index 00000000..f79cc30f --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedTab.java @@ -0,0 +1,77 @@ +package ctbrec.ui.sites.streamate; + +import ctbrec.sites.streamate.Streamate; +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 StreamateFollowedTab extends ThumbOverviewTab implements FollowedTab { + private Label status; + + public StreamateFollowedTab(Streamate streamate) { + super("Favorites", new StreamateFollowedService(streamate), streamate); + + 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) -> { + ((StreamateFollowedService)updateService).setOnline(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/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java index bd29af2b..c7348a1f 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java @@ -11,8 +11,10 @@ public class StreamateSiteUi implements SiteUI { private StreamateTabProvider tabProvider; private StreamateConfigUI configUi; + private Streamate streamate; public StreamateSiteUi(Streamate streamate) { + this.streamate = streamate; tabProvider = new StreamateTabProvider(streamate); configUi = new StreamateConfigUI(streamate); } @@ -29,7 +31,7 @@ public class StreamateSiteUi implements SiteUI { @Override public boolean login() throws IOException { - return false; + return streamate.login(); } } diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java index 137854a5..d43ec25f 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateTabProvider.java @@ -18,6 +18,7 @@ public class StreamateTabProvider extends TabProvider { private static final transient Logger LOG = LoggerFactory.getLogger(StreamateTabProvider.class); private Streamate streamate; private Recorder recorder; + private ThumbOverviewTab followedTab; public StreamateTabProvider(Streamate streamate) { this.streamate = streamate; @@ -37,6 +38,10 @@ public class StreamateTabProvider extends TabProvider { tabs.add(createTab("Trans female", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:tm2f")); tabs.add(createTab("Trans male", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:tf2m")); tabs.add(createTab("New", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=new:true")); + + followedTab = new StreamateFollowedTab(streamate); + followedTab.setRecorder(recorder); + tabs.add(followedTab); } catch (IOException e) { LOG.error("Couldn't create streamate tab", e); } @@ -45,7 +50,7 @@ public class StreamateTabProvider extends TabProvider { @Override public Tab getFollowedTab() { - return null; + return followedTab; } private Tab createTab(String title, String url) throws IOException { diff --git a/common/src/main/java/ctbrec/sites/streamate/Streamate.java b/common/src/main/java/ctbrec/sites/streamate/Streamate.java index 12cf63c7..390767f0 100644 --- a/common/src/main/java/ctbrec/sites/streamate/Streamate.java +++ b/common/src/main/java/ctbrec/sites/streamate/Streamate.java @@ -15,6 +15,7 @@ import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.Model; +import ctbrec.StringUtil; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import ctbrec.sites.AbstractSite; @@ -150,6 +151,7 @@ public class Streamate extends AbstractSite { if (response.isSuccessful()) { String body = response.body().string(); JSONObject json = new JSONObject(body); + LOG.debug(json.toString(2)); if (json.optString("status").equals("SM_OK")) { List models = new ArrayList<>(); JSONObject results = json.getJSONObject("results"); @@ -183,7 +185,8 @@ public class Streamate extends AbstractSite { @Override public boolean credentialsAvailable() { - return false; + String username = Config.getInstance().getSettings().username; + return StringUtil.isNotBlank(username); } @Override diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java b/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java index aa64b61f..d1242dac 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java @@ -3,17 +3,26 @@ package ctbrec.sites.streamate; import java.io.IOException; import java.util.Collections; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ctbrec.Config; import ctbrec.io.HttpClient; import okhttp3.Cookie; import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; public class StreamateHttpClient extends HttpClient { private static final transient Logger LOG = LoggerFactory.getLogger(StreamateHttpClient.class); + private String userId = ""; + private String saKey = ""; + public StreamateHttpClient() { super("streamate"); @@ -39,7 +48,38 @@ public class StreamateHttpClient extends HttpClient { return true; } - return false; + JSONObject loginRequest = new JSONObject(); + loginRequest.put("email", Config.getInstance().getSettings().streamateUsername); + loginRequest.put("password", Config.getInstance().getSettings().streamatePassword); + loginRequest.put("referrerId", 0); + loginRequest.put("siteId", 1); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), loginRequest.toString()); + Request login = new Request.Builder() + .url(Streamate.BASE_URL + "/api/member/login") + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", Streamate.BASE_URL) + .addHeader("X-Requested-With", "XMLHttpRequest") + .post(body) + .build(); + try (Response response = client.newCall(login).execute()) { + String content = response.body().string(); + //LOG.debug(content); + if(response.isSuccessful()) { + JSONObject json = new JSONObject(content); + LOG.debug(json.toString()); + loggedIn = json.has("sakey"); + saKey = json.optString("sakey"); + JSONObject account = json.getJSONObject("account"); + userId = Long.toString(account.getLong("userid")); + } else { + throw new IOException("Login failed: " + response.code() + " " + response.message()); + } + response.close(); + } + + return loggedIn; } /** @@ -47,6 +87,7 @@ public class StreamateHttpClient extends HttpClient { * @throws IOException */ public boolean checkLoginSuccess() throws IOException { + //https://www.streamate.com/api/search/v1/favorites?host=streamate.com&domain=streamate.com&page_number=1&results_per_page=48&sakey=62857cfd1908cd28 return false; // String modelName = getAnyModelName(); // // we request the roomData of a random model, because it contains @@ -83,4 +124,12 @@ public class StreamateHttpClient extends HttpClient { // } // } } + + public String getSaKey() { + return saKey; + } + + public String getUserId() { + return userId; + } } From 75ab95e1ea8f56c3c7bc98817fa4dcb1fd65c4f3 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 14 Dec 2018 17:43:38 +0100 Subject: [PATCH 209/231] Shut down more gracefully (hopefully) --- client/src/main/java/ctbrec/ui/CamrecApplication.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index d91a9d5e..b2913d98 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -178,9 +178,13 @@ public class CamrecApplication extends Application { try { Config.getInstance().save(); LOG.info("Shutdown complete. Goodbye!"); - Platform.exit(); - // This is needed, because OkHttp?! seems to block the shutdown with its writer threads. They are not daemon threads :( - System.exit(0); + Platform.runLater(() -> { + primaryStage.close(); + shutdownInfo.close(); + Platform.exit(); + // This is needed, because OkHttp?! seems to block the shutdown with its writer threads. They are not daemon threads :( + System.exit(0); + }); } catch (IOException e1) { Platform.runLater(() -> { Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); From 4d7409f443fc7e4541c6391d49b7758bd7326e77 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 14 Dec 2018 20:25:57 +0100 Subject: [PATCH 210/231] Implement follow/unfollow and login with cookies --- .../streamate/StreamateFollowedService.java | 4 +- .../streamate/StreamateUpdateService.java | 2 +- .../ctbrec/sites/streamate/Streamate.java | 5 +- .../sites/streamate/StreamateHttpClient.java | 82 +++++++++---------- .../sites/streamate/StreamateModel.java | 61 ++++++++++++-- 5 files changed, 98 insertions(+), 56 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java index 6aab7d5b..2c78e1d7 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java @@ -47,7 +47,7 @@ public class StreamateFollowedService extends PaginatedScheduledService { public List call() throws IOException, SAXException, ParserConfigurationException, XPathExpressionException { httpClient.login(); String saKey = httpClient.getSaKey(); - String userId = httpClient.getUserId(); + Long userId = httpClient.getUserId(); String _url = url + "&page_number=" + page + "&results_per_page=" + MODELS_PER_PAGE + "&sakey=" + saKey + "&userid=" + userId; LOG.debug("Fetching page {}", _url); Request request = new Request.Builder() @@ -68,7 +68,7 @@ public class StreamateFollowedService extends PaginatedScheduledService { JSONObject p = performers.getJSONObject(i); String nickname = p.getString("Nickname"); StreamateModel model = (StreamateModel) streamate.createModel(nickname); - model.setId(Long.toString(p.getLong("PerformerId"))); + model.setId(p.getLong("PerformerId")); model.setPreview("https://m1.nsimg.net/biopic/320x240/" + model.getId()); boolean online = p.optString("LiveStatus").equals("live"); model.setOnline(online); diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java index 37ea045c..083a2008 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateUpdateService.java @@ -61,7 +61,7 @@ public class StreamateUpdateService extends PaginatedScheduledService { JSONObject p = performers.getJSONObject(i); String nickname = p.getString("nickname"); StreamateModel model = (StreamateModel) streamate.createModel(nickname); - model.setId(Long.toString(p.getLong("id"))); + model.setId(p.getLong("id")); model.setPreview(p.getString("thumbnail")); model.setOnline(p.optBoolean("online")); // TODO figure out, what all the states mean diff --git a/common/src/main/java/ctbrec/sites/streamate/Streamate.java b/common/src/main/java/ctbrec/sites/streamate/Streamate.java index 390767f0..a86eb91b 100644 --- a/common/src/main/java/ctbrec/sites/streamate/Streamate.java +++ b/common/src/main/java/ctbrec/sites/streamate/Streamate.java @@ -125,7 +125,7 @@ public class Streamate extends AbstractSite { @Override public boolean supportsFollow() { - return false; + return true; } @Override @@ -151,7 +151,6 @@ public class Streamate extends AbstractSite { if (response.isSuccessful()) { String body = response.body().string(); JSONObject json = new JSONObject(body); - LOG.debug(json.toString(2)); if (json.optString("status").equals("SM_OK")) { List models = new ArrayList<>(); JSONObject results = json.getJSONObject("results"); @@ -159,7 +158,7 @@ public class Streamate extends AbstractSite { for (int i = 0; i < nickname.length(); i++) { JSONObject result = nickname.getJSONObject(i); StreamateModel model = (StreamateModel) createModel(result.getString("nickname")); - model.setId(result.getString("performerId")); + model.setId(Long.parseLong(result.getString("performerId"))); String thumb = result.getString("thumbnail"); if (thumb != null) { model.setPreview(thumb); diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java b/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java index d1242dac..b901b385 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java @@ -2,6 +2,7 @@ package ctbrec.sites.streamate; import java.io.IOException; import java.util.Collections; +import java.util.NoSuchElementException; import org.json.JSONObject; import org.slf4j.Logger; @@ -20,7 +21,7 @@ public class StreamateHttpClient extends HttpClient { private static final transient Logger LOG = LoggerFactory.getLogger(StreamateHttpClient.class); - private String userId = ""; + private Long userId; private String saKey = ""; public StreamateHttpClient() { @@ -33,6 +34,14 @@ public class StreamateHttpClient extends HttpClient { .value("1") .build(); getCookieJar().saveFromResponse(HttpUrl.parse(Streamate.BASE_URL), Collections.singletonList(searchCookie)); + + // try to load sakey from cookie + try { + Cookie cookie = getCookieJar().getCookie(HttpUrl.parse("https://www.streamate.com"), "sakey"); + saKey = cookie.value(); + } catch (NoSuchElementException e) { + // ignore + } } @Override @@ -65,14 +74,12 @@ public class StreamateHttpClient extends HttpClient { .build(); try (Response response = client.newCall(login).execute()) { String content = response.body().string(); - //LOG.debug(content); if(response.isSuccessful()) { JSONObject json = new JSONObject(content); - LOG.debug(json.toString()); loggedIn = json.has("sakey"); saKey = json.optString("sakey"); JSONObject account = json.getJSONObject("account"); - userId = Long.toString(account.getLong("userid")); + userId = account.getLong("userid"); } else { throw new IOException("Login failed: " + response.code() + " " + response.message()); } @@ -83,53 +90,40 @@ public class StreamateHttpClient extends HttpClient { } /** - * Check, if the login worked - * @throws IOException + * Check, if the login worked by loading the favorites */ - public boolean checkLoginSuccess() throws IOException { - //https://www.streamate.com/api/search/v1/favorites?host=streamate.com&domain=streamate.com&page_number=1&results_per_page=48&sakey=62857cfd1908cd28 - return false; - // String modelName = getAnyModelName(); - // // we request the roomData of a random model, because it contains - // // user data, if the user is logged in, which we can use to verify, that the login worked - // String url = Streamate.BASE_URL + "/tools/amf.php"; - // RequestBody body = new FormBody.Builder() - // .add("method", "getRoomData") - // .add("args[]", modelName) - // .add("args[]", "false") - // //.add("method", "ping") // TODO alternative request, but - // //.add("args[]", ) // where to get the userId - // .build(); - // 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", Streamate.BASE_URL) - // .addHeader("X-Requested-With", "XMLHttpRequest") - // .post(body) - // .build(); - // try(Response response = execute(request)) { - // if(response.isSuccessful()) { - // JSONObject json = new JSONObject(response.body().string()); - // if(json.optString("status").equals("success")) { - // JSONObject userData = json.getJSONObject("userData"); - // userId = userData.optInt("userId"); - // return userId > 0; - // } else { - // throw new IOException("Request was not successful: " + json.toString(2)); - // } - // } else { - // throw new HttpException(response.code(), response.message()); - // } - // } + public boolean checkLoginSuccess() { + String url = Streamate.BASE_URL + "/api/search/v1/favorites?host=streamate.com&domain=streamate.com"; + url = url + "&page_number=1&results_per_page=48&sakey=" + saKey + "&userid=" + userId; + Request request = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "application/json, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", Streamate.BASE_URL) + .build(); + try(Response response = execute(request)) { + if (response.isSuccessful()) { + String content = response.body().string(); + JSONObject json = new JSONObject(content); + if(json.optString("status").equals("SM_OK")) { + return true; + } else { + return false; + } + } else { + return false; + } + } catch(Exception e) { + return false; + } } public String getSaKey() { return saKey; } - public String getUserId() { + public Long getUserId() { return userId; } } diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java index 66e58376..4ea1b54b 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java @@ -15,12 +15,16 @@ import org.slf4j.LoggerFactory; import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.io.HttpException; import ctbrec.recorder.download.StreamSource; +import okhttp3.MediaType; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; public class StreamateModel extends AbstractModel { @@ -30,7 +34,7 @@ public class StreamateModel extends AbstractModel { private boolean online = false; private List streamSources = new ArrayList<>(); private int[] resolution; - private String id; + private Long id; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { @@ -180,19 +184,64 @@ public class StreamateModel extends AbstractModel { @Override public boolean follow() throws IOException { - return false; + return follow(true); } @Override public boolean unfollow() throws IOException { - return false; + return follow(false); } - public String getId() { + private boolean follow(boolean follow) throws IOException { + StreamateHttpClient client = (StreamateHttpClient) getSite().getHttpClient(); + client.login(); + String saKey = client.getSaKey(); + Long userId = client.getUserId(); + + JSONObject requestParams = new JSONObject(); + requestParams.put("sakey", saKey); + requestParams.put("userid", userId); + requestParams.put("pid", id); + requestParams.put("domain", "streamate.com"); + requestParams.put("fav", follow); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), requestParams.toString()); + + String url = site.getBaseUrl() + "/ajax/fav-notify.php?userid="+userId+"&sakey="+saKey+"&pid="+id+"&fav="+follow+"&domain=streamate.com"; + Request request = new Request.Builder() + .url(url) + .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .addHeader("Accept", "application/json, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", getSite().getBaseUrl()) + .post(body) + .build(); + try(Response response = getSite().getHttpClient().execute(request)) { + String content = response.body().string(); + if (response.isSuccessful()) { + JSONObject json = new JSONObject(content); + return json.optBoolean("success"); + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + public Long getId() { return id; } - public void setId(String id) { + public void setId(Long id) { this.id = id; } -} + + @Override + public void readSiteSpecificData(JsonReader reader) throws IOException { + reader.nextName(); + id = reader.nextLong(); + } + + @Override + public void writeSiteSpecificData(JsonWriter writer) throws IOException { + writer.name("id").value(id); + } +} \ No newline at end of file From bd719eac08b0a1b474b30603aaa720df0fd09dd8 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 14 Dec 2018 23:35:44 +0100 Subject: [PATCH 211/231] Remove direct refences to chaturbate in TipDialog This dialog is used for other sites, too. So we have to use the site object to get the name and the affiliate link --- client/src/main/java/ctbrec/ui/TipDialog.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/TipDialog.java b/client/src/main/java/ctbrec/ui/TipDialog.java index 2b4dfcf0..8aaefb93 100644 --- a/client/src/main/java/ctbrec/ui/TipDialog.java +++ b/client/src/main/java/ctbrec/ui/TipDialog.java @@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory; import ctbrec.Model; import ctbrec.sites.Site; -import ctbrec.sites.chaturbate.Chaturbate; import javafx.application.Platform; import javafx.concurrent.Task; import javafx.scene.control.Alert; @@ -48,7 +47,7 @@ public class TipDialog extends TextInputDialog { int tokens = get(); Platform.runLater(() -> { if (tokens <= 0) { - String msg = "Do you want to buy tokens now?\n\nIf you agree, Chaturbate will open in a browser. " + String msg = "Do you want to buy tokens now?\n\nIf you agree, "+site.getName()+" will open in a browser. " + "The used address is an affiliate link, which supports me, but doesn't cost you anything more."; Alert buyTokens = new AutosizeAlert(Alert.AlertType.CONFIRMATION, msg, ButtonType.NO, ButtonType.YES); buyTokens.setTitle("No tokens"); @@ -56,7 +55,7 @@ public class TipDialog extends TextInputDialog { buyTokens.showAndWait(); TipDialog.this.close(); if(buyTokens.getResult() == ButtonType.YES) { - DesktopIntegration.open(Chaturbate.AFFILIATE_LINK); + DesktopIntegration.open(site.getAffiliateLink()); } } else { getEditor().setDisable(false); From b83235a32f2c6b3aa386a6edf8fb210ce13be543 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 14 Dec 2018 23:36:00 +0100 Subject: [PATCH 212/231] Log error, if sending tip failed --- client/src/main/java/ctbrec/ui/ThumbOverviewTab.java | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index aa06a2b7..6ce0cdde 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -479,6 +479,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { event.put("amount", tokens); EventBusHolder.BUS.post(event); } catch (Exception e1) { + LOG.error("An error occured while sending tip", e1); showError("Couldn't send tip", "An error occured while sending tip:", e1); } } else { From 1ce9a111a98d7a906b0d87476e9de013af822fd0 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 14 Dec 2018 23:37:55 +0100 Subject: [PATCH 213/231] Add tipping for Streamate Tipping does not work, yet. The server returns success: false. I don't know, what the parameters have to look like --- .../streamate/StreamateFollowedService.java | 3 +- .../sites/streamate/StreamateHttpClient.java | 17 ++- .../sites/streamate/StreamateModel.java | 136 ++++++++++++++---- 3 files changed, 124 insertions(+), 32 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java index 2c78e1d7..d78b04c1 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateFollowedService.java @@ -47,8 +47,7 @@ public class StreamateFollowedService extends PaginatedScheduledService { public List call() throws IOException, SAXException, ParserConfigurationException, XPathExpressionException { httpClient.login(); String saKey = httpClient.getSaKey(); - Long userId = httpClient.getUserId(); - String _url = url + "&page_number=" + page + "&results_per_page=" + MODELS_PER_PAGE + "&sakey=" + saKey + "&userid=" + userId; + String _url = url + "&page_number=" + page + "&results_per_page=" + MODELS_PER_PAGE + "&sakey=" + saKey; LOG.debug("Fetching page {}", _url); Request request = new Request.Builder() .url(_url) diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java b/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java index b901b385..3f056e73 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateHttpClient.java @@ -23,6 +23,7 @@ public class StreamateHttpClient extends HttpClient { private Long userId; private String saKey = ""; + private String userNickname = ""; public StreamateHttpClient() { super("streamate"); @@ -57,6 +58,11 @@ public class StreamateHttpClient extends HttpClient { return true; } + loggedIn = loginWithoutCookies(); + return loggedIn; + } + + private synchronized boolean loginWithoutCookies() throws IOException { JSONObject loginRequest = new JSONObject(); loginRequest.put("email", Config.getInstance().getSettings().streamateUsername); loginRequest.put("password", Config.getInstance().getSettings().streamatePassword); @@ -76,10 +82,12 @@ public class StreamateHttpClient extends HttpClient { String content = response.body().string(); if(response.isSuccessful()) { JSONObject json = new JSONObject(content); + LOG.debug(json.toString(2)); loggedIn = json.has("sakey"); saKey = json.optString("sakey"); JSONObject account = json.getJSONObject("account"); userId = account.getLong("userid"); + userNickname = account.getString("nickname"); } else { throw new IOException("Login failed: " + response.code() + " " + response.message()); } @@ -123,7 +131,14 @@ public class StreamateHttpClient extends HttpClient { return saKey; } - public Long getUserId() { + public Long getUserId() throws IOException { + if(userId == null) { + loginWithoutCookies(); + } return userId; } + + public String getUserNickname() { + return userNickname; + } } diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java index 4ea1b54b..c4e04670 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java @@ -22,10 +22,12 @@ import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.io.HttpException; import ctbrec.recorder.download.StreamSource; +import okhttp3.FormBody; import okhttp3.MediaType; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; +import okio.Buffer; public class StreamateModel extends AbstractModel { @@ -128,35 +130,111 @@ public class StreamateModel extends AbstractModel { @Override public void receiveTip(int tokens) throws IOException { - // String url = Streamate.BASE_URL + "/chat-ajax-amf-service?" + System.currentTimeMillis(); - // int userId = ((StreamateHttpClient)site.getHttpClient()).getUserId(); - // RequestBody body = new FormBody.Builder() - // .add("method", "tipModel") - // .add("args[]", getName()) - // .add("args[]", Integer.toString(tokens)) - // .add("args[]", Integer.toString(userId)) - // .add("args[3]", "") - // .build(); - // 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", Streamate.BASE_URL + '/' + getName()) - // .addHeader("X-Requested-With", "XMLHttpRequest") - // .post(body) - // .build(); - // try(Response response = site.getHttpClient().execute(request)) { - // if(response.isSuccessful()) { - // JSONObject json = new JSONObject(response.body().string()); - // if(!json.optString("status").equals("success")) { - // LOG.error("Sending tip failed {}", json.toString(2)); - // throw new IOException("Sending tip failed"); - // } - // } else { - // throw new IOException(response.code() + ' ' + response.message()); - // } - // } + /* + Mt._giveGoldAjax = function(e, t) { + var n = _t.getState(), + a = n.nickname, + o = n.id, + i = Ds.getState(), + r = i.userStreamId, + s = i.sakey, + l = i.userId, + c = i.nickname, + u = ""; + switch (Ot.getState().streamType) { + case z.STREAM_TYPE_PRIVATE: + case z.STREAM_TYPE_BLOCK: + u = "premium"; + break; + case z.STREAM_TYPE_EXCLUSIVE: + case z.STREAM_TYPE_BLOCK_EXCLUSIVE: + u = "exclusive" + } + if (!l) return ae.a.reject("no userId!"); + var d = { + amt: e, + isprepopulated: t, + modelname: a, + nickname: c, + performernickname: a, + sakey: s, + session: u, + smid: o, + streamid: r, + userid: l, + username: c + }, + p = de.a.getBaseUrl() + "/api/v1/givegold/"; + return de.a.postPromise(p, d, "json") + }, + */ + + StreamateHttpClient client = (StreamateHttpClient) getSite().getHttpClient(); + client.login(); + String saKey = client.getSaKey(); + Long userId = client.getUserId(); + String nickname = client.getUserNickname(); + + String url = "https://hybridclient.naiadsystems.com/api/v1/givegold/"; // this returns 404 at the moment. not sure if it's the wrong server, or if this is not used anymore + RequestBody body = new FormBody.Builder() + .add("amt", Integer.toString(tokens)) // amount + .add("isprepopulated", "1") // ? + .add("modelname", getName()) // model's name + .add("nickname", nickname) // user's nickname + .add("performernickname", getName()) // model's name + .add("sakey", saKey) // sakey from login + .add("session", "") // is related to gold an private shows, for normal tips keep it empty + .add("smid", Long.toString(getId())) // model id + .add("streamid", getStreamId()) // id of the current stream + .add("userid", Long.toString(userId)) // user's id + .add("username", nickname) // user's nickname + .build(); + Buffer b = new Buffer(); + body.writeTo(b); + LOG.debug("tip params {}", b.readUtf8()); + 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", Streamate.BASE_URL + '/' + getName()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .post(body) + .build(); + try(Response response = site.getHttpClient().execute(request)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + LOG.debug(json.toString(2)); + if(!json.optString("status").equals("success")) { + LOG.error("Sending tip failed {}", json.toString(2)); + throw new IOException("Sending tip failed"); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private String getStreamId() throws IOException { + String url = "https://hybridclient.naiadsystems.com/api/v1/config/?name=" + getName() + + "&sabasic=&sakey=&sk=www.streamate.com&userid=0&version=6.3.17&ajax=1"; + 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", Streamate.BASE_URL + '/' + getName()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + try(Response response = site.getHttpClient().execute(request)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + JSONObject stream = json.getJSONObject("stream"); + return stream.getString("streamId"); + } else { + throw new HttpException(response.code(), response.message()); + } + } } @Override From b2d1d41abc6c14f445f831178c421b15dfebf65d Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 14 Dec 2018 23:53:10 +0100 Subject: [PATCH 214/231] Remove ordering by sequence This was used for Chaturbate, because the filename format was known. With several camsites the filename format can differ and this is not a good solution anymore. Instead we now just sort filename. To make sure, the files have the right order, HlsDownload now creates a prefix for each segment. --- .../java/ctbrec/recorder/PlaylistGenerator.java | 16 +--------------- .../ctbrec/recorder/download/HlsDownload.java | 15 +++++++++++---- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java b/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java index a4180765..1c02a614 100644 --- a/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java +++ b/common/src/main/java/ctbrec/recorder/PlaylistGenerator.java @@ -9,8 +9,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,10 +46,8 @@ public class PlaylistGenerator { Arrays.sort(files, (f1, f2) -> { String n1 = f1.getName(); - int seq1 = getSequence(n1); String n2 = f2.getName(); - int seq2 = getSequence(n2); - return seq1 - seq2; + return n1.compareTo(n2); }); // create a track containing all files @@ -102,16 +98,6 @@ public class PlaylistGenerator { return output; } - private int getSequence(String filename) { - filename = filename.substring(0, filename.lastIndexOf('.')); // cut off file suffix - Matcher matcher = Pattern.compile(".*?(\\d+)").matcher(filename); - if(matcher.matches()) { - return Integer.parseInt(matcher.group(1)); - } else { - return -1; - } - } - private void updateProgressListeners(double percentage) { int p = (int) (percentage*100); if(p > lastPercentage) { diff --git a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java index 94aa5a4d..b99f3be7 100644 --- a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java @@ -13,6 +13,8 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; +import java.text.DecimalFormat; +import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.Date; @@ -39,6 +41,9 @@ public class HlsDownload extends AbstractHlsDownload { protected Path downloadDir; + private int segmentCounter = 1; + private NumberFormat nf = new DecimalFormat("000000"); + public HlsDownload(HttpClient client) { super(client); } @@ -78,7 +83,8 @@ public class HlsDownload extends AbstractHlsDownload { for (int i = nextSegment; i < lsp.seq; i++) { URL segmentUrl = new URL(first.replaceAll(Integer.toString(seq), Integer.toString(i))); LOG.debug("Reloading segment {} for model {}", i, model.getName()); - downloadThreadPool.submit(new SegmentDownload(segmentUrl, downloadDir, client)); + String prefix = nf.format(segmentCounter++); + downloadThreadPool.submit(new SegmentDownload(segmentUrl, downloadDir, client, prefix)); } // TODO switch to a lower bitrate/resolution ?!? } @@ -88,7 +94,8 @@ public class HlsDownload extends AbstractHlsDownload { skip--; } else { URL segmentUrl = new URL(segment); - downloadThreadPool.submit(new SegmentDownload(segmentUrl, downloadDir, client)); + String prefix = nf.format(segmentCounter++); + downloadThreadPool.submit(new SegmentDownload(segmentUrl, downloadDir, client, prefix)); //new SegmentDownload(segment, downloadDir).call(); } } @@ -150,11 +157,11 @@ public class HlsDownload extends AbstractHlsDownload { private Path file; private HttpClient client; - public SegmentDownload(URL url, Path dir, HttpClient client) { + public SegmentDownload(URL url, Path dir, HttpClient client, String prefix) { this.url = url; this.client = client; File path = new File(url.getPath()); - file = FileSystems.getDefault().getPath(dir.toString(), path.getName()); + file = FileSystems.getDefault().getPath(dir.toString(), prefix + '_' + path.getName()); } @Override From e1c16cda9b290febf0de9dc2750e22d3c2c68ab8 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 14 Dec 2018 23:53:19 +0100 Subject: [PATCH 215/231] Add Streamate --- server/src/main/java/ctbrec/recorder/server/HttpServer.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index ee57211d..4262c6ff 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -33,6 +33,7 @@ import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.mfc.MyFreeCams; +import ctbrec.sites.streamate.Streamate; public class HttpServer { @@ -82,6 +83,7 @@ public class HttpServer { sites.add(new Camsoda()); sites.add(new Cam4()); sites.add(new BongaCams()); + sites.add(new Streamate()); } private void addShutdownHook() { From 465e417b6c2695a5a4db8ce8f8eaa4ac5a982b27 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 15 Dec 2018 13:18:15 +0100 Subject: [PATCH 216/231] Ignore models without username in JSON response Fix for #120 There are objects in the JSON response, which don't look like regular model entries. If an object doesn't have a username, ignore it. --- .../java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java index e7845956..beb2f07c 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsUpdateService.java @@ -56,7 +56,10 @@ public class BongaCamsUpdateService extends PaginatedScheduledService { JSONArray _models = json.getJSONArray("models"); for (int i = 0; i < _models.length(); i++) { JSONObject m = _models.getJSONObject(i); - String name = m.getString("username"); + String name = m.optString("username"); + if(name.isEmpty()) { + continue; + } BongaCamsModel model = (BongaCamsModel) bongaCams.createModel(name); boolean away = m.optBoolean("is_away"); boolean online = m.optBoolean("online"); From d09aad1bf630ed8acf7f1e5a18863c0c110fb2af Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 15 Dec 2018 15:55:17 +0100 Subject: [PATCH 217/231] Move stream preview to its own control Move stream preview to its own control, so that it can be used in the ThumbCell, too --- .../java/ctbrec/ui/PreviewPopupHandler.java | 168 +---------------- .../ctbrec/ui/controls/StreamPreview.java | 178 ++++++++++++++++++ 2 files changed, 188 insertions(+), 158 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/controls/StreamPreview.java diff --git a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java index e6ffc72a..16de2224 100644 --- a/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java +++ b/client/src/main/java/ctbrec/ui/PreviewPopupHandler.java @@ -1,39 +1,23 @@ package ctbrec.ui; -import java.io.InterruptedIOException; -import java.util.Collections; -import java.util.List; import java.util.Objects; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ctbrec.Config; -import ctbrec.io.HttpException; -import ctbrec.recorder.download.StreamSource; +import ctbrec.ui.controls.StreamPreview; import javafx.application.Platform; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.scene.Node; -import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; import javafx.scene.control.TableView; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; -import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; -import javafx.scene.media.Media; -import javafx.scene.media.MediaPlayer; -import javafx.scene.media.MediaView; import javafx.stage.Popup; public class PreviewPopupHandler implements EventHandler { @@ -44,53 +28,24 @@ public class PreviewPopupHandler implements EventHandler { private long timeForPopupClose = 400; private Popup popup = new Popup(); private Node parent; - private ImageView preview = new ImageView(); - private MediaView videoPreview; - private MediaPlayer videoPlayer; - private Media video; + private StreamPreview streamPreview; private JavaFxModel model; private volatile long openCountdown = -1; private volatile long closeCountdown = -1; private volatile long lastModelChange = -1; private volatile boolean changeModel = false; - private ExecutorService executor = Executors.newSingleThreadExecutor(); - private Future future; - private ProgressIndicator progressIndicator; - private StackPane pane; public PreviewPopupHandler(Node parent) { this.parent = parent; - videoPreview = new MediaView(); - videoPreview.setFitWidth(Config.getInstance().getSettings().thumbWidth); - videoPreview.setFitHeight(videoPreview.getFitWidth() * 9 / 16); - videoPreview.setPreserveRatio(true); - StackPane.setMargin(videoPreview, new Insets(5)); - - preview.setFitWidth(Config.getInstance().getSettings().thumbWidth); - preview.setPreserveRatio(true); - preview.setSmooth(true); - preview.setStyle("-fx-background-radius: 10px, 10px, 10px, 10px;"); - preview.visibleProperty().bind(videoPreview.visibleProperty().not()); - StackPane.setMargin(preview, new Insets(5)); - - progressIndicator = new ProgressIndicator(); - progressIndicator.setVisible(false); - progressIndicator.prefWidthProperty().bind(videoPreview.fitWidthProperty()); - - Region veil = new Region(); - veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.8)"); - veil.visibleProperty().bind(progressIndicator.visibleProperty()); - StackPane.setMargin(veil, new Insets(5)); - - pane = new StackPane(); - pane.getChildren().addAll(preview, videoPreview, veil, progressIndicator); - pane.setStyle("-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-base;"+ + streamPreview = new StreamPreview(); + streamPreview.setStyle("-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-base;"+ "-fx-background-insets: 0 0 -1 0, 0, 1, 2;" + "-fx-background-radius: 10px, 10px, 10px, 10px;" + "-fx-padding: 1;" + "-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.8), 20, 0, 0, 0);"); - popup.getContent().add(pane); + popup.getContent().add(streamPreview); + StackPane.setMargin(streamPreview, new Insets(5)); createTimerThread(); } @@ -121,8 +76,7 @@ public class PreviewPopupHandler implements EventHandler { if(modelChanged) { lastModelChange = System.currentTimeMillis(); changeModel = true; - future.cancel(true); - progressIndicator.setVisible(true); + streamPreview.stop(); } } else { openCountdown = timeForPopupOpen; @@ -173,121 +127,19 @@ public class PreviewPopupHandler implements EventHandler { } private void startStream(JavaFxModel model) { - if(future != null && !future.isDone()) { - future.cancel(true); - } - future = executor.submit(() -> { - try { - Platform.runLater(() -> { - progressIndicator.setVisible(true); - popup.show(parent.getScene().getWindow()); - }); - List sources = model.getStreamSources(); - Collections.sort(sources); - StreamSource best = sources.get(0); - checkInterrupt(); - LOG.debug("Preview url for {} is {}", model.getName(), best.getMediaPlaylistUrl()); - video = new Media(best.getMediaPlaylistUrl()); - if(videoPlayer != null) { - videoPlayer.dispose(); - } - videoPlayer = new MediaPlayer(video); - videoPlayer.setMute(true); - checkInterrupt(); - videoPlayer.setOnReady(() -> { - if(!future.isCancelled()) { - Platform.runLater(() -> { - double aspect = (double)video.getWidth() / video.getHeight(); - double w = Config.getInstance().getSettings().thumbWidth; - double h = w / aspect; - resize(w, h); - progressIndicator.setVisible(false); - videoPreview.setVisible(true); - videoPreview.setMediaPlayer(videoPlayer); - videoPlayer.play(); - }); - } - }); - videoPlayer.setOnError(() -> onError(videoPlayer)); - } catch (IllegalStateException e) { - if(e.getMessage().equals("Stream url unknown")) { - // fine hls url for mfc not known yet - } else { - LOG.warn("Couldn't start preview video: {}", e.getMessage()); - } - showTestImage(); - } catch (HttpException e) { - if(e.getResponseCode() != 404) { - LOG.warn("Couldn't start preview video: {}", e.getMessage()); - } - showTestImage(); - } catch (InterruptedException | InterruptedIOException e) { - // future has been canceled, that's fine - } catch (ExecutionException e) { - if(e.getCause() instanceof InterruptedException || e.getCause() instanceof InterruptedIOException) { - // future has been canceled, that's fine - } else { - LOG.warn("Couldn't start preview video: {}", e.getMessage()); - showTestImage(); - } - } catch (Exception e) { - LOG.warn("Couldn't start preview video: {}", e.getMessage()); - showTestImage(); - } - }); - } - - private void onError(MediaPlayer videoPlayer) { - LOG.error("Error while starting preview stream", videoPlayer.getError()); - if(videoPlayer.getError().getCause() != null) { - LOG.error("Error while starting preview stream root cause:", videoPlayer.getError().getCause()); - } - videoPlayer.dispose(); Platform.runLater(() -> { - showTestImage(); + streamPreview.startStream(model); + popup.show(parent.getScene().getWindow()); }); - } - private void resize(double w, double h) { - preview.setFitWidth(w); - preview.setFitHeight(h); - videoPreview.setFitWidth(w); - videoPreview.setFitHeight(h); - pane.setPrefSize(w, h); - popup.setWidth(w); - popup.setHeight(h); - } - - private void checkInterrupt() throws InterruptedException { - if(Thread.interrupted()) { - throw new InterruptedException(); - } - } - - private void showTestImage() { - Platform.runLater(() -> { - videoPreview.setVisible(false); - Image img = new Image(getClass().getResource("/image_not_found.png").toString(), true); - preview.setImage(img); - double aspect = img.getWidth() / img.getHeight(); - double w = Config.getInstance().getSettings().thumbWidth; - double h = w / aspect; - resize(w, h); - progressIndicator.setVisible(false); - }); } private void hidePopup() { - if(future != null && !future.isDone()) { - future.cancel(true); - } Platform.runLater(() -> { popup.setX(-1000); popup.setY(-1000); popup.hide(); - if(videoPlayer != null) { - videoPlayer.dispose(); - } + streamPreview.stop(); }); } diff --git a/client/src/main/java/ctbrec/ui/controls/StreamPreview.java b/client/src/main/java/ctbrec/ui/controls/StreamPreview.java new file mode 100644 index 00000000..258ec42b --- /dev/null +++ b/client/src/main/java/ctbrec/ui/controls/StreamPreview.java @@ -0,0 +1,178 @@ +package ctbrec.ui.controls; + +import java.io.InterruptedIOException; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HttpException; +import ctbrec.recorder.download.StreamSource; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; +import javafx.scene.media.MediaView; + +public class StreamPreview extends StackPane { + private static final transient Logger LOG = LoggerFactory.getLogger(StreamPreview.class); + + private ImageView preview = new ImageView(); + private MediaView videoPreview; + private MediaPlayer videoPlayer; + private Media video; + private ProgressIndicator progressIndicator; + private ExecutorService executor = Executors.newSingleThreadExecutor(); + private Future future; + + public StreamPreview() { + videoPreview = new MediaView(); + videoPreview.setFitWidth(Config.getInstance().getSettings().thumbWidth); + videoPreview.setFitHeight(videoPreview.getFitWidth() * 9 / 16); + videoPreview.setPreserveRatio(true); + StackPane.setMargin(videoPreview, new Insets(5)); + + preview.setFitWidth(Config.getInstance().getSettings().thumbWidth); + preview.setPreserveRatio(true); + preview.setSmooth(true); + preview.setStyle("-fx-background-radius: 10px, 10px, 10px, 10px;"); + preview.visibleProperty().bind(videoPreview.visibleProperty().not()); + StackPane.setMargin(preview, new Insets(5)); + + progressIndicator = new ProgressIndicator(); + progressIndicator.setVisible(false); + progressIndicator.prefWidthProperty().bind(videoPreview.fitWidthProperty()); + + Region veil = new Region(); + veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.8)"); + veil.visibleProperty().bind(progressIndicator.visibleProperty()); + StackPane.setMargin(veil, new Insets(5)); + + getChildren().addAll(preview, videoPreview, veil, progressIndicator); + } + + public void startStream(Model model) { + if(future != null && !future.isDone()) { + future.cancel(true); + } + future = executor.submit(() -> { + try { + Platform.runLater(() -> { + progressIndicator.setVisible(true); + }); + List sources = model.getStreamSources(); + Collections.sort(sources); + StreamSource best = sources.get(0); + checkInterrupt(); + LOG.debug("Preview url for {} is {}", model.getName(), best.getMediaPlaylistUrl()); + video = new Media(best.getMediaPlaylistUrl()); + if(videoPlayer != null) { + videoPlayer.dispose(); + } + videoPlayer = new MediaPlayer(video); + videoPlayer.setMute(true); + checkInterrupt(); + videoPlayer.setOnReady(() -> { + if(!future.isCancelled()) { + Platform.runLater(() -> { + double aspect = (double)video.getWidth() / video.getHeight(); + double w = Config.getInstance().getSettings().thumbWidth; + double h = w / aspect; + resizeToFitContent(w, h); + progressIndicator.setVisible(false); + videoPreview.setVisible(true); + videoPreview.setMediaPlayer(videoPlayer); + videoPlayer.play(); + }); + } + }); + videoPlayer.setOnError(() -> onError(videoPlayer)); + } catch (IllegalStateException e) { + if(e.getMessage().equals("Stream url unknown")) { + // fine hls url for mfc not known yet + } else { + LOG.warn("Couldn't start preview video: {}", e.getMessage()); + } + showTestImage(); + } catch (HttpException e) { + if(e.getResponseCode() != 404) { + LOG.warn("Couldn't start preview video: {}", e.getMessage()); + } + showTestImage(); + } catch (InterruptedException | InterruptedIOException e) { + // future has been canceled, that's fine + } catch (ExecutionException e) { + if(e.getCause() instanceof InterruptedException || e.getCause() instanceof InterruptedIOException) { + // future has been canceled, that's fine + } else { + LOG.warn("Couldn't start preview video: {}", e.getMessage()); + showTestImage(); + } + } catch (Exception e) { + LOG.warn("Couldn't start preview video: {}", e.getMessage()); + showTestImage(); + } + }); + } + + private void resizeToFitContent(double w, double h) { + setPrefSize(w, h); + preview.setFitWidth(w); + preview.setFitHeight(h); + videoPreview.setFitWidth(w); + videoPreview.setFitHeight(h); + } + + public void stop() { + if(future != null && !future.isDone()) { + future.cancel(true); + } + Platform.runLater(() -> { + if(videoPlayer != null) { + videoPlayer.dispose(); + } + }); + } + + private void onError(MediaPlayer videoPlayer) { + LOG.error("Error while starting preview stream", videoPlayer.getError()); + if(videoPlayer.getError().getCause() != null) { + LOG.error("Error while starting preview stream root cause:", videoPlayer.getError().getCause()); + } + videoPlayer.dispose(); + Platform.runLater(() -> { + showTestImage(); + }); + } + + private void showTestImage() { + Platform.runLater(() -> { + videoPreview.setVisible(false); + Image img = new Image(getClass().getResource("/image_not_found.png").toString(), true); + preview.setImage(img); + double aspect = img.getWidth() / img.getHeight(); + double w = Config.getInstance().getSettings().thumbWidth; + double h = w / aspect; + resizeToFitContent(w, h); + progressIndicator.setVisible(false); + }); + } + + private void checkInterrupt() throws InterruptedException { + if(Thread.interrupted()) { + throw new InterruptedException(); + } + } +} From f6313067682ef2815f61624807100c73949d1cfa Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 15 Dec 2018 20:33:57 +0100 Subject: [PATCH 218/231] Tweak video preview in thumb cell --- client/src/main/java/ctbrec/ui/ThumbCell.java | 66 ++++++++++++++++++- .../ctbrec/ui/controls/StreamPreview.java | 36 ++++++---- 2 files changed, 87 insertions(+), 15 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index 898f3516..cb67fa21 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -22,6 +22,7 @@ import ctbrec.Model; import ctbrec.io.HttpException; import ctbrec.recorder.Recorder; import ctbrec.ui.action.PlayAction; +import ctbrec.ui.controls.StreamPreview; import javafx.animation.FadeTransition; import javafx.animation.FillTransition; import javafx.animation.ParallelTransition; @@ -43,6 +44,7 @@ import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.scene.shape.Circle; +import javafx.scene.shape.Polygon; import javafx.scene.shape.Rectangle; import javafx.scene.shape.Shape; import javafx.scene.text.Font; @@ -58,6 +60,7 @@ public class ThumbCell extends StackPane { private static final Duration ANIMATION_DURATION = new Duration(250); private Model model; + private StreamPreview streamPreview; private ImageView iv; private Rectangle resolutionBackground; private final Paint resolutionOnlineColor = new Color(0.22, 0.8, 0.29, 1); @@ -87,8 +90,10 @@ public class ThumbCell extends StackPane { .expireAfterAccess(4, TimeUnit.HOURS) .maximumSize(1000) .build(); + private ThumbOverviewTab parent; public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder) { + this.parent = parent; this.thumbCellList = parent.grid.getChildren(); this.model = model; this.recorder = recorder; @@ -96,6 +101,11 @@ public class ThumbCell extends StackPane { model.setSuspended(recorder.isSuspended(model)); this.setStyle("-fx-background-color: -fx-base"); + streamPreview = new StreamPreview(); + streamPreview.prefWidthProperty().bind(widthProperty()); + streamPreview.prefHeightProperty().bind(heightProperty()); + getChildren().add(streamPreview); + iv = new ImageView(); iv.setSmooth(true); iv.setPreserveRatio(true); @@ -164,8 +174,10 @@ public class ThumbCell extends StackPane { StackPane.setAlignment(pausedIndicator, Pos.TOP_LEFT); getChildren().add(pausedIndicator); + getChildren().add(createPreviewTrigger()); + selectionOverlay = new Rectangle(); - selectionOverlay.setOpacity(0); + selectionOverlay.visibleProperty().bind(selectionProperty); selectionOverlay.widthProperty().bind(widthProperty()); selectionOverlay.heightProperty().bind(heightProperty()); StackPane.setAlignment(selectionOverlay, Pos.TOP_LEFT); @@ -197,6 +209,50 @@ public class ThumbCell extends StackPane { update(); } + private Node createPreviewTrigger() { + int s = 32; + StackPane previewTrigger = new StackPane(); + previewTrigger.setStyle("-fx-background-color: white;"); + previewTrigger.setOpacity(.8); + previewTrigger.setMaxSize(s, s); + + Polygon play = new Polygon(new double[] { + 16, 8, + 26, 15, + 16, 22 + }); + StackPane.setMargin(play, new Insets(0, 0, 0, 3)); + play.setStyle("-fx-background-color: black;"); + previewTrigger.getChildren().add(play); + + Circle clip = new Circle(s / 2); + clip.setTranslateX(clip.getRadius()); + clip.setTranslateY(clip.getRadius()); + previewTrigger.setClip(clip); + StackPane.setAlignment(previewTrigger, Pos.BOTTOM_LEFT); + StackPane.setMargin(previewTrigger, new Insets(0, 0, 24, 4)); + previewTrigger.setOnMouseEntered(evt -> setPreviewVisible(previewTrigger, true)); + previewTrigger.setOnMouseExited(evt -> setPreviewVisible(previewTrigger, false)); + return previewTrigger; + } + + private void setPreviewVisible(Node previewTrigger, boolean visible) { + parent.suspendUpdates(visible); + iv.setVisible(!visible); + topic.setVisible(!visible); + topicBackground.setVisible(!visible); + name.setVisible(!visible); + nameBackground.setVisible(!visible); + streamPreview.setVisible(visible); + streamPreview.startStream(model); + recordingIndicator.setVisible(!visible); + pausedIndicator.setVisible(!visible); + if(!visible) { + updateRecordingIndicator(); + } + previewTrigger.setCursor(visible ? Cursor.HAND : Cursor.DEFAULT); + } + public void setSelected(boolean selected) { selectionProperty.set(selected); selectionOverlay.getStyleClass().add("selection-background"); @@ -356,6 +412,10 @@ public class ThumbCell extends StackPane { nameBackground.setFill(c); } + updateRecordingIndicator(); + } + + private void updateRecordingIndicator() { if(recording) { recordingIndicator.setVisible(!model.isSuspended()); pausedIndicator.setVisible(model.isSuspended()); @@ -574,13 +634,15 @@ public class ThumbCell extends StackPane { nameBackground.setWidth(w); nameBackground.setHeight(20); topicBackground.setWidth(w); - topicBackground.setHeight(getHeight()-nameBackground.getHeight()); + topicBackground.setHeight(h - nameBackground.getHeight()); topic.prefHeight(getHeight()-25); topic.maxHeight(getHeight()-25); int margin = 4; topic.maxWidth(w-margin*2); topic.setWrappingWidth(w-margin*2); + streamPreview.resizeTo(w, h); + Rectangle clip = new Rectangle(w, h); clip.setArcWidth(10); clip.arcHeightProperty().bind(clip.arcWidthProperty()); diff --git a/client/src/main/java/ctbrec/ui/controls/StreamPreview.java b/client/src/main/java/ctbrec/ui/controls/StreamPreview.java index 258ec42b..6b7bc022 100644 --- a/client/src/main/java/ctbrec/ui/controls/StreamPreview.java +++ b/client/src/main/java/ctbrec/ui/controls/StreamPreview.java @@ -34,8 +34,8 @@ public class StreamPreview extends StackPane { private MediaPlayer videoPlayer; private Media video; private ProgressIndicator progressIndicator; - private ExecutorService executor = Executors.newSingleThreadExecutor(); - private Future future; + private static ExecutorService executor = Executors.newSingleThreadExecutor(); + private static Future future; public StreamPreview() { videoPreview = new MediaView(); @@ -53,7 +53,6 @@ public class StreamPreview extends StackPane { progressIndicator = new ProgressIndicator(); progressIndicator.setVisible(false); - progressIndicator.prefWidthProperty().bind(videoPreview.fitWidthProperty()); Region veil = new Region(); veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.8)"); @@ -64,14 +63,25 @@ public class StreamPreview extends StackPane { } public void startStream(Model model) { + Platform.runLater(() -> { + progressIndicator.setVisible(true); + if(model.getPreview() != null) { + try { + videoPreview.setVisible(false); + Image img = new Image(model.getPreview(), true); + preview.setImage(img); + double aspect = img.getWidth() / img.getHeight(); + double w = Config.getInstance().getSettings().thumbWidth; + double h = w / aspect; + resizeTo(w, h); + } catch (Exception e) {} + } + }); if(future != null && !future.isDone()) { future.cancel(true); } future = executor.submit(() -> { try { - Platform.runLater(() -> { - progressIndicator.setVisible(true); - }); List sources = model.getStreamSources(); Collections.sort(sources); StreamSource best = sources.get(0); @@ -90,7 +100,7 @@ public class StreamPreview extends StackPane { double aspect = (double)video.getWidth() / video.getHeight(); double w = Config.getInstance().getSettings().thumbWidth; double h = w / aspect; - resizeToFitContent(w, h); + resizeTo(w, h); progressIndicator.setVisible(false); videoPreview.setVisible(true); videoPreview.setMediaPlayer(videoPlayer); @@ -127,23 +137,23 @@ public class StreamPreview extends StackPane { }); } - private void resizeToFitContent(double w, double h) { - setPrefSize(w, h); + public void resizeTo(double w, double h) { preview.setFitWidth(w); preview.setFitHeight(h); videoPreview.setFitWidth(w); videoPreview.setFitHeight(h); + progressIndicator.setPrefSize(w, h); } public void stop() { if(future != null && !future.isDone()) { future.cancel(true); } - Platform.runLater(() -> { + new Thread(() -> { if(videoPlayer != null) { videoPlayer.dispose(); } - }); + }).start(); } private void onError(MediaPlayer videoPlayer) { @@ -151,7 +161,7 @@ public class StreamPreview extends StackPane { if(videoPlayer.getError().getCause() != null) { LOG.error("Error while starting preview stream root cause:", videoPlayer.getError().getCause()); } - videoPlayer.dispose(); + stop(); Platform.runLater(() -> { showTestImage(); }); @@ -165,7 +175,7 @@ public class StreamPreview extends StackPane { double aspect = img.getWidth() / img.getHeight(); double w = Config.getInstance().getSettings().thumbWidth; double h = w / aspect; - resizeToFitContent(w, h); + resizeTo(w, h); progressIndicator.setVisible(false); }); } From e621e49e0052f8b9e5dc147a495e9b8be8800902 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 16 Dec 2018 02:38:21 +0100 Subject: [PATCH 219/231] Wait for segment download thread pool to finish ... when the download terminates --- .../ctbrec/recorder/download/HlsDownload.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java index b99f3be7..9148f198 100644 --- a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java @@ -19,6 +19,7 @@ import java.text.SimpleDateFormat; import java.time.Instant; import java.util.Date; import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -74,19 +75,14 @@ public class HlsDownload extends AbstractHlsDownload { } int lastSegment = 0; int nextSegment = 0; + boolean sleep = true; // this enables sleeping between playlist requests + // once we miss a segment, this is set to false, so that no sleeping happens anymore while(running) { SegmentPlaylist lsp = getNextSegments(segments); if(nextSegment > 0 && lsp.seq > nextSegment) { - LOG.warn("Missed segments {} < {} in download for {}", nextSegment, lsp.seq, model); - String first = lsp.segments.get(0); - int seq = lsp.seq; - for (int i = nextSegment; i < lsp.seq; i++) { - URL segmentUrl = new URL(first.replaceAll(Integer.toString(seq), Integer.toString(i))); - LOG.debug("Reloading segment {} for model {}", i, model.getName()); - String prefix = nf.format(segmentCounter++); - downloadThreadPool.submit(new SegmentDownload(segmentUrl, downloadDir, client, prefix)); - } // TODO switch to a lower bitrate/resolution ?!? + LOG.warn("Missed segments {} < {} in download for {}", nextSegment, lsp.seq, model); + sleep = false; } int skip = nextSegment - lsp.seq; for (String segment : lsp.segments) { @@ -101,7 +97,7 @@ public class HlsDownload extends AbstractHlsDownload { } long wait = 0; - if(lastSegment == lsp.seq) { + if(sleep && lastSegment == lsp.seq) { // playlist didn't change -> wait for at least half the target duration wait = (long) lsp.targetDuration * 1000 / 2; LOG.trace("Playlist didn't change... waiting for {}ms", wait); @@ -142,6 +138,11 @@ public class HlsDownload extends AbstractHlsDownload { throw new IOException("Couldn't download segment", e); } finally { alive = false; + downloadThreadPool.shutdown(); + try { + LOG.debug("Waiting for last segments for {}", model); + downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) {} LOG.debug("Download for {} terminated", model); } } From ebb5310d262134265643be7dcf9bcf8e0c7a0032 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 16 Dec 2018 16:14:53 +0100 Subject: [PATCH 220/231] Wait for the download to terminate before starting PP Sometimes the PP was started before the last segments were downloaded. This could cause unexpected effects. E.g. the playlist generator would fail, because the number of segments chained during playlist generation. --- .../java/ctbrec/recorder/LocalRecorder.java | 10 +++++-- .../ctbrec/recorder/download/HlsDownload.java | 17 ++++++++--- .../recorder/download/MergedHlsDownload.java | 28 +++++++++++++++++-- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 6e16c809..5dafee65 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -216,10 +216,14 @@ public class LocalRecorder implements Recorder { private void stopRecordingProcess(Model model) { Download download = recordingProcesses.get(model); - download.stop(); recordingProcesses.remove(model); fireRecordingStateChanged(download.getTarget(), STOPPED, model, download.getStartTime()); - ppThreadPool.submit(createPostProcessor(download)); + + Runnable stopAndThePostProcess = () -> { + download.stop(); + createPostProcessor(download).run(); + }; + ppThreadPool.submit(stopAndThePostProcess); } private void postprocess(Download download) { @@ -551,6 +555,8 @@ public class LocalRecorder implements Recorder { continue; } + // TODO don't list recordings, which currently get deleted + Date startDate = sdf.parse(rec.getName()); Recording recording = new Recording(); recording.setModelName(subdir.getName()); diff --git a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java index 9148f198..f682acda 100644 --- a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java @@ -44,6 +44,7 @@ public class HlsDownload extends AbstractHlsDownload { private int segmentCounter = 1; private NumberFormat nf = new DecimalFormat("000000"); + private Object downloadFinished = new Object(); public HlsDownload(HttpClient client) { super(client); @@ -75,8 +76,7 @@ public class HlsDownload extends AbstractHlsDownload { } int lastSegment = 0; int nextSegment = 0; - boolean sleep = true; // this enables sleeping between playlist requests - // once we miss a segment, this is set to false, so that no sleeping happens anymore + boolean sleep = true; // this enables sleeping between playlist requests. once we miss a segment, this is set to false, so that no sleeping happens anymore while(running) { SegmentPlaylist lsp = getNextSegments(segments); if(nextSegment > 0 && lsp.seq > nextSegment) { @@ -137,12 +137,15 @@ public class HlsDownload extends AbstractHlsDownload { } catch(Exception e) { throw new IOException("Couldn't download segment", e); } finally { - alive = false; downloadThreadPool.shutdown(); try { LOG.debug("Waiting for last segments for {}", model); downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS); } catch (InterruptedException e) {} + alive = false; + synchronized (downloadFinished) { + downloadFinished.notifyAll(); + } LOG.debug("Download for {} terminated", model); } } @@ -150,7 +153,13 @@ public class HlsDownload extends AbstractHlsDownload { @Override public void stop() { running = false; - alive = false; + try { + synchronized (downloadFinished) { + downloadFinished.wait(); + } + } catch (InterruptedException e) { + LOG.error("Couldn't wait for download to finish", e); + } } private static class SegmentDownload implements Callable { diff --git a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java index e298d48a..958fae17 100644 --- a/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java @@ -66,6 +66,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { 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(); public MergedHlsDownload(HttpClient client) { super(client); @@ -105,13 +106,20 @@ public class MergedHlsDownload extends AbstractHlsDownload { } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) { throw new IOException("Couldn't add HMAC to playlist url", e); } finally { - alive = false; try { streamer.stop(); } catch(Exception e) { LOG.error("Couldn't stop streamer", e); } downloadThreadPool.shutdown(); + try { + LOG.debug("Waiting for last segments for {}", model); + downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) {} + alive = false; + synchronized (downloadFinished) { + downloadFinished.notifyAll(); + } LOG.debug("Download terminated for {}", segmentPlaylistUri); } } @@ -155,7 +163,6 @@ public class MergedHlsDownload extends AbstractHlsDownload { } catch(Exception e) { throw new IOException("Couldn't download segment", e); } finally { - alive = false; if(streamer != null) { try { streamer.stop(); @@ -163,6 +170,15 @@ public class MergedHlsDownload extends AbstractHlsDownload { LOG.error("Couldn't stop streamer", e); } } + downloadThreadPool.shutdown(); + try { + LOG.debug("Waiting for last segments for {}", model); + downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS); + } catch (InterruptedException e) {} + alive = false; + synchronized (downloadFinished) { + downloadFinished.notifyAll(); + } LOG.debug("Download for {} terminated", model); } } @@ -353,10 +369,16 @@ public class MergedHlsDownload extends AbstractHlsDownload { @Override public void stop() { running = false; - alive = false; if(streamer != null) { streamer.stop(); } + try { + synchronized (downloadFinished) { + downloadFinished.wait(); + } + } catch (InterruptedException e) { + LOG.error("Couldn't wait for download to finish", e); + } LOG.debug("Download stopped"); } From f75687752c0fd24ec59532322a42d762e8be525b Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 16 Dec 2018 17:19:57 +0100 Subject: [PATCH 221/231] Add config setting for stream previews in thumbnails This setting allows to switch stream previews of in the thumbnail views. The little play circle will not show up. --- client/src/main/java/ctbrec/ui/ThumbCell.java | 4 ++- .../java/ctbrec/ui/settings/SettingsTab.java | 26 ++++++++++++++----- common/src/main/java/ctbrec/Settings.java | 1 + 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/ThumbCell.java b/client/src/main/java/ctbrec/ui/ThumbCell.java index cb67fa21..fa3d4746 100644 --- a/client/src/main/java/ctbrec/ui/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/ThumbCell.java @@ -174,7 +174,9 @@ public class ThumbCell extends StackPane { StackPane.setAlignment(pausedIndicator, Pos.TOP_LEFT); getChildren().add(pausedIndicator); - getChildren().add(createPreviewTrigger()); + if(Config.getInstance().getSettings().previewInThumbnails) { + getChildren().add(createPreviewTrigger()); + } selectionOverlay = new Rectangle(); selectionOverlay.visibleProperty().bind(selectionProperty); diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index c06733b2..d0794572 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -64,6 +64,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private CheckBox chooseStreamQuality = new CheckBox(); private CheckBox multiplePlayers = new CheckBox(); private CheckBox updateThumbnails = new CheckBox(); + private CheckBox previewInThumbnails = new CheckBox(); private CheckBox showPlayerStarting = new CheckBox(); private RadioButton recordLocal; private RadioButton recordRemote; @@ -422,7 +423,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { Config.getInstance().getSettings().showPlayerStarting = showPlayerStarting.isSelected(); saveConfig(); }); - GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); + GridPane.setMargin(l, new Insets(3, 0, 0, 0)); GridPane.setMargin(showPlayerStarting, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(showPlayerStarting, 1, row++); @@ -434,7 +435,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { Config.getInstance().getSettings().determineResolution = loadResolution.isSelected(); saveConfig(); }); - GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); + GridPane.setMargin(l, new Insets(3, 0, 0, 0)); GridPane.setMargin(loadResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(loadResolution, 1, row++); @@ -445,7 +446,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected(); saveConfig(); }); - GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); + GridPane.setMargin(l, new Insets(3, 0, 0, 0)); GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(chooseStreamQuality, 1, row++); @@ -456,10 +457,21 @@ public class SettingsTab extends Tab implements TabSelectionListener { Config.getInstance().getSettings().updateThumbnails = updateThumbnails.isSelected(); saveConfig(); }); - GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); - GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, CHECKBOX_MARGIN, CHECKBOX_MARGIN)); + GridPane.setMargin(l, new Insets(3, 0, 0, 0)); + GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(updateThumbnails, 1, row++); + l = new Label("Preview in thumbnails"); + layout.add(l, 0, row); + previewInThumbnails.setSelected(Config.getInstance().getSettings().previewInThumbnails); + previewInThumbnails.setOnAction((e) -> { + Config.getInstance().getSettings().previewInThumbnails = previewInThumbnails.isSelected(); + saveConfig(); + }); + GridPane.setMargin(l, new Insets(3, 0, 0, 0)); + GridPane.setMargin(previewInThumbnails, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + layout.add(previewInThumbnails, 1, row++); + l = new Label("Start Tab"); layout.add(l, 0, row); startTab = new ComboBox<>(); @@ -468,8 +480,8 @@ public class SettingsTab extends Tab implements TabSelectionListener { saveConfig(); }); layout.add(startTab, 1, row++); - GridPane.setMargin(l, new Insets(0, 0, 0, 0)); - GridPane.setMargin(startTab, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + GridPane.setMargin(l, new Insets(3, 0, 0, 0)); + GridPane.setMargin(startTab, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); l = new Label("Colors (Base / Accent)"); layout.add(l, 0, row); diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 168f2e25..eb480855 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -69,6 +69,7 @@ public class Settings { public List models = new ArrayList<>(); public List eventHandlers = new ArrayList<>(); public boolean determineResolution = false; + public boolean previewInThumbnails = true; public boolean requireAuthentication = false; public boolean chooseStreamQuality = false; public int maximumResolution = 0; From 3d7fc64bf54d40020e6ce7e537397f7ab17e232b Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 16 Dec 2018 17:20:27 +0100 Subject: [PATCH 222/231] Improve error handling in the StreamPreview --- .../ctbrec/ui/controls/StreamPreview.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/controls/StreamPreview.java b/client/src/main/java/ctbrec/ui/controls/StreamPreview.java index 6b7bc022..2dd21c52 100644 --- a/client/src/main/java/ctbrec/ui/controls/StreamPreview.java +++ b/client/src/main/java/ctbrec/ui/controls/StreamPreview.java @@ -88,6 +88,7 @@ public class StreamPreview extends StackPane { checkInterrupt(); LOG.debug("Preview url for {} is {}", model.getName(), best.getMediaPlaylistUrl()); video = new Media(best.getMediaPlaylistUrl()); + video.setOnError(() -> onError(videoPlayer)); if(videoPlayer != null) { videoPlayer.dispose(); } @@ -128,8 +129,8 @@ public class StreamPreview extends StackPane { // future has been canceled, that's fine } else { LOG.warn("Couldn't start preview video: {}", e.getMessage()); - showTestImage(); } + showTestImage(); } catch (Exception e) { LOG.warn("Couldn't start preview video: {}", e.getMessage()); showTestImage(); @@ -146,12 +147,14 @@ public class StreamPreview extends StackPane { } public void stop() { - if(future != null && !future.isDone()) { - future.cancel(true); - } + MediaPlayer old = videoPlayer; + Future oldFuture = future; new Thread(() -> { - if(videoPlayer != null) { - videoPlayer.dispose(); + if(oldFuture != null && !oldFuture.isDone()) { + oldFuture.cancel(true); + } + if(old != null) { + old.dispose(); } }).start(); } @@ -161,13 +164,11 @@ public class StreamPreview extends StackPane { if(videoPlayer.getError().getCause() != null) { LOG.error("Error while starting preview stream root cause:", videoPlayer.getError().getCause()); } - stop(); - Platform.runLater(() -> { - showTestImage(); - }); + showTestImage(); } private void showTestImage() { + stop(); Platform.runLater(() -> { videoPreview.setVisible(false); Image img = new Image(getClass().getResource("/image_not_found.png").toString(), true); From 1e4743271402b44cff0947a4aa955c4851239857 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 16 Dec 2018 17:21:11 +0100 Subject: [PATCH 223/231] Add origin stream source only, if mp4-ws sources are available --- .../sites/streamate/StreamateModel.java | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java index c4e04670..55497e8a 100644 --- a/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java +++ b/common/src/main/java/ctbrec/sites/streamate/StreamateModel.java @@ -91,7 +91,6 @@ public class StreamateModel extends AbstractModel { if(response.isSuccessful()) { JSONObject json = new JSONObject(response.body().string()); JSONObject formats = json.getJSONObject("formats"); - JSONObject ws = formats.getJSONObject("mp4-ws"); JSONObject hls = formats.getJSONObject("mp4-hls"); // add encodings @@ -108,14 +107,17 @@ public class StreamateModel extends AbstractModel { } // add raw source stream - JSONObject origin = hls.getJSONObject("origin"); - StreamSource src = new StreamSource(); - src.mediaPlaylistUrl = origin.getString("location"); - origin = ws.getJSONObject("origin"); // switch to web socket origin, because it has width, height and bitrates - src.width = origin.optInt("videoWidth"); - src.height = origin.optInt("videoHeight"); - src.bandwidth = (origin.optInt("videoKbps") + origin.optInt("audioKbps")) * 1024; - streamSources.add(src); + if(formats.has("mp4-ws")) { + JSONObject ws = formats.getJSONObject("mp4-ws"); + JSONObject origin = hls.getJSONObject("origin"); + StreamSource src = new StreamSource(); + src.mediaPlaylistUrl = origin.getString("location"); + origin = ws.getJSONObject("origin"); // switch to web socket origin, because it has width, height and bitrates + src.width = origin.optInt("videoWidth"); + src.height = origin.optInt("videoHeight"); + src.bandwidth = (origin.optInt("videoKbps") + origin.optInt("audioKbps")) * 1024; + streamSources.add(src); + } } else { throw new HttpException(response.code(), response.message()); } From a7ab34c9d76b6c31e27424414a50795753814ab1 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 16 Dec 2018 17:25:05 +0100 Subject: [PATCH 224/231] Set user data directory for WebbrowserTab --- client/src/main/java/ctbrec/ui/WebbrowserTab.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/WebbrowserTab.java b/client/src/main/java/ctbrec/ui/WebbrowserTab.java index 3def3feb..2e44c556 100644 --- a/client/src/main/java/ctbrec/ui/WebbrowserTab.java +++ b/client/src/main/java/ctbrec/ui/WebbrowserTab.java @@ -1,5 +1,6 @@ package ctbrec.ui; +import ctbrec.Config; import javafx.scene.control.Tab; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; @@ -10,6 +11,7 @@ public class WebbrowserTab extends Tab { WebView browser = new WebView(); WebEngine webEngine = browser.getEngine(); webEngine.load(uri); + webEngine.setUserDataDirectory(Config.getInstance().getConfigDir()); setContent(browser); } } From 7b7c7b24b1bd3f902823ceeb98ab8c0992ef8caf Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 16 Dec 2018 17:50:56 +0100 Subject: [PATCH 225/231] Replace Exception parameter with Throwable --- client/src/main/java/ctbrec/ui/controls/Dialogs.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/controls/Dialogs.java b/client/src/main/java/ctbrec/ui/controls/Dialogs.java index 558f6e0f..653bbc91 100644 --- a/client/src/main/java/ctbrec/ui/controls/Dialogs.java +++ b/client/src/main/java/ctbrec/ui/controls/Dialogs.java @@ -5,14 +5,14 @@ import javafx.application.Platform; import javafx.scene.control.Alert; public class Dialogs { - public static void showError(String header, String text, Exception e) { + public static void showError(String header, String text, Throwable t) { Runnable r = () -> { Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); alert.setTitle("Error"); alert.setHeaderText(header); String content = text; - if(e != null) { - content += " " + e.getLocalizedMessage(); + if(t != null) { + content += " " + t.getLocalizedMessage(); } alert.setContentText(content); alert.showAndWait(); From 10184176b0cddfa65335c0d012b2d83ed3c13c7c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 16 Dec 2018 19:53:54 +0100 Subject: [PATCH 226/231] Enable JavaScript and register an error handler --- .../src/main/java/ctbrec/ui/WebbrowserTab.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/WebbrowserTab.java b/client/src/main/java/ctbrec/ui/WebbrowserTab.java index 2e44c556..cf904a3e 100644 --- a/client/src/main/java/ctbrec/ui/WebbrowserTab.java +++ b/client/src/main/java/ctbrec/ui/WebbrowserTab.java @@ -1,17 +1,31 @@ package ctbrec.ui; -import ctbrec.Config; +import java.io.File; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.OS; +import ctbrec.ui.controls.Dialogs; import javafx.scene.control.Tab; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; public class WebbrowserTab extends Tab { + private static final transient Logger LOG = LoggerFactory.getLogger(WebbrowserTab.class); + public WebbrowserTab(String uri) { WebView browser = new WebView(); WebEngine webEngine = browser.getEngine(); + webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine")); + webEngine.setJavaScriptEnabled(true); webEngine.load(uri); - webEngine.setUserDataDirectory(Config.getInstance().getConfigDir()); setContent(browser); + + webEngine.setOnError(evt -> { + LOG.error("Couldn't load {}", uri, evt.getException()); + Dialogs.showError("Error", "Couldn't load " + uri, evt.getException()); + }); } } From d74737113a9be646046624cc102e74e945d7e67a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 16 Dec 2018 20:04:26 +0100 Subject: [PATCH 227/231] Change max resolution input to textfield ... to allow arbitrary values --- .../java/ctbrec/ui/settings/SettingsTab.java | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index d0794572..4916974f 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -70,7 +70,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private RadioButton recordRemote; private ToggleGroup recordLocation; private ProxySettingsPane proxySettingsPane; - private ComboBox maxResolution; + private TextField maxResolution; private ComboBox splitAfter; private ComboBox directoryStructure; private ComboBox startTab; @@ -264,26 +264,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { layout.add(directoryStructure, 1, row++); recordingsDirectory.prefWidthProperty().bind(directoryStructure.widthProperty()); - Label l = new Label("Maximum resolution (0 = unlimited)"); - layout.add(l, 0, row); - List resolutionOptions = new ArrayList<>(); - resolutionOptions.add(1080); - resolutionOptions.add(720); - resolutionOptions.add(600); - resolutionOptions.add(480); - resolutionOptions.add(0); - maxResolution = new ComboBox<>(FXCollections.observableList(resolutionOptions)); - setMaxResolutionValue(); - maxResolution.setOnAction((e) -> { - Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem(); - saveConfig(); - }); - maxResolution.prefWidthProperty().bind(directoryStructure.widthProperty()); - layout.add(maxResolution, 1, row++); - GridPane.setMargin(l, new Insets(0, 0, 0, 0)); - GridPane.setMargin(maxResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - - l = new Label("Split recordings after (minutes)"); + Label l = new Label("Split recordings after (minutes)"); layout.add(l, 0, row); List splitOptions = new ArrayList<>(); splitOptions.add(new SplitAfterOption("disabled", 0)); @@ -308,6 +289,26 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(l, new Insets(0, 0, 0, 0)); GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + l = new Label("Maximum resolution (0 = unlimited)"); + layout.add(l, 0, row); + maxResolution = new TextField(Integer.toString(Config.getInstance().getSettings().maximumResolution)); + maxResolution.textProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue.matches("\\d*")) { + maxResolution.setText(newValue.replaceAll("[^\\d]", "")); + } + if (!maxResolution.getText().isEmpty()) { + int newRes = Integer.parseInt(maxResolution.getText()); + if (newRes != Config.getInstance().getSettings().maximumResolution) { + Config.getInstance().getSettings().maximumResolution = newRes; + saveConfig(); + } + } + }); + maxResolution.prefWidthProperty().bind(directoryStructure.widthProperty()); + layout.add(maxResolution, 1, row++); + GridPane.setMargin(l, new Insets(0, 0, 0, 0)); + GridPane.setMargin(maxResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + layout.add(new Label("Post-Processing"), 0, row); // TODO allow empty strings to remove post-processing scripts postProcessing = new ProgramSelectionBox(Config.getInstance().getSettings().postProcessing); @@ -504,15 +505,6 @@ public class SettingsTab extends Tab implements TabSelectionListener { } } - private void setMaxResolutionValue() { - int value = Config.getInstance().getSettings().maximumResolution; - for (Integer option : maxResolution.getItems()) { - if(option == value) { - maxResolution.getSelectionModel().select(option); - } - } - } - void showRestartRequired() { restartLabel.setVisible(true); } From 910d21463a75aae99ea1a843b62ca82ff6414436 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 16 Dec 2018 20:29:48 +0100 Subject: [PATCH 228/231] Fix: allow empty input / deletion of post-processing script --- .../ui/controls/AbstractFileSelectionBox.java | 57 ++++++++++++------- .../ui/controls/DirectorySelectionBox.java | 2 +- .../ui/settings/ActionSettingsPanel.java | 4 +- .../java/ctbrec/ui/settings/SettingsTab.java | 8 +-- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java index 556c1393..f377998b 100644 --- a/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java +++ b/client/src/main/java/ctbrec/ui/controls/AbstractFileSelectionBox.java @@ -6,9 +6,10 @@ import java.io.IOException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ctbrec.StringUtil; import ctbrec.ui.AutosizeAlert; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ObjectPropertyBase; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.geometry.Point2D; import javafx.scene.Node; @@ -29,18 +30,20 @@ import javafx.stage.FileChooser; public abstract class AbstractFileSelectionBox extends HBox { private static final transient Logger LOG = LoggerFactory.getLogger(AbstractFileSelectionBox.class); - private ObjectProperty fileProperty = new ObjectPropertyBase() { - @Override - public Object getBean() { - return null; - } - - @Override - public String getName() { - return "file"; - } - }; + // private ObjectProperty fileProperty = new ObjectPropertyBase() { + // @Override + // public Object getBean() { + // return null; + // } + // + // @Override + // public String getName() { + // return "file"; + // } + // }; + private StringProperty fileProperty = new SimpleStringProperty(); protected TextField fileInput; + protected boolean allowEmptyValue = false; private Tooltip validationError = new Tooltip(); public AbstractFileSelectionBox() { @@ -67,8 +70,14 @@ public abstract class AbstractFileSelectionBox extends HBox { private ChangeListener textListener() { return (obs, o, n) -> { String input = fileInput.getText(); - File program = new File(input); - setFile(program); + if(StringUtil.isBlank(input) && allowEmptyValue) { + fileProperty.set(""); + hideValidationHints(); + return; + } else { + File program = new File(input); + setFile(program); + } }; } @@ -83,13 +92,17 @@ public abstract class AbstractFileSelectionBox extends HBox { validationError.show(getScene().getWindow(), p.getX(), p.getY() + fileInput.getHeight() + 4); } } else { - fileInput.setBorder(Border.EMPTY); - fileInput.setTooltip(null); - fileProperty.set(file); - validationError.hide(); + fileProperty.set(file.getAbsolutePath()); + hideValidationHints(); } } + private void hideValidationHints() { + fileInput.setBorder(Border.EMPTY); + fileInput.setTooltip(null); + validationError.hide(); + } + protected String validate(File file) { if (file == null || !file.exists()) { return "File does not exist"; @@ -98,6 +111,10 @@ public abstract class AbstractFileSelectionBox extends HBox { } } + public void allowEmptyValue() { + this.allowEmptyValue = true; + } + private Button createBrowseButton() { Button button = new Button("Select"); button.setOnAction((e) -> { @@ -123,7 +140,7 @@ public abstract class AbstractFileSelectionBox extends HBox { } } - public ObjectProperty fileProperty() { + public StringProperty fileProperty() { return fileProperty; } } diff --git a/client/src/main/java/ctbrec/ui/controls/DirectorySelectionBox.java b/client/src/main/java/ctbrec/ui/controls/DirectorySelectionBox.java index f3f1a5e5..ca65a7c4 100644 --- a/client/src/main/java/ctbrec/ui/controls/DirectorySelectionBox.java +++ b/client/src/main/java/ctbrec/ui/controls/DirectorySelectionBox.java @@ -12,7 +12,7 @@ public class DirectorySelectionBox extends AbstractFileSelectionBox { @Override protected void choose() { DirectoryChooser chooser = new DirectoryChooser(); - File currentDir = fileProperty().get(); + File currentDir = new File(fileProperty().get()); if (currentDir.exists() && currentDir.isDirectory()) { chooser.setInitialDirectory(currentDir); } diff --git a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java index a163a19e..182ca2d5 100644 --- a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java +++ b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java @@ -173,7 +173,7 @@ public class ActionSettingsPanel extends TitledPane { if(playSound.isSelected()) { ActionConfiguration ac = new ActionConfiguration(); ac.setType(PlaySound.class.getName()); - File file = sound.fileProperty().get(); + File file = new File(sound.fileProperty().get()); ac.getConfiguration().put("file", file.getAbsolutePath()); ac.setName("play " + file.getName()); config.getActions().add(ac); @@ -181,7 +181,7 @@ public class ActionSettingsPanel extends TitledPane { if(executeProgram.isSelected()) { ActionConfiguration ac = new ActionConfiguration(); ac.setType(ExecuteProgram.class.getName()); - File file = program.fileProperty().get(); + File file = new File(program.fileProperty().get()); ac.getConfiguration().put("file", file.getAbsolutePath()); ac.setName("execute " + file.getName()); config.getActions().add(ac); diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index 4916974f..4585978f 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -238,7 +238,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { recordingsDirectory = new DirectorySelectionBox(Config.getInstance().getSettings().recordingsDir); recordingsDirectory.prefWidth(400); recordingsDirectory.fileProperty().addListener((obs, o, n) -> { - String path = n.getAbsolutePath(); + String path = n; if(!Objects.equals(path, Config.getInstance().getSettings().recordingsDir)) { Config.getInstance().getSettings().recordingsDir = path; saveConfig(); @@ -310,10 +310,10 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(maxResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(new Label("Post-Processing"), 0, row); - // TODO allow empty strings to remove post-processing scripts postProcessing = new ProgramSelectionBox(Config.getInstance().getSettings().postProcessing); + postProcessing.allowEmptyValue(); postProcessing.fileProperty().addListener((obs, o, n) -> { - String path = n.getAbsolutePath(); + String path = n; if(!Objects.equals(path, Config.getInstance().getSettings().postProcessing)) { Config.getInstance().getSettings().postProcessing = path; saveConfig(); @@ -395,7 +395,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { layout.add(new Label("Player"), 0, row); mediaPlayer = new ProgramSelectionBox(Config.getInstance().getSettings().mediaPlayer); mediaPlayer.fileProperty().addListener((obs, o, n) -> { - String path = n.getAbsolutePath(); + String path = n; if (!Objects.equals(path, Config.getInstance().getSettings().mediaPlayer)) { Config.getInstance().getSettings().mediaPlayer = path; saveConfig(); From d1cf6a681b9933cca9f28dd16b0fda9e09802142 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 16 Dec 2018 20:31:09 +0100 Subject: [PATCH 229/231] Remove outdated comment --- common/src/main/java/ctbrec/OS.java | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/java/ctbrec/OS.java b/common/src/main/java/ctbrec/OS.java index d5181887..e86842e4 100644 --- a/common/src/main/java/ctbrec/OS.java +++ b/common/src/main/java/ctbrec/OS.java @@ -93,7 +93,6 @@ public class OS { } else if(OS.getOsType() == OS.TYPE.WINDOWS) { notifyWindows(title, header, msg); } else if(OS.getOsType() == OS.TYPE.MAC) { - // TODO find out, if it makes a sound or if we have to play a sound notifyMac(title, header, msg); } else { // unknown system, try systemtray notification anyways From bfbd6b17828f090629b34c16619cf975bfdb56a5 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 16 Dec 2018 20:38:58 +0100 Subject: [PATCH 230/231] Open the player on double-click in the Recording tab Implements #121 --- client/src/main/java/ctbrec/ui/RecordedModelsTab.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java index ae41af76..ab693d3f 100644 --- a/client/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/client/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -58,6 +58,7 @@ import javafx.scene.input.ClipboardContent; import javafx.scene.input.ContextMenuEvent; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.FlowPane; @@ -149,6 +150,14 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { } event.consume(); }); + table.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { + if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) { + JavaFxModel model = table.getSelectionModel().getSelectedItem(); + if(model != null) { + new PlayAction(table, model).execute(); + } + } + }); table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { if (popup != null) { popup.hide(); From 5145ed0ce25529e62b728f332e902f8edfaa4e2b Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 17 Dec 2018 13:00:46 +0100 Subject: [PATCH 231/231] Update changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9235ca88..41422077 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +1.16.0 +======================== +* Thumbnails can show a live preview. Can be switched off in the settings. +* Added Streamate (metcams, xhamstercams, pornhublive) +* Maximum resolution can be an arbitrary value now +* Added setting for minimal recording length. Recordings, which are shorter + than this value, get deleted automatically. +* Double-click in Recording tab starts the player +* Fix: BongaCams friends tab not working +* Fix: In some cases MFC models got confused + 1.15.0 ======================== * Fix: BongaCams overview didn't work anymore