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 01/32] 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 02/32] 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 03/32] 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 04/32] 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 05/32] 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 06/32] 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 07/32] 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 08/32] 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 09/32] 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 918f63b1f5db597667f594828a5f423a8f17c30f Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 30 Nov 2018 13:42:51 +0100 Subject: [PATCH 10/32] 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 11/32] 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 12/32] 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 13/32] 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 14/32] 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 15/32] 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 16/32] 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 17/32] 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 18/32] 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 19/32] 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 20/32] 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 21/32] 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 22/32] 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 23/32] 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 24/32] 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 25/32] 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 26/32] 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 27/32] 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 28/32] 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 29/32] 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 30/32] 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 31/32] 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 32/32] 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(); }); }