Merge branch 'dev' into v4

# Conflicts:
#	client/src/main/java/ctbrec/ui/action/CheckModelAccountAction.java
#	client/src/main/java/ctbrec/ui/controls/SearchPopoverTreeList.java
#	client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaShowsTab.java
#	client/src/main/resources/logback.xml
#	common/src/main/java/ctbrec/recorder/NextGenLocalRecorder.java
#	server/src/main/resources/logback.xml
This commit is contained in:
0xb00bface 2021-01-09 15:59:31 +01:00
commit cdf582ad8f
54 changed files with 1271 additions and 486 deletions

View File

@ -1,9 +1,21 @@
NEXT
========================
* Added "record later" tab to "bookmark" models
* Added config option to show the total number of models in the title bar
* Fixed problem with Cam4 playlist URLs, thanks @gohufrapoc
3.11.0 3.11.0
======================== ========================
* Added config option for faster scroll speed * Added config option for faster scroll speed
* Added a few more settings to the web interface * Added a few more settings to the web interface
* Added config option to show confirmation dialogs for irreversible actions
* Disabled right click in context menus
* Fixed unjustified chaturbate follow / unfollow error dialog
* Use lowercase model names for Cam4. This should resolve recording problems
* Updated Configration.md page in help section * Updated Configration.md page in help section
* Updated bundled Java to version 15.0.1 * Updated bundled Java to version 15.0.1
* Improved robustness of live previews (still experimental though)
* Some smaller UI tweaks here and there
3.10.10 3.10.10
======================== ========================

View File

@ -132,6 +132,7 @@
<bundledJre64Bit>true</bundledJre64Bit> <bundledJre64Bit>true</bundledJre64Bit>
<minVersion>15</minVersion> <minVersion>15</minVersion>
<maxHeapSize>512</maxHeapSize> <maxHeapSize>512</maxHeapSize>
<opt>-Dfile.encoding=utf-8</opt>
</jre> </jre>
<versionInfo> <versionInfo>
<fileVersion>4.0.0.0</fileVersion> <fileVersion>4.0.0.0</fileVersion>

View File

@ -4,5 +4,5 @@ DIR="$(dirname "$0")"
pushd "${DIR}" pushd "${DIR}"
JAVA=./jre/bin/java JAVA=./jre/bin/java
$JAVA -version $JAVA -version
$JAVA -Djdk.gtk.version=3 -cp "${DIR}:${name.final}.jar" ctbrec.ui.Launcher $JAVA -Djdk.gtk.version=3 -cp "${DIR}:${name.final}.jar" -Dfile.encoding=utf-8 ctbrec.ui.Launcher
popd popd

View File

@ -4,5 +4,5 @@ DIR="$(dirname "$0")"
pushd "${DIR}" pushd "${DIR}"
JAVA=java JAVA=java
$JAVA -version $JAVA -version
$JAVA -Djdk.gtk.version=3 -cp "${DIR}:${name.final}.jar" ctbrec.ui.Launcher $JAVA -Djdk.gtk.version=3 -cp "${DIR}:${name.final}.jar" -Dfile.encoding=utf-8 ctbrec.ui.Launcher
popd popd

View File

@ -5,5 +5,5 @@ pushd "$DIR"
JAVA_HOME="$DIR/jre/Contents/Home" JAVA_HOME="$DIR/jre/Contents/Home"
JAVA="$JAVA_HOME/bin/java" JAVA="$JAVA_HOME/bin/java"
$JAVA -version $JAVA -version
$JAVA -cp "${DIR}:${name.final}.jar" ctbrec.ui.Launcher $JAVA -cp "${DIR}:${name.final}.jar" -Dfile.encoding=utf-8 ctbrec.ui.Launcher
popd popd

View File

@ -4,5 +4,5 @@ DIR=$(dirname $0)
pushd "$DIR" pushd "$DIR"
JAVA=java JAVA=java
$JAVA -version $JAVA -version
$JAVA -cp "${DIR}:${name.final}.jar" ctbrec.ui.Launcher $JAVA -cp "${DIR}:${name.final}.jar" -Dfile.encoding=utf-8 ctbrec.ui.Launcher
popd popd

View File

@ -1 +1 @@
jre\bin\java -Xmx512m -cp ".;${name.final}.jar" ctbrec.ui.Launcher jre\bin\java -Xmx512m -cp ".;${name.final}.jar" -Dfile.encoding=utf-8 ctbrec.ui.Launcher

View File

@ -1,6 +1,8 @@
package ctbrec.ui; package ctbrec.ui;
import static ctbrec.event.Event.Type.*;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@ -61,7 +63,7 @@ import ctbrec.ui.news.NewsTab;
import ctbrec.ui.settings.SettingsTab; import ctbrec.ui.settings.SettingsTab;
import ctbrec.ui.tabs.DonateTabFx; import ctbrec.ui.tabs.DonateTabFx;
import ctbrec.ui.tabs.HelpTab; import ctbrec.ui.tabs.HelpTab;
import ctbrec.ui.tabs.RecordedModelsTab; import ctbrec.ui.tabs.RecordedTab;
import ctbrec.ui.tabs.RecordingsTab; import ctbrec.ui.tabs.RecordingsTab;
import ctbrec.ui.tabs.SiteTab; import ctbrec.ui.tabs.SiteTab;
import ctbrec.ui.tabs.TabSelectionListener; import ctbrec.ui.tabs.TabSelectionListener;
@ -102,7 +104,7 @@ public class CamrecApplication extends Application {
public static HttpClient httpClient; public static HttpClient httpClient;
public static String title; public static String title;
private Stage primaryStage; private Stage primaryStage;
private RecordedModelsTab modelsTab; private RecordedTab modelsTab;
private RecordingsTab recordingsTab; private RecordingsTab recordingsTab;
private ScheduledExecutorService scheduler; private ScheduledExecutorService scheduler;
private int activeRecordings = 0; private int activeRecordings = 0;
@ -210,7 +212,7 @@ public class CamrecApplication extends Application {
} }
} }
modelsTab = new RecordedModelsTab("Recording", recorder, sites); modelsTab = new RecordedTab(recorder, sites);
tabPane.getTabs().add(modelsTab); tabPane.getTabs().add(modelsTab);
recordingsTab = new RecordingsTab("Recordings", recorder, config); recordingsTab = new RecordingsTab("Recordings", recorder, config);
tabPane.getTabs().add(recordingsTab); tabPane.getTabs().add(recordingsTab);
@ -348,11 +350,12 @@ public class CamrecApplication extends Application {
EventBusHolder.BUS.register(new Object() { EventBusHolder.BUS.register(new Object() {
@Subscribe @Subscribe
public void handleEvent(Event evt) { public void handleEvent(Event evt) {
if(evt.getType() == Event.Type.MODEL_ONLINE || evt.getType() == Event.Type.MODEL_STATUS_CHANGED || evt.getType() == Event.Type.RECORDING_STATUS_CHANGED) { if (evt.getType() == MODEL_ONLINE || evt.getType() == MODEL_STATUS_CHANGED || evt.getType() == RECORDING_STATUS_CHANGED) {
try { try {
List<Model> models = recorder.getCurrentlyRecording(); int modelCount = recorder.getModelCount();
activeRecordings = models.size(); List<Model> currentlyRecording = recorder.getCurrentlyRecording();
String windowTitle = activeRecordings > 0 ? "(" + activeRecordings + ") " + title : title; activeRecordings = currentlyRecording.size();
String windowTitle = getActiveRecordings(activeRecordings, modelCount) + title;
Platform.runLater(() -> primaryStage.setTitle(windowTitle)); Platform.runLater(() -> primaryStage.setTitle(windowTitle));
updateStatus(); updateStatus();
} catch (Exception e) { } catch (Exception e) {
@ -360,6 +363,19 @@ public class CamrecApplication extends Application {
} }
} }
} }
private String getActiveRecordings(int activeRecordings, int modelCount) {
if (activeRecordings > 0) {
StringBuilder s = new StringBuilder("(").append(activeRecordings);
if (config.getSettings().totalModelCountInTitle) {
s.append("/").append(modelCount);
}
s.append(") ");
return s.toString();
} else {
return "";
}
}
}); });
} }
@ -377,7 +393,7 @@ public class CamrecApplication extends Application {
bytesPerSecond = 0; bytesPerSecond = 0;
} }
String humanReadable = ByteUnitFormatter.format(bytesPerSecond); String humanReadable = ByteUnitFormatter.format(bytesPerSecond);
String status = String.format("Recording %s / %s models @ %s/s", activeRecordings, recorder.getModels().size(), humanReadable); String status = String.format("Recording %s / %s models @ %s/s", activeRecordings, recorder.getModelCount(), humanReadable);
Platform.runLater(() -> statusLabel.setText(status)); Platform.runLater(() -> statusLabel.setText(status));
} }

View File

@ -1,19 +1,18 @@
package ctbrec.ui; package ctbrec.ui;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.scene.input.Clipboard; import javafx.scene.input.Clipboard;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Objects;
public class ClipboardListener implements Runnable { public class ClipboardListener implements Runnable {
@ -54,7 +53,7 @@ public class ClipboardListener implements Runnable {
Model m = site.createModelFromUrl(url); Model m = site.createModelFromUrl(url);
if (m != null) { if (m != null) {
try { try {
recorder.startRecording(m); recorder.addModel(m);
DesktopIntegration.notification("Add from clipboard", "Model added", "Model " + m.getDisplayName() + " added"); DesktopIntegration.notification("Add from clipboard", "Model added", "Model " + m.getDisplayName() + " added");
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) { } catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
DesktopIntegration.notification("Add from clipboard", "Error", "Couldn't add URL from clipboard: " + e.getLocalizedMessage()); DesktopIntegration.notification("Add from clipboard", "Error", "Couldn't add URL from clipboard: " + e.getLocalizedMessage());

View File

@ -319,4 +319,16 @@ public class JavaFxModel implements Model {
public boolean isRecordingTimeLimited() { public boolean isRecordingTimeLimited() {
return delegate.isRecordingTimeLimited(); return delegate.isRecordingTimeLimited();
} }
@Override
public boolean isMarkedForLaterRecording() {
return delegate.isMarkedForLaterRecording();
}
@Override
public void setMarkedForLaterRecording(boolean marked) {
delegate.setMarkedForLaterRecording(marked);
}
} }

View File

@ -1,5 +1,7 @@
package ctbrec.ui; package ctbrec.ui;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu; import javafx.scene.control.ContextMenu;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
@ -15,4 +17,17 @@ public class UiUtils {
} }
}); });
} }
public static void ignoreMouseReleasedIfMouseExited(ContextMenu menu) {
menu.addEventFilter(MouseEvent.MOUSE_RELEASED, evt -> {
if (evt.getTarget() instanceof Node) {
Node target = (Node) evt.getTarget();
Bounds screenBounds = target.localToScreen(target.getBoundsInLocal());
boolean releasedOnOriginalMouseItem = screenBounds.contains(evt.getScreenX(), evt.getScreenY());
if (!releasedOnOriginalMouseItem) {
evt.consume();
}
}
});
}
} }

View File

@ -3,6 +3,9 @@ package ctbrec.ui.action;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -26,13 +29,15 @@ public class CheckModelAccountAction {
} }
public void execute() { public void execute(Predicate<Model> filter) {
String buttonText = b.getText(); String buttonText = b.getText();
b.setDisable(true); b.setDisable(true);
Runnable checker = (() -> { CompletableFuture.runAsync(() -> {
List<Model> deletedAccounts = new ArrayList<>(); List<Model> deletedAccounts = new ArrayList<>();
try { try {
List<Model> models = recorder.getModels(); List<Model> models = recorder.getModels().stream() //
.filter(filter) //
.collect(Collectors.toList());
int total = models.size(); int total = models.size();
for (int i = 0; i < total; i++) { for (int i = 0; i < total; i++) {
final int counter = i+1; final int counter = i+1;
@ -66,6 +71,5 @@ public class CheckModelAccountAction {
}); });
} }
}); });
new Thread(checker).start();
} }
} }

View File

@ -1,20 +1,20 @@
package ctbrec.ui.action; package ctbrec.ui.action;
import java.util.List;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.scene.Node; import javafx.scene.Node;
import java.util.List;
public class StartRecordingAction extends ModelMassEditAction { public class StartRecordingAction extends ModelMassEditAction {
public StartRecordingAction(Node source, List<? extends Model> models, Recorder recorder) { public StartRecordingAction(Node source, List<? extends Model> models, Recorder recorder) {
super(source, models); super(source, models);
action = (m) -> { action = (m) -> {
try { try {
recorder.startRecording(m); recorder.addModel(m);
} catch (Exception e) { } catch (Exception e) {
Platform.runLater(() -> Platform.runLater(() ->
Dialogs.showError(source.getScene(), "Couldn't start recording", "Starting recording of " + m.getName() + " failed", e)); Dialogs.showError(source.getScene(), "Couldn't start recording", "Starting recording of " + m.getName() + " failed", e));

View File

@ -0,0 +1,14 @@
package ctbrec.ui.controls;
import ctbrec.ui.UiUtils;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
public class CustomMouseBehaviorContextMenu extends ContextMenu {
public CustomMouseBehaviorContextMenu(MenuItem...items) {
super(items);
UiUtils.disableRightClickFor(this);
UiUtils.ignoreMouseReleasedIfMouseExited(this);
}
}

View File

@ -31,13 +31,6 @@
*/ */
package ctbrec.ui.controls; package ctbrec.ui.controls;
import java.net.URL;
import java.util.Objects;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.PlayAction;
@ -47,15 +40,18 @@ import javafx.event.EventHandler;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.Skin;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.shape.Rectangle; import javafx.scene.shape.Rectangle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URL;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/** /**
* Popover page that displays a list of samples and sample categories for a given SampleCategory. * Popover page that displays a list of samples and sample categories for a given SampleCategory.
@ -167,7 +163,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
follow = new Button("Follow"); follow = new Button("Follow");
follow.setOnAction(evt -> { follow.setOnAction(evt -> {
setCursor(Cursor.WAIT); setCursor(Cursor.WAIT);
new Thread(new Task<Boolean>() { CompletableFuture.runAsync(new Task<Boolean>() {
@Override @Override
protected Boolean call() throws Exception { protected Boolean call() throws Exception {
model.getSite().login(); model.getSite().login();
@ -183,15 +179,15 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
} }
Platform.runLater(() -> setCursor(Cursor.DEFAULT)); Platform.runLater(() -> setCursor(Cursor.DEFAULT));
} }
}).start(); });
}); });
record = new Button("Record"); record = new Button("Record");
record.setOnAction(evt -> { record.setOnAction(evt -> {
setCursor(Cursor.WAIT); setCursor(Cursor.WAIT);
new Thread(new Task<Void>() { CompletableFuture.runAsync(new Task<Void>() {
@Override @Override
protected Void call() throws Exception { protected Void call() throws Exception {
recorder.startRecording(model); recorder.addModel(model);
return null; return null;
} }
@ -199,7 +195,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
protected void done() { protected void done() {
Platform.runLater(() -> setCursor(Cursor.DEFAULT)); Platform.runLater(() -> setCursor(Cursor.DEFAULT));
} }
}).start(); });
}); });
getChildren().addAll(thumb, title, follow, record); getChildren().addAll(thumb, title, follow, record);

View File

@ -123,6 +123,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private SimpleStringProperty path; private SimpleStringProperty path;
private SimpleStringProperty downloadFilename; private SimpleStringProperty downloadFilename;
private SimpleBooleanProperty requireAuthentication; private SimpleBooleanProperty requireAuthentication;
private SimpleBooleanProperty totalModelCountInTitle;
private SimpleBooleanProperty transportLayerSecurity; private SimpleBooleanProperty transportLayerSecurity;
private SimpleBooleanProperty fastScrollSpeed; private SimpleBooleanProperty fastScrollSpeed;
private ExclusiveSelectionProperty recordLocal; private ExclusiveSelectionProperty recordLocal;
@ -176,6 +177,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
downloadFilename = new SimpleStringProperty(null, "downloadFilename", settings.downloadFilename); downloadFilename = new SimpleStringProperty(null, "downloadFilename", settings.downloadFilename);
requireAuthentication = new SimpleBooleanProperty(null, "requireAuthentication", settings.requireAuthentication); requireAuthentication = new SimpleBooleanProperty(null, "requireAuthentication", settings.requireAuthentication);
requireAuthentication.addListener(this::requireAuthenticationChanged); requireAuthentication.addListener(this::requireAuthenticationChanged);
totalModelCountInTitle = new SimpleBooleanProperty(null, "totalModelCountInTitle", settings.totalModelCountInTitle);
transportLayerSecurity = new SimpleBooleanProperty(null, "transportLayerSecurity", settings.transportLayerSecurity); transportLayerSecurity = new SimpleBooleanProperty(null, "transportLayerSecurity", settings.transportLayerSecurity);
recordLocal = new ExclusiveSelectionProperty(null, "localRecording", settings.localRecording, "Local", "Remote"); recordLocal = new ExclusiveSelectionProperty(null, "localRecording", settings.localRecording, "Local", "Remote");
postProcessingThreads = new SimpleIntegerProperty(null, "postProcessingThreads", settings.postProcessingThreads); postProcessingThreads = new SimpleIntegerProperty(null, "postProcessingThreads", settings.postProcessingThreads);
@ -208,6 +210,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Setting.of("Add models from clipboard", monitorClipboard, "Monitor clipboard for model URLs and automatically add them to the recorder").needsRestart(), Setting.of("Add models from clipboard", monitorClipboard, "Monitor clipboard for model URLs and automatically add them to the recorder").needsRestart(),
Setting.of("Fast scroll speed", fastScrollSpeed, "Makes the thumbnail overviews scroll faster with the mouse wheel").needsRestart(), Setting.of("Fast scroll speed", fastScrollSpeed, "Makes the thumbnail overviews scroll faster with the mouse wheel").needsRestart(),
Setting.of("Show confirmation dialogs", confirmationDialogs, "Show confirmation dialogs for irreversible actions"), Setting.of("Show confirmation dialogs", confirmationDialogs, "Show confirmation dialogs for irreversible actions"),
Setting.of("Total model count in title", totalModelCountInTitle, "Show the total number of models in the title bar"),
Setting.of("Start Tab", startTab), Setting.of("Start Tab", startTab),
Setting.of("Colors (Base / Accent)", new ColorSettingsPane(Config.getInstance())).needsRestart() Setting.of("Colors (Base / Accent)", new ColorSettingsPane(Config.getInstance())).needsRestart()
), ),

View File

@ -33,7 +33,7 @@ import okhttp3.Response;
public class Cam4UpdateService extends PaginatedScheduledService { public class Cam4UpdateService extends PaginatedScheduledService {
private static final transient Logger LOG = LoggerFactory.getLogger(Cam4UpdateService.class); private static final Logger LOG = LoggerFactory.getLogger(Cam4UpdateService.class);
private String url; private String url;
private Cam4 site; private Cam4 site;
private boolean loginRequired; private boolean loginRequired;
@ -86,6 +86,7 @@ public class Cam4UpdateService extends PaginatedScheduledService {
String slug = path.substring(1); String slug = path.substring(1);
Cam4Model model = site.createModel(slug); Cam4Model model = site.createModel(slug);
String playlistUrl = profileLink.attr("data-hls-preview-url"); String playlistUrl = profileLink.attr("data-hls-preview-url");
model.setDisplayName(HtmlParser.getText(boxHtml, "div.profileBoxTitle a").trim());
model.setPlaylistUrl(playlistUrl); model.setPlaylistUrl(playlistUrl);
model.setPreview("https://snapshots.xcdnpro.com/thumbnails/" + model.getName() + "?s=" + System.currentTimeMillis()); model.setPreview("https://snapshots.xcdnpro.com/thumbnails/" + model.getName() + "?s=" + System.currentTimeMillis());
model.setDescription(parseDesription(boxHtml)); model.setDescription(parseDesription(boxHtml));

View File

@ -1,24 +1,5 @@
package ctbrec.ui.sites.camsoda; package ctbrec.ui.sites.camsoda;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.camsoda.Camsoda;
@ -32,14 +13,7 @@ import javafx.concurrent.Task;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Alert; import javafx.scene.control.*;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TitledPane;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
@ -48,6 +22,25 @@ import javafx.scene.text.Font;
import javafx.scene.text.FontWeight; import javafx.scene.text.FontWeight;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
public class CamsodaShowsTab extends Tab implements TabSelectionListener { public class CamsodaShowsTab extends Tab implements TabSelectionListener {
@ -136,7 +129,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
}); });
} }
}; };
new Thread(task).start(); CompletableFuture.runAsync(task);
} }
@Override @Override
@ -202,7 +195,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
private void follow(Model model) { private void follow(Model model) {
setCursor(Cursor.WAIT); setCursor(Cursor.WAIT);
new Thread(() -> { CompletableFuture.runAsync(() -> {
try { try {
SiteUiFactory.getUi(model.getSite()).login(); SiteUiFactory.getUi(model.getSite()).login();
model.follow(); model.follow();
@ -214,14 +207,14 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
setCursor(Cursor.DEFAULT); setCursor(Cursor.DEFAULT);
}); });
} }
}).start(); });
} }
private void record(Model model) { private void record(Model model) {
setCursor(Cursor.WAIT); setCursor(Cursor.WAIT);
new Thread(() -> { CompletableFuture.runAsync(() -> {
try { try {
recorder.startRecording(model); recorder.addModel(model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) { } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
showErrorDialog("Oh no!", "Couldn't add model to the recorder", "Recorder error: " + e.getMessage()); showErrorDialog("Oh no!", "Couldn't add model to the recorder", "Recorder error: " + e.getMessage());
} finally { } finally {
@ -229,11 +222,11 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
setCursor(Cursor.DEFAULT); setCursor(Cursor.DEFAULT);
}); });
} }
}).start(); });
} }
private void loadImage(Model model, ImageView thumb) { private void loadImage(Model model, ImageView thumb) {
new Thread(() -> { CompletableFuture.runAsync(() -> {
try { try {
String url = camsoda.getBaseUrl() + "/api/v1/user/" + model.getName(); String url = camsoda.getBaseUrl() + "/api/v1/user/" + model.getName();
Request detailRequest = new Request.Builder().url(url).build(); Request detailRequest = new Request.Builder().url(url).build();
@ -270,7 +263,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
} catch (Exception e) { } catch (Exception e) {
LOG.error("Couldn't load model details", e); LOG.error("Couldn't load model details", e);
} }
}).start(); });
} }
private Node createLabel(String string, boolean bold) { private Node createLabel(String string, boolean bold) {

View File

@ -30,12 +30,10 @@ public class LiveJasminFollowedUpdateService extends PaginatedScheduledService {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminFollowedUpdateService.class); private static final Logger LOG = LoggerFactory.getLogger(LiveJasminFollowedUpdateService.class);
private LiveJasmin liveJasmin; private LiveJasmin liveJasmin;
private String url; private String url;
private boolean showOnline = true;
public LiveJasminFollowedUpdateService(LiveJasmin liveJasmin) { public LiveJasminFollowedUpdateService(LiveJasmin liveJasmin) {
this.liveJasmin = liveJasmin; this.liveJasmin = liveJasmin;
this.url = liveJasmin.getBaseUrl() + "/en/member/favorite"; this.url = liveJasmin.getBaseUrl() + "/en/member/favorite";
//this.url = liveJasmin.getBaseUrl() + "/en/free/favourite/get-favourite-list?_dc=" + ts;
} }
@Override @Override
@ -49,9 +47,9 @@ public class LiveJasminFollowedUpdateService extends PaginatedScheduledService {
boolean loggedIn = SiteUiFactory.getUi(liveJasmin).login(); boolean loggedIn = SiteUiFactory.getUi(liveJasmin).login();
if (!loggedIn) { if (!loggedIn) {
throw new RuntimeException("Couldn't login to livejasmin"); throw new NotLoggedInExcetion("Couldn't login to livejasmin");
} }
//LOG.debug("Fetching page {}", url); LOG.debug("Fetching page {}", url);
Request request = new Request.Builder() Request request = new Request.Builder()
.url(url) .url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
@ -80,8 +78,4 @@ public class LiveJasminFollowedUpdateService extends PaginatedScheduledService {
} }
}; };
} }
public void setShowOnline(boolean showOnline) {
this.showOnline = showOnline;
}
} }

View File

@ -45,6 +45,7 @@ import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.action.FollowAction; import ctbrec.ui.action.FollowAction;
import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.StartRecordingAction; import ctbrec.ui.action.StartRecordingAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.SearchBox; import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.tabs.TabSelectionListener; import ctbrec.ui.tabs.TabSelectionListener;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
@ -341,7 +342,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
MenuItem follow = new MenuItem("Follow"); MenuItem follow = new MenuItem("Follow");
follow.setOnAction(e -> new FollowAction(getTabPane(), selectedModels).execute()); follow.setOnAction(e -> new FollowAction(getTabPane(), selectedModels).execute());
ContextMenu menu = new ContextMenu(); ContextMenu menu = new CustomMouseBehaviorContextMenu();
menu.getItems().addAll(startRecording, copyUrl, openInPlayer, openInBrowser, follow); menu.getItems().addAll(startRecording, copyUrl, openInPlayer, openInBrowser, follow);
if (selectedModels.size() > 1) { if (selectedModels.size() > 1) {
@ -467,7 +468,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
} }
private void showColumnSelection(ActionEvent evt) { private void showColumnSelection(ActionEvent evt) {
ContextMenu menu = new ContextMenu(); ContextMenu menu = new CustomMouseBehaviorContextMenu();
for (TableColumn<ModelTableRow, ?> tc : columns) { for (TableColumn<ModelTableRow, ?> tc : columns) {
CheckMenuItem item = new CheckMenuItem(tc.getText()); CheckMenuItem item = new CheckMenuItem(tc.getText());
item.setSelected(isColumnEnabled(tc)); item.setSelected(isColumnEnabled(tc));

View File

@ -14,6 +14,7 @@ import org.json.JSONObject;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.UnexpectedResponseException;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.sites.showup.Showup; import ctbrec.sites.showup.Showup;
import ctbrec.sites.showup.ShowupModel; import ctbrec.sites.showup.ShowupModel;
@ -55,7 +56,7 @@ public class ShowupFollowedUpdateService extends PaginatedScheduledService {
.filter(m -> onlineModels.containsKey(m.getName()) == showOnline) .filter(m -> onlineModels.containsKey(m.getName()) == showOnline)
.collect(Collectors.toList()); .collect(Collectors.toList());
} else { } else {
throw new RuntimeException("Request was not successful: " + body); throw new UnexpectedResponseException("Request was not successful: " + body);
} }
} else { } else {
throw new HttpException(response.code(), response.message()); throw new HttpException(response.code(), response.message());
@ -83,7 +84,7 @@ public class ShowupFollowedUpdateService extends PaginatedScheduledService {
for (int i = 0; i < online.length(); i++) { for (int i = 0; i < online.length(); i++) {
JSONObject m = online.getJSONObject(i); JSONObject m = online.getJSONObject(i);
String preview = site.getBaseUrl() + "/files/" + m.optString("big_img") + ".jpg"; String preview = site.getBaseUrl() + "/files/" + m.optString("big_img") + ".jpg";
//onlineModels.put(m.optLong("uid"), preview); onlineModels.put(String.valueOf(m.optLong("uid")), preview);
} }
return onlineModels; return onlineModels;
} }

View File

@ -0,0 +1,607 @@
package ctbrec.ui.tabs;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.JavaFxModel;
import ctbrec.ui.PreviewPopupHandler;
import ctbrec.ui.action.*;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.controls.autocomplete.AutoFillTextField;
import ctbrec.ui.controls.autocomplete.ObservableListSuggester;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringPropertyBase;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.util.Callback;
import javafx.util.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
public class RecordLaterTab extends Tab implements TabSelectionListener {
private static final Logger LOG = LoggerFactory.getLogger(RecordLaterTab.class);
private ReentrantLock lock = new ReentrantLock();
private ScheduledService<List<JavaFxModel>> updateService;
private Recorder recorder;
private List<Site> sites;
FlowPane grid = new FlowPane();
ScrollPane scrollPane = new ScrollPane();
TableView<JavaFxModel> table = new TableView<>();
ObservableList<JavaFxModel> observableModels = FXCollections.observableArrayList();
ObservableList<JavaFxModel> filteredModels = FXCollections.observableArrayList();
ContextMenu popup;
Label modelLabel = new Label("Model");
AutoFillTextField model;
Button addModelButton = new Button("Record");
Button checkModelAccountExistance = new Button("Check URLs");
TextField filter;
public RecordLaterTab(String title, Recorder recorder, List<Site> sites) {
super(title);
this.recorder = recorder;
this.sites = sites;
createGui();
setClosable(false);
initializeUpdateService();
}
@SuppressWarnings("unchecked")
private void createGui() {
grid.setPadding(new Insets(5));
grid.setHgap(5);
grid.setVgap(5);
scrollPane.setContent(grid);
scrollPane.setFitToHeight(true);
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<JavaFxModel> row = new TableRow<>();
row.addEventHandler(MouseEvent.ANY, previewPopupHandler);
return row;
});
TableColumn<JavaFxModel, String> preview = new TableColumn<>("🎥");
preview.setPrefWidth(35);
preview.setCellValueFactory(cdf -> new SimpleStringProperty(""));
preview.setEditable(false);
preview.setId("preview");
if (!Config.getInstance().getSettings().livePreviews) {
preview.setVisible(false);
}
TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
name.setPrefWidth(200);
name.setCellValueFactory(new PropertyValueFactory<>("displayName"));
name.setCellFactory(new ClickableCellFactory<>());
name.setEditable(false);
name.setId("name");
TableColumn<JavaFxModel, String> url = new TableColumn<>("URL");
url.setCellValueFactory(new PropertyValueFactory<>("url"));
url.setCellFactory(new ClickableCellFactory<>());
url.setPrefWidth(400);
url.setEditable(false);
url.setId("url");
TableColumn<JavaFxModel, String> notes = new TableColumn<>("Notes");
notes.setCellValueFactory(cdf -> {
JavaFxModel m = cdf.getValue();
return new StringPropertyBase() {
@Override
public String getName() {
return "Model Notes";
}
@Override
public Object getBean() {
return null;
}
@Override
public String get() {
String modelNotes = Config.getInstance().getModelNotes(m);
return modelNotes;
}
};
});
notes.setPrefWidth(400);
notes.setEditable(false);
notes.setId("notes");
table.getColumns().addAll(preview, name, url, notes);
table.setItems(observableModels);
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
popup = createContextMenu();
if (popup != null) {
popup.show(table, event.getScreenX(), event.getScreenY());
}
event.consume();
});
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (popup != null) {
popup.hide();
}
});
table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
List<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
if (event.getCode() == KeyCode.DELETE) {
stopAction(selectedModels);
} else {
jumpToNextModel(event.getCode());
}
});
scrollPane.setContent(table);
HBox addModelBox = new HBox(5);
modelLabel.setPadding(new Insets(5, 0, 0, 0));
ObservableList<String> suggestions = FXCollections.observableArrayList();
sites.forEach(site -> suggestions.add(site.getClass().getSimpleName()));
model = new AutoFillTextField(new ObservableListSuggester(suggestions));
model.minWidth(150);
model.prefWidth(600);
HBox.setHgrow(model, Priority.ALWAYS);
model.setPromptText("e.g. MyFreeCams:ModelName or an URL like https://chaturbate.com/modelname/");
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(this::addModel);
addModelButton.setPadding(new Insets(5));
addModelBox.getChildren().addAll(modelLabel, model, addModelButton, checkModelAccountExistance);
checkModelAccountExistance.setPadding(new Insets(5));
checkModelAccountExistance.setTooltip(new Tooltip("Go over all model URLs and check, if the account still exists"));
HBox.setMargin(checkModelAccountExistance, new Insets(0, 0, 0, 20));
checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder)
.execute(Model::isMarkedForLaterRecording));
HBox filterContainer = new HBox();
filterContainer.setSpacing(0);
filterContainer.setPadding(new Insets(0));
filterContainer.setAlignment(Pos.CENTER_RIGHT);
filterContainer.minWidth(100);
filterContainer.prefWidth(150);
HBox.setHgrow(filterContainer, Priority.ALWAYS);
filter = new SearchBox(false);
filter.minWidth(100);
filter.prefWidth(150);
filter.setPromptText("Filter");
filter.textProperty().addListener((observableValue, oldValue, newValue) -> {
String q = filter.getText();
lock.lock();
try {
filter(q);
} finally {
lock.unlock();
}
});
filter.getStyleClass().remove("search-box-icon");
filterContainer.getChildren().add(filter);
addModelBox.getChildren().add(filterContainer);
BorderPane root = new BorderPane();
root.setPadding(new Insets(5));
root.setTop(addModelBox);
root.setCenter(scrollPane);
setContent(root);
restoreState();
}
private void jumpToNextModel(KeyCode code) {
if (!table.getItems().isEmpty() && (code.isLetterKey() || code.isDigitKey())) {
// determine where to start looking for the next model
int startAt = 0;
if (table.getSelectionModel().getSelectedIndex() >= 0) {
startAt = table.getSelectionModel().getSelectedIndex() + 1;
if (startAt >= table.getItems().size()) {
startAt = 0;
}
}
String c = code.getChar().toLowerCase();
int i = startAt;
do {
JavaFxModel current = table.getItems().get(i);
if (current.getName().toLowerCase().replaceAll("[^0-9a-z]", "").startsWith(c)) {
table.getSelectionModel().clearAndSelect(i);
table.scrollTo(i);
break;
}
i++;
if (i >= table.getItems().size()) {
i = 0;
}
} while (i != startAt);
}
}
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 newModel = site.createModelFromUrl(url);
if (newModel != null) {
try {
newModel.setMarkedForLaterRecording(true);
recorder.addModel(newModel);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + newModel.getName() + " could not be added: ", e1);
}
return;
}
}
Dialogs.showError(getTabPane().getScene(), "Unknown URL format",
"The URL you entered has an unknown format or the function does not support this site, yet", null);
}
private void addModelByName(String siteModelCombo) {
String[] parts = siteModelCombo.trim().split(":");
if (parts.length != 2) {
Dialogs.showError(getTabPane().getScene(), "Wrong input format", "Use something like \"MyFreeCams:ModelName\"", null);
return;
}
String siteName = parts[0];
String modelName = parts[1];
for (Site site : sites) {
if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) {
try {
Model m = site.createModel(modelName);
m.setMarkedForLaterRecording(true);
recorder.addModel(m);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + modelName + " could not be added:", e1);
}
return;
}
}
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getTabPane().getScene());
alert.setTitle("Unknown site");
alert.setHeaderText("Couldn't add model");
alert.setContentText("The site you entered is unknown");
alert.showAndWait();
}
void initializeUpdateService() {
updateService = createUpdateService();
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
updateService.setOnSucceeded(this::onUpdateSuccess);
updateService.setOnFailed(event -> LOG.info("Couldn't get list of models from recorder", event.getSource().getException()));
}
private void onUpdateSuccess(WorkerStateEvent event) {
List<JavaFxModel> updatedModels = updateService.getValue();
if (updatedModels == null) {
return;
}
lock.lock();
try {
addOrUpdateModels(updatedModels);
// remove old ones, which are not in the list of updated models
for (Iterator<JavaFxModel> iterator = observableModels.iterator(); iterator.hasNext();) {
Model oldModel = iterator.next();
if (!updatedModels.contains(oldModel)) {
iterator.remove();
}
}
} finally {
lock.unlock();
}
filteredModels.clear();
filter(filter.getText());
table.sort();
}
private void addOrUpdateModels(List<JavaFxModel> updatedModels) {
for (JavaFxModel updatedModel : updatedModels) {
int index = observableModels.indexOf(updatedModel);
if (index == -1) {
observableModels.add(updatedModel);
} else {
// make sure to update the JavaFX online property, so that the table cell is updated
JavaFxModel oldModel = observableModels.get(index);
oldModel.setSuspended(updatedModel.isSuspended());
oldModel.getOnlineProperty().set(updatedModel.getOnlineProperty().get());
oldModel.getRecordingProperty().set(updatedModel.getRecordingProperty().get());
oldModel.lastRecordedProperty().set(updatedModel.lastRecordedProperty().get());
oldModel.lastSeenProperty().set(updatedModel.lastSeenProperty().get());
}
}
}
private void filter(String filter) {
lock.lock();
try {
if (StringUtil.isBlank(filter)) {
observableModels.addAll(filteredModels);
filteredModels.clear();
return;
}
String[] tokens = filter.split(" ");
observableModels.addAll(filteredModels);
filteredModels.clear();
for (int i = 0; i < table.getItems().size(); i++) {
StringBuilder sb = new StringBuilder();
for (TableColumn<JavaFxModel, ?> tc : table.getColumns()) {
Object cellData = tc.getCellData(i);
if (cellData != null) {
String content = cellData.toString();
sb.append(content).append(' ');
}
}
String searchText = sb.toString();
boolean tokensMissing = false;
for (String token : tokens) {
if (!searchText.toLowerCase().contains(token.toLowerCase())) {
tokensMissing = true;
break;
}
}
if (tokensMissing) {
JavaFxModel filteredModel = table.getItems().get(i);
filteredModels.add(filteredModel);
}
}
observableModels.removeAll(filteredModels);
} finally {
lock.unlock();
}
}
private ScheduledService<List<JavaFxModel>> createUpdateService() {
ScheduledService<List<JavaFxModel>> modelUpdateService = new ScheduledService<List<JavaFxModel>>() {
@Override
protected Task<List<JavaFxModel>> createTask() {
return new Task<List<JavaFxModel>>() {
@Override
public List<JavaFxModel> call() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
LOG.trace("Updating models marked for later recording");
return recorder.getModels().stream().filter(Model::isMarkedForLaterRecording).map(JavaFxModel::new).collect(Collectors.toList());
}
};
}
};
ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("RecordLaterTab UpdateService");
return t;
});
modelUpdateService.setExecutor(executor);
return modelUpdateService;
}
@Override
public void selected() {
if (updateService != null) {
updateService.reset();
updateService.restart();
}
}
@Override
public void deselected() {
if (updateService != null) {
updateService.cancel();
}
}
private ContextMenu createContextMenu() {
ObservableList<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
if (selectedModels.isEmpty()) {
return null;
}
MenuItem start = new MenuItem("Start Recording");
start.setOnAction(e -> startAction(selectedModels));
MenuItem stop = new MenuItem("Remove Model");
stop.setOnAction(e -> stopAction(selectedModels));
MenuItem copyUrl = new MenuItem("Copy URL");
copyUrl.setOnAction(e -> {
Model selected = selectedModels.get(0);
final Clipboard clipboard = Clipboard.getSystemClipboard();
final ClipboardContent content = new ClipboardContent();
content.putString(selected.getUrl());
clipboard.setContent(content);
});
MenuItem openInBrowser = new MenuItem("Open in Browser");
openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl()));
MenuItem openInPlayer = new MenuItem("Open in Player");
openInPlayer.setOnAction(e -> openInPlayer(selectedModels.get(0)));
MenuItem follow = new MenuItem("Follow");
follow.setOnAction(e -> follow(selectedModels));
follow.setDisable(!selectedModels.stream().allMatch(m -> m.getSite().supportsFollow() && m.getSite().credentialsAvailable()));
MenuItem ignore = new MenuItem("Ignore");
ignore.setOnAction(e -> ignore(selectedModels));
MenuItem notes = new MenuItem("Notes");
notes.setOnAction(e -> notes(selectedModels));
ContextMenu menu = new CustomMouseBehaviorContextMenu(start, stop, copyUrl, openInPlayer, openInBrowser, follow, notes, ignore);
if (selectedModels.size() > 1) {
copyUrl.setDisable(true);
openInPlayer.setDisable(true);
openInBrowser.setDisable(true);
notes.setDisable(true);
}
return menu;
}
private void ignore(ObservableList<JavaFxModel> selectedModels) {
new IgnoreModelsAction(table, selectedModels, recorder, true).execute();
}
private void follow(ObservableList<JavaFxModel> selectedModels) {
new FollowAction(getTabPane(), new ArrayList<>(selectedModels)).execute();
}
private void notes(ObservableList<JavaFxModel> selectedModels) {
new EditNotesAction(getTabPane(), selectedModels.get(0), table).execute();
}
private void openInPlayer(JavaFxModel selectedModel) {
new PlayAction(getTabPane(), selectedModel).execute();
}
private void startAction(List<JavaFxModel> selectedModels) {
List<Model> models = selectedModels.stream().map(JavaFxModel::getDelegate).collect(Collectors.toList());
new ResumeAction(table, models, recorder).execute();
}
private void stopAction(List<JavaFxModel> selectedModels) {
boolean confirmed = true;
if (Config.getInstance().getSettings().confirmationForDangerousActions) {
int n = selectedModels.size();
String plural = n > 1 ? "s" : "";
String header = "This will remove " + n + " model" + plural;
confirmed = Dialogs.showConfirmDialog("Remove From List", "Continue?", header, table.getScene());
}
if (confirmed) {
List<Model> models = selectedModels.stream().map(JavaFxModel::getDelegate).collect(Collectors.toList());
new StopRecordingAction(getTabPane(), models, recorder).execute(m -> Platform.runLater(() -> {
table.getSelectionModel().clearSelection(table.getItems().indexOf(m));
table.getItems().remove(m);
}));
}
}
public void saveState() {
if (!table.getSortOrder().isEmpty()) {
TableColumn<JavaFxModel, ?> col = table.getSortOrder().get(0);
Config.getInstance().getSettings().recordLaterSortColumn = col.getText();
Config.getInstance().getSettings().recordLaterSortType = col.getSortType().toString();
}
int columns = table.getColumns().size();
double[] columnWidths = new double[columns];
String[] columnIds = new String[columns];
for (int i = 0; i < columnWidths.length; i++) {
columnWidths[i] = table.getColumns().get(i).getWidth();
columnIds[i] = table.getColumns().get(i).getId();
}
Config.getInstance().getSettings().recordLaterColumnWidths = columnWidths;
Config.getInstance().getSettings().recordLaterColumnIds = columnIds;
}
private void restoreState() {
restoreColumnOrder();
restoreColumnWidths();
restoreSorting();
}
private void restoreSorting() {
String sortCol = Config.getInstance().getSettings().recordLaterSortColumn;
if (StringUtil.isNotBlank(sortCol)) {
for (TableColumn<JavaFxModel, ?> col : table.getColumns()) {
if (Objects.equals(sortCol, col.getText())) {
col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordLaterSortType));
table.getSortOrder().clear();
table.getSortOrder().add(col);
break;
}
}
}
}
private void restoreColumnOrder() {
String[] columnIds = Config.getInstance().getSettings().recordLaterColumnIds;
ObservableList<TableColumn<JavaFxModel,?>> columns = table.getColumns();
for (int i = 0; i < columnIds.length; i++) {
for (int j = 0; j < table.getColumns().size(); j++) {
if(Objects.equals(columnIds[i], columns.get(j).getId())) {
TableColumn<JavaFxModel, ?> col = columns.get(j);
columns.remove(j); // NOSONAR
columns.add(i, col);
}
}
}
}
private void restoreColumnWidths() {
double[] columnWidths = Config.getInstance().getSettings().recordLaterColumnWidths;
if (columnWidths != null && columnWidths.length == table.getColumns().size()) {
for (int i = 0; i < columnWidths.length; i++) {
table.getColumns().get(i).setPrefWidth(columnWidths[i]);
}
}
}
private class ClickableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
@Override
public TableCell<S, T> call(TableColumn<S, T> param) {
TableCell<S, T> cell = new TableCell<>() {
@Override
protected void updateItem(Object item, boolean empty) {
setText(empty ? "" : Objects.toString(item));
}
};
cell.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem();
if (selectedModel != null) {
new PlayAction(table, selectedModel).execute();
}
}
});
return cell;
}
}
}

View File

@ -1,57 +1,14 @@
package ctbrec.ui.tabs; package ctbrec.ui.tabs;
import static ctbrec.Recording.State.*;
import static ctbrec.ui.UnicodeEmoji.*;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.StringUtil; import ctbrec.StringUtil;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import ctbrec.ui.AutosizeAlert; import ctbrec.ui.*;
import ctbrec.ui.DesktopIntegration; import ctbrec.ui.action.*;
import ctbrec.ui.JavaFxModel; import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.PreviewPopupHandler;
import ctbrec.ui.StreamSourceSelectionDialog;
import ctbrec.ui.UiUtils;
import ctbrec.ui.action.CheckModelAccountAction;
import ctbrec.ui.action.EditNotesAction;
import ctbrec.ui.action.FollowAction;
import ctbrec.ui.action.IgnoreModelsAction;
import ctbrec.ui.action.OpenRecordingsDir;
import ctbrec.ui.action.PauseAction;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.RemoveTimeLimitAction;
import ctbrec.ui.action.ResumeAction;
import ctbrec.ui.action.SetStopDateAction;
import ctbrec.ui.action.StopRecordingAction;
import ctbrec.ui.action.ToggleRecordingAction;
import ctbrec.ui.controls.DateTimeCellFactory; import ctbrec.ui.controls.DateTimeCellFactory;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.controls.SearchBox; import ctbrec.ui.controls.SearchBox;
@ -69,33 +26,13 @@ import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Alert; import javafx.scene.control.*;
import javafx.scene.control.Button;
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.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent; import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableColumn.SortType; import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.CheckBoxTableCell; import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell; import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.input.Clipboard; import javafx.scene.input.*;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
@ -104,6 +41,30 @@ import javafx.util.Callback;
import javafx.util.Duration; import javafx.util.Duration;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import javafx.util.converter.NumberStringConverter; import javafx.util.converter.NumberStringConverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static ctbrec.Recording.State.RECORDING;
import static ctbrec.ui.UnicodeEmoji.CLOCK;
import static ctbrec.ui.UnicodeEmoji.HEAVY_CHECK_MARK;
public class RecordedModelsTab extends Tab implements TabSelectionListener { public class RecordedModelsTab extends Tab implements TabSelectionListener {
private static final Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class); private static final Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class);
@ -297,7 +258,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
checkModelAccountExistance.setPadding(new Insets(5)); checkModelAccountExistance.setPadding(new Insets(5));
checkModelAccountExistance.setTooltip(new Tooltip("Go over all model URLs and check, if the account still exists")); checkModelAccountExistance.setTooltip(new Tooltip("Go over all model URLs and check, if the account still exists"));
HBox.setMargin(checkModelAccountExistance, new Insets(0, 0, 0, 20)); HBox.setMargin(checkModelAccountExistance, new Insets(0, 0, 0, 20));
checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder).execute()); checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder)
.execute(Predicate.not(Model::isMarkedForLaterRecording)));
HBox filterContainer = new HBox(); HBox filterContainer = new HBox();
filterContainer.setSpacing(0); filterContainer.setSpacing(0);
@ -404,7 +366,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
Model newModel = site.createModelFromUrl(url); Model newModel = site.createModelFromUrl(url);
if (newModel != null) { if (newModel != null) {
try { try {
recorder.startRecording(newModel); recorder.addModel(newModel);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + newModel.getName() + " could not be added: ", e1); Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + newModel.getName() + " could not be added: ", e1);
} }
@ -429,7 +391,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) { if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) {
try { try {
Model m = site.createModel(modelName); Model m = site.createModel(modelName);
recorder.startRecording(m); recorder.addModel(m);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + modelName + " could not be added:", e1); Dialogs.showError(getTabPane().getScene(), "Couldn't add model", "The model " + modelName + " could not be added:", e1);
} }
@ -584,6 +546,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
List<Model> onlineModels = recorder.getOnlineModels(); List<Model> onlineModels = recorder.getOnlineModels();
return recorder.getModels() return recorder.getModels()
.stream() .stream()
.filter(Predicate.not(Model::isMarkedForLaterRecording))
.map(JavaFxModel::new) .map(JavaFxModel::new)
.peek(fxm -> { // NOSONAR .peek(fxm -> { // NOSONAR
for (Recording recording : recordings) { for (Recording recording : recordings) {
@ -641,6 +604,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
} }
MenuItem stop = new MenuItem("Remove Model"); MenuItem stop = new MenuItem("Remove Model");
stop.setOnAction(e -> stopAction(selectedModels)); stop.setOnAction(e -> stopAction(selectedModels));
MenuItem recordLater = new MenuItem("Record Later");
recordLater.setOnAction(e -> recordLater(selectedModels));
MenuItem copyUrl = new MenuItem("Copy URL"); MenuItem copyUrl = new MenuItem("Copy URL");
copyUrl.setOnAction(e -> { copyUrl.setOnAction(e -> {
@ -675,8 +640,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
MenuItem openRecDir = new MenuItem("Open recording directory"); MenuItem openRecDir = new MenuItem("Open recording directory");
openRecDir.setOnAction(e -> new OpenRecordingsDir(table, selectedModels.get(0)).execute()); openRecDir.setOnAction(e -> new OpenRecordingsDir(table, selectedModels.get(0)).execute());
ContextMenu menu = new ContextMenu(stop); ContextMenu menu = new CustomMouseBehaviorContextMenu(stop, recordLater);
UiUtils.disableRightClickFor(menu);
if (selectedModels.size() == 1) { if (selectedModels.size() == 1) {
menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording); menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording);
menu.getItems().add(stopRecordingAt); menu.getItems().add(stopRecordingAt);
@ -773,7 +737,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
alert.showAndWait(); alert.showAndWait();
} }
private void stopAction(List<JavaFxModel> selectedModels) { private boolean stopAction(List<JavaFxModel> selectedModels) {
boolean confirmed = true; boolean confirmed = true;
if (Config.getInstance().getSettings().confirmationForDangerousActions) { if (Config.getInstance().getSettings().confirmationForDangerousActions) {
int n = selectedModels.size(); int n = selectedModels.size();
@ -788,6 +752,21 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
table.getItems().remove(m); table.getItems().remove(m);
})); }));
} }
return confirmed;
}
private void recordLater(List<JavaFxModel> selectedModels) {
boolean confirmed = stopAction(selectedModels);
if (confirmed) {
List<Model> models = selectedModels.stream()
.map(JavaFxModel::getDelegate)
.map(m -> {
m.setMarkedForLaterRecording(true);
return m;
})
.collect(Collectors.toList());
new StartRecordingAction(table, models, recorder).execute();
}
} }
private void pauseRecording(List<JavaFxModel> selectedModels) { private void pauseRecording(List<JavaFxModel> selectedModels) {

View File

@ -0,0 +1,61 @@
package ctbrec.ui.tabs;
import java.util.List;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Side;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
public class RecordedTab extends Tab implements TabSelectionListener {
private TabPane tabPane;
private RecordedModelsTab recordedModelsTab;
private RecordLaterTab recordLaterTab;
public RecordedTab(Recorder recorder, List<Site> sites) {
super("Recording");
setClosable(false);
recordedModelsTab = new RecordedModelsTab("Active", recorder, sites);
recordLaterTab = new RecordLaterTab("Later", recorder, sites);
tabPane = new TabPane();
tabPane.setSide(Side.LEFT);
tabPane.getTabs().addAll(recordedModelsTab, recordLaterTab);
setContent(tabPane);
// register changelistener to activate / deactivate tabs, when the user switches between them
tabPane.getSelectionModel().selectedItemProperty().addListener((ChangeListener<Tab>) (ov, from, to) -> {
if (from instanceof TabSelectionListener) {
((TabSelectionListener) from).deselected();
}
if (to instanceof TabSelectionListener) {
((TabSelectionListener) to).selected();
}
});
}
@Override
public void selected() {
Tab selectedTab = tabPane.getSelectionModel().getSelectedItem();
if(selectedTab instanceof TabSelectionListener) {
((TabSelectionListener) selectedTab).selected();
}
}
@Override
public void deselected() {
Tab selectedTab = tabPane.getSelectionModel().getSelectedItem();
if(selectedTab instanceof TabSelectionListener) {
((TabSelectionListener) selectedTab).deselected();
}
}
public void saveState() {
recordedModelsTab.saveState();
recordLaterTab.saveState();
}
}

View File

@ -45,11 +45,11 @@ import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.FileDownload; import ctbrec.ui.FileDownload;
import ctbrec.ui.JavaFxRecording; import ctbrec.ui.JavaFxRecording;
import ctbrec.ui.Player; import ctbrec.ui.Player;
import ctbrec.ui.UiUtils;
import ctbrec.ui.action.FollowAction; import ctbrec.ui.action.FollowAction;
import ctbrec.ui.action.PauseAction; import ctbrec.ui.action.PauseAction;
import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.StopRecordingAction; import ctbrec.ui.action.StopRecordingAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.DateTimeCellFactory; import ctbrec.ui.controls.DateTimeCellFactory;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.controls.Toast; import ctbrec.ui.controls.Toast;
@ -389,11 +389,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
} }
private ContextMenu createContextMenu(List<JavaFxRecording> recordings) { private ContextMenu createContextMenu(List<JavaFxRecording> recordings) {
ContextMenu contextMenu = new ContextMenu(); ContextMenu contextMenu = new CustomMouseBehaviorContextMenu();
contextMenu.setHideOnEscape(true); contextMenu.setHideOnEscape(true);
contextMenu.setAutoHide(true); contextMenu.setAutoHide(true);
contextMenu.setAutoFix(true); contextMenu.setAutoFix(true);
UiUtils.disableRightClickFor(contextMenu);
JavaFxRecording first = recordings.get(0); JavaFxRecording first = recordings.get(0);
MenuItem openInPlayer = new MenuItem("Open in Player"); MenuItem openInPlayer = new MenuItem("Open in Player");

View File

@ -511,12 +511,13 @@ public class ThumbCell extends StackPane {
new Thread(() -> { new Thread(() -> {
try { try {
if (start) { if (start) {
recorder.startRecording(model); recorder.addModel(model);
setRecording(true); setRecording(!model.isMarkedForLaterRecording());
} else { } else {
recorder.stopRecording(model); recorder.stopRecording(model);
setRecording(false); setRecording(false);
} }
update();
} catch (Exception e1) { } catch (Exception e1) {
LOG.error(COULDNT_START_STOP_RECORDING, e1); LOG.error(COULDNT_START_STOP_RECORDING, e1);
Dialogs.showError(getScene(), COULDNT_START_STOP_RECORDING, "I/O error while starting/stopping the recording: ", e1); Dialogs.showError(getScene(), COULDNT_START_STOP_RECORDING, "I/O error while starting/stopping the recording: ", e1);
@ -561,6 +562,11 @@ public class ThumbCell extends StackPane {
}); });
} }
void recordLater() {
model.setMarkedForLaterRecording(true);
startStopAction(true);
}
public Model getModel() { public Model getModel() {
return model; return model;
} }
@ -684,6 +690,7 @@ public class ThumbCell extends StackPane {
void addInPausedState() { void addInPausedState() {
model.setSuspended(true); model.setSuspended(true);
model.setMarkedForLaterRecording(false);
startStopAction(true); startStopAction(true);
} }
} }

View File

@ -1,31 +1,5 @@
package ctbrec.ui.tabs; package ctbrec.ui.tabs;
import static ctbrec.ui.controls.Dialogs.*;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.event.EventBusHolder; import ctbrec.event.EventBusHolder;
@ -33,24 +7,12 @@ import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import ctbrec.sites.mfc.MyFreeCamsClient; import ctbrec.sites.mfc.MyFreeCamsClient;
import ctbrec.sites.mfc.MyFreeCamsModel; import ctbrec.sites.mfc.MyFreeCamsModel;
import ctbrec.ui.AutosizeAlert; import ctbrec.ui.*;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.TipDialog;
import ctbrec.ui.TokenLabel;
import ctbrec.ui.UiUtils;
import ctbrec.ui.action.IgnoreModelsAction; import ctbrec.ui.action.IgnoreModelsAction;
import ctbrec.ui.action.OpenRecordingsDir; import ctbrec.ui.action.OpenRecordingsDir;
import ctbrec.ui.action.SetStopDateAction; import ctbrec.ui.action.SetStopDateAction;
import ctbrec.ui.controls.FasterVerticalScrollPaneSkin; import ctbrec.ui.controls.*;
import ctbrec.ui.controls.SearchBox; import javafx.animation.*;
import ctbrec.ui.controls.SearchPopover;
import ctbrec.ui.controls.SearchPopoverTreeList;
import javafx.animation.FadeTransition;
import javafx.animation.Interpolator;
import javafx.animation.ParallelTransition;
import javafx.animation.ScaleTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -66,33 +28,24 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.Parent; import javafx.scene.Parent;
import javafx.scene.control.Alert; import javafx.scene.control.*;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.Clipboard; import javafx.scene.input.*;
import javafx.scene.input.ClipboardContent; import javafx.scene.layout.*;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.transform.Transform; import javafx.scene.transform.Transform;
import javafx.util.Duration; import javafx.util.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import static ctbrec.ui.controls.Dialogs.showError;
public class ThumbOverviewTab extends Tab implements TabSelectionListener { public class ThumbOverviewTab extends Tab implements TabSelectionListener {
private static final Logger LOG = LoggerFactory.getLogger(ThumbOverviewTab.class); private static final Logger LOG = LoggerFactory.getLogger(ThumbOverviewTab.class);
@ -123,6 +76,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
SearchPopoverTreeList popoverTreeList = new SearchPopoverTreeList(); SearchPopoverTreeList popoverTreeList = new SearchPopoverTreeList();
double imageAspectRatio = 3.0 / 4.0; double imageAspectRatio = 3.0 / 4.0;
private final SimpleBooleanProperty preserveAspectRatio = new SimpleBooleanProperty(true); private final SimpleBooleanProperty preserveAspectRatio = new SimpleBooleanProperty(true);
ProgressIndicator progressIndicator;
Label noResultsFound = new Label("Nothing found!");
private ComboBox<Integer> thumbWidth; private ComboBox<Integer> thumbWidth;
@ -140,6 +95,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
grid.setHgap(5); grid.setHgap(5);
grid.setVgap(5); grid.setVgap(5);
progressIndicator = new ProgressIndicator();
progressIndicator.setPrefSize(100, 100);
SearchBox filterInput = new SearchBox(false); SearchBox filterInput = new SearchBox(false);
filterInput.setPromptText("Filter models on this page"); filterInput.setPromptText("Filter models on this page");
filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> { filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> {
@ -361,6 +319,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
gridLock.lock(); gridLock.lock();
try { try {
ObservableList<Node> nodes = grid.getChildren(); ObservableList<Node> nodes = grid.getChildren();
nodes.remove(progressIndicator);
nodes.remove(noResultsFound);
// first remove models, which are not in the updated list // first remove models, which are not in the updated list
removeModelsMissingInUpdate(nodes, models); removeModelsMissingInUpdate(nodes, models);
@ -374,6 +334,11 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
// move models, which are tracked by the recorder to the front // move models, which are tracked by the recorder to the front
moveActiveRecordingsToFront(); moveActiveRecordingsToFront();
// show "empty" label, if grid is still empty
if (grid.getChildren().isEmpty()) {
nodes.add(noResultsFound);
}
} finally { } finally {
gridLock.unlock(); gridLock.unlock();
} }
@ -460,28 +425,29 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
private ContextMenu createContextMenu(ThumbCell cell) { private ContextMenu createContextMenu(ThumbCell cell) {
Model model = cell.getModel(); Model model = cell.getModel();
boolean modelIsTrackedByRecorder = recorder.isTracked(model); boolean modelIsTrackedByRecorder = recorder.isTracked(model);
MenuItem openInPlayer = new MenuItem("Open in Player"); MenuItem openInPlayer = new MenuItem("Open in Player");
openInPlayer.setOnAction(e -> startPlayer(getSelectedThumbCells(cell))); openInPlayer.setOnAction(e -> startPlayer(getSelectedThumbCells(cell)));
MenuItem start = new MenuItem("Start Recording"); MenuItem start = new MenuItem("Start Recording");
start.setOnAction(e -> startStopAction(e, getSelectedThumbCells(cell), true)); start.setOnAction(e -> startStopAction(getSelectedThumbCells(cell), true));
MenuItem stop = new MenuItem("Stop Recording"); MenuItem stop = new MenuItem("Stop Recording");
stop.setOnAction(e -> startStopAction(e, getSelectedThumbCells(cell), false)); stop.setOnAction(e -> startStopAction(getSelectedThumbCells(cell), false));
MenuItem startStop = recorder.isTracked(cell.getModel()) ? stop : start; MenuItem startStop = recorder.isTracked(model) ? stop : start;
MenuItem recordUntil = new MenuItem("Start Recording Until"); MenuItem recordUntil = new MenuItem("Start Recording Until");
recordUntil.setOnAction(e -> startRecordingWithTimeLimit(getSelectedThumbCells(cell))); recordUntil.setOnAction(e -> startRecordingWithTimeLimit(getSelectedThumbCells(cell)));
MenuItem addPaused = new MenuItem("Add in paused state"); MenuItem addPaused = new MenuItem("Add in paused state");
addPaused.setOnAction(e -> addPaused(getSelectedThumbCells(cell))); addPaused.setOnAction(e -> addPaused(getSelectedThumbCells(cell)));
MenuItem recordLater = new MenuItem("Record Later"); MenuItem recordLater = new MenuItem("Record Later");
recordLater.setOnAction(e -> LOG.debug("Record Later not implemented, yet")); recordLater.setOnAction(e -> recordLater(getSelectedThumbCells(cell)));
MenuItem pause = new MenuItem("Pause Recording"); MenuItem pause = new MenuItem("Pause Recording");
pause.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), true)); pause.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), true));
MenuItem resume = new MenuItem("Resume Recording"); MenuItem resume = new MenuItem("Resume Recording");
resume.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), false)); resume.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), false));
MenuItem pauseResume = recorder.isSuspended(cell.getModel()) ? resume : pause; MenuItem pauseResume = recorder.isSuspended(model) ? resume : pause;
MenuItem follow = new MenuItem("Follow"); MenuItem follow = new MenuItem("Follow");
follow.setOnAction(e -> follow(getSelectedThumbCells(cell), true)); follow.setOnAction(e -> follow(getSelectedThumbCells(cell), true));
@ -495,23 +461,25 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
refresh.setOnAction(e -> refresh()); refresh.setOnAction(e -> refresh());
MenuItem openRecDir = new MenuItem("Open recording directory"); MenuItem openRecDir = new MenuItem("Open recording directory");
openRecDir.setOnAction(e -> new OpenRecordingsDir(cell, cell.getModel()).execute()); openRecDir.setOnAction(e -> new OpenRecordingsDir(cell, model).execute());
MenuItem copyUrl = createCopyUrlMenuItem(cell); MenuItem copyUrl = createCopyUrlMenuItem(cell);
MenuItem sendTip = createTipMenuItem(cell); MenuItem sendTip = createTipMenuItem(cell);
configureItemsForSelection(cell, openInPlayer, copyUrl, sendTip); configureItemsForSelection(cell, openInPlayer, copyUrl, sendTip);
ContextMenu contextMenu = new ContextMenu(); ContextMenu contextMenu = new CustomMouseBehaviorContextMenu();
contextMenu.setAutoHide(true); contextMenu.setAutoHide(true);
contextMenu.setHideOnEscape(true); contextMenu.setHideOnEscape(true);
contextMenu.setAutoFix(true); contextMenu.setAutoFix(true);
contextMenu.getItems().addAll(openInPlayer, new SeparatorMenuItem(), startStop); contextMenu.getItems().addAll(openInPlayer, new SeparatorMenuItem(), startStop);
UiUtils.disableRightClickFor(contextMenu);
if (modelIsTrackedByRecorder) { if (modelIsTrackedByRecorder) {
contextMenu.getItems().add(pauseResume); contextMenu.getItems().addAll(pauseResume, recordLater);
} else { } else {
contextMenu.getItems().addAll(recordUntil, addPaused/*, recordLater*/); contextMenu.getItems().addAll(recordUntil, addPaused);
if (!recorder.isMarkedForLaterRecording(model)) {
contextMenu.getItems().add(recordLater);
}
} }
contextMenu.getItems().add(new SeparatorMenuItem()); contextMenu.getItems().add(new SeparatorMenuItem());
if (site.supportsFollow()) { if (site.supportsFollow()) {
@ -523,17 +491,24 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
contextMenu.getItems().add(sendTip); contextMenu.getItems().add(sendTip);
} }
contextMenu.getItems().addAll(copyUrl, ignore, refresh, openRecDir); contextMenu.getItems().addAll(copyUrl, ignore, refresh, openRecDir);
if(cell.getModel() instanceof MyFreeCamsModel && Objects.equals(System.getenv("CTBREC_DEV"), "1")) { if (model instanceof MyFreeCamsModel && Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
MenuItem debug = new MenuItem("debug"); MenuItem debug = new MenuItem("debug");
debug.setOnAction(e -> MyFreeCamsClient.getInstance().getSessionState(cell.getModel())); debug.setOnAction(e -> MyFreeCamsClient.getInstance().getSessionState(model));
contextMenu.getItems().add(debug); contextMenu.getItems().add(debug);
} }
return contextMenu; return contextMenu;
} }
private void recordLater(List<ThumbCell> list) {
for (ThumbCell cell : list) {
cell.recordLater();
}
}
private void startRecordingWithTimeLimit(List<ThumbCell> list) { private void startRecordingWithTimeLimit(List<ThumbCell> list) {
for (ThumbCell cell : list) { for (ThumbCell cell : list) {
cell.getModel().setMarkedForLaterRecording(false);
cell.startStopAction(true); cell.startStopAction(true);
new SetStopDateAction(cell, cell.getModel(), recorder).execute(); new SetStopDateAction(cell, cell.getModel(), recorder).execute();
} }
@ -691,8 +666,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
return 0; return 0;
} }
private void startStopAction(ActionEvent e, List<ThumbCell> selection, boolean start) { private void startStopAction(List<ThumbCell> selection, boolean start) {
for (ThumbCell thumbCell : selection) { for (ThumbCell thumbCell : selection) {
thumbCell.getModel().setSuspended(false);
thumbCell.getModel().setMarkedForLaterRecording(false);
thumbCell.startStopAction(start); thumbCell.startStopAction(start);
} }
} }
@ -768,6 +745,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
// remove the ones from grid, which don't match // remove the ones from grid, which don't match
for (Iterator<Node> iterator = grid.getChildren().iterator(); iterator.hasNext();) { for (Iterator<Node> iterator = grid.getChildren().iterator(); iterator.hasNext();) {
Node node = iterator.next(); Node node = iterator.next();
if (node instanceof ThumbCell) {
ThumbCell cell = (ThumbCell) node; ThumbCell cell = (ThumbCell) node;
Model m = cell.getModel(); Model m = cell.getModel();
if (!matches(m, filter)) { if (!matches(m, filter)) {
@ -776,6 +754,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
cell.setSelected(false); cell.setSelected(false);
} }
} }
}
// add the ones, which might have been filtered before, but now match // add the ones, which might have been filtered before, but now match
for (Iterator<ThumbCell> iterator = filteredThumbCells.iterator(); iterator.hasNext();) { for (Iterator<ThumbCell> iterator = filteredThumbCells.iterator(); iterator.hasNext();) {
@ -881,6 +860,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
@Override @Override
public void selected() { public void selected() {
grid.getChildren().remove(noResultsFound);
if (grid.getChildren().isEmpty()) {
grid.getChildren().add(progressIndicator);
}
queue.clear(); queue.clear();
if (updateService != null) { if (updateService != null) {
State s = updateService.getState(); State s = updateService.getState();

View File

@ -1,5 +1,10 @@
package ctbrec.ui.tabs; package ctbrec.ui.tabs;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.ui.CamrecApplication; import ctbrec.ui.CamrecApplication;
import ctbrec.ui.CamrecApplication.Release; import ctbrec.ui.CamrecApplication.Release;
@ -15,10 +20,8 @@ import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UpdateTab extends Tab { public class UpdateTab extends Tab implements TabSelectionListener {
private static final Logger LOG = LoggerFactory.getLogger(UpdateTab.class); private static final Logger LOG = LoggerFactory.getLogger(UpdateTab.class);
@ -37,11 +40,14 @@ public class UpdateTab extends Tab {
vbox.setAlignment(Pos.CENTER); vbox.setAlignment(Pos.CENTER);
changelog = new TextArea(); changelog = new TextArea();
changelog.setEditable(false); changelog.setEditable(false);
changelog.setText("Loading changelog...");
vbox.getChildren().add(changelog); vbox.getChildren().add(changelog);
VBox.setVgrow(changelog, Priority.ALWAYS); VBox.setVgrow(changelog, Priority.ALWAYS);
setContent(vbox); setContent(vbox);
}
new Thread(() -> { public void loadChangeLog() {
CompletableFuture.runAsync(() -> {
Request req = new Request.Builder().url("https://pastebin.com/raw/fiAPtM0s").build(); Request req = new Request.Builder().url("https://pastebin.com/raw/fiAPtM0s").build();
try (Response resp = CamrecApplication.httpClient.execute(req)) { try (Response resp = CamrecApplication.httpClient.execute(req)) {
if (resp.isSuccessful()) { if (resp.isSuccessful()) {
@ -53,6 +59,16 @@ public class UpdateTab extends Tab {
LOG.error("Couldn't download the changelog", e1); LOG.error("Couldn't download the changelog", e1);
Dialogs.showError(getTabPane().getScene(), "Communication error", "Couldn't download the changelog", e1); Dialogs.showError(getTabPane().getScene(), "Communication error", "Couldn't download the changelog", e1);
} }
}).start(); });
}
@Override
public void selected() {
loadChangeLog();
}
@Override
public void deselected() {
// nothing to do
} }
} }

View File

@ -18,6 +18,7 @@ import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.LoggingEvent; import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.spi.StackTraceElementProxy; import ch.qos.logback.classic.spi.StackTraceElementProxy;
import ctbrec.event.EventBusHolder; import ctbrec.event.EventBusHolder;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.SearchBox; import ctbrec.ui.controls.SearchBox;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
@ -150,7 +151,7 @@ public class LoggingTab extends Tab {
}); });
}); });
ContextMenu menu = new ContextMenu(copy); ContextMenu menu = new CustomMouseBehaviorContextMenu(copy);
return menu; return menu;
} }

View File

@ -43,6 +43,9 @@
<logger name="ctbrec.LoggingInterceptor" level="info"/> <logger name="ctbrec.LoggingInterceptor" level="info"/>
<logger name="ctbrec.io.CookieJarImpl" level="INFO"/> <logger name="ctbrec.io.CookieJarImpl" level="INFO"/>
<logger name="ctbrec.recorder.OnlineMonitor" level="INFO"/>
<logger name="ctbrec.recorder.RecordingFileMonitor" level="TRACE"/>
<logger name="ctbrec.recorder.download.dash.DashDownload" level="DEBUG"/>
<logger name="ctbrec.recorder.server.HlsServlet" level="INFO"/> <logger name="ctbrec.recorder.server.HlsServlet" level="INFO"/>
<logger name="ctbrec.recorder.server.RecorderServlet" level="INFO"/> <logger name="ctbrec.recorder.server.RecorderServlet" level="INFO"/>
<logger name="ctbrec.recorder.ThreadPoolScaler" level="DEBUG"/> <logger name="ctbrec.recorder.ThreadPoolScaler" level="DEBUG"/>

View File

@ -6,11 +6,7 @@
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"> <classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes> <attributes>
<attribute name="maven.pomderived" value="true"/> <attribute name="maven.pomderived" value="true"/>

View File

@ -11,6 +11,6 @@ org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.release=disabled org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=1.8 org.eclipse.jdt.core.compiler.source=1.8

View File

@ -33,6 +33,7 @@ public abstract class AbstractModel implements Model {
private int streamUrlIndex = -1; private int streamUrlIndex = -1;
private int priority = 50; private int priority = 50;
private boolean suspended = false; private boolean suspended = false;
private boolean markedForLaterRecording = false;
protected transient Site site; protected transient Site site;
protected State onlineState = State.UNKNOWN; protected State onlineState = State.UNKNOWN;
private Instant lastSeen; private Instant lastSeen;
@ -145,6 +146,16 @@ public abstract class AbstractModel implements Model {
this.suspended = suspended; this.suspended = suspended;
} }
@Override
public boolean isMarkedForLaterRecording() {
return markedForLaterRecording;
}
@Override
public void setMarkedForLaterRecording(boolean marked) {
this.markedForLaterRecording = marked;
}
@Override @Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException { public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
return onlineState; return onlineState;

View File

@ -174,6 +174,13 @@ public class Config {
iterator.remove(); iterator.remove();
} }
} }
// 3.11.0 make Cam4 model names lower case
settings.models.stream()
.filter(m -> m instanceof Cam4Model)
.forEach(m -> m.setName(m.getName().toLowerCase()));
settings.modelsIgnored.stream()
.filter(m -> m instanceof Cam4Model)
.forEach(m -> m.setName(m.getName().toLowerCase()));
} }
private void makeBackup(File source) { private void makeBackup(File source) {

View File

@ -122,6 +122,10 @@ public interface Model extends Comparable<Model>, Serializable {
public void setSuspended(boolean suspended); public void setSuspended(boolean suspended);
public boolean isMarkedForLaterRecording();
public void setMarkedForLaterRecording(boolean marked);
public Download createDownload(); public Download createDownload();
public void setPriority(int priority); public void setPriority(int priority);

View File

@ -118,6 +118,10 @@ public class Settings {
public String proxyPort; public String proxyPort;
public ProxyType proxyType = ProxyType.DIRECT; public ProxyType proxyType = ProxyType.DIRECT;
public String proxyUser; public String proxyUser;
public double[] recordLaterColumnWidths = new double[0];
public String[] recordLaterColumnIds = new String[0];
public String recordLaterSortColumn = "";
public String recordLaterSortType = "";
public double[] recordedModelsColumnWidths = new double[0]; public double[] recordedModelsColumnWidths = new double[0];
public String[] recordedModelsColumnIds = new String[0]; public String[] recordedModelsColumnIds = new String[0];
public String recordedModelsSortColumn = ""; public String recordedModelsSortColumn = "";
@ -128,6 +132,7 @@ public class Settings {
public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT; public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
public String recordingsSortColumn = ""; public String recordingsSortColumn = "";
public String recordingsSortType = ""; public String recordingsSortType = "";
public List<Model> recordLater = new ArrayList<>();
public boolean recordSingleFile = false; public boolean recordSingleFile = false;
public boolean removeRecordingAfterPostProcessing = false; public boolean removeRecordingAfterPostProcessing = false;
public boolean requireAuthentication = false; public boolean requireAuthentication = false;
@ -147,6 +152,7 @@ public class Settings {
public String stripchatUsername = ""; public String stripchatUsername = "";
public String stripchatPassword = ""; public String stripchatPassword = "";
public boolean stripchatUseXhamster = false; public boolean stripchatUseXhamster = false;
public boolean totalModelCountInTitle = false;
public boolean transportLayerSecurity = true; public boolean transportLayerSecurity = true;
public int thumbWidth = 180; public int thumbWidth = 180;
public boolean updateThumbnails = true; public boolean updateThumbnails = true;

View File

@ -0,0 +1,9 @@
package ctbrec;
public class UnexpectedResponseException extends RuntimeException {
public UnexpectedResponseException(String msg) {
super(msg);
}
}

View File

@ -1,7 +1,15 @@
package ctbrec.io; package ctbrec.io;
import static java.nio.charset.StandardCharsets.*; import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.Settings.ProxyType;
import okhttp3.*;
import okhttp3.OkHttpClient.Builder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.*;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
@ -13,39 +21,11 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.net.ssl.KeyManager; import static java.nio.charset.StandardCharsets.UTF_8;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.Settings.ProxyType;
import okhttp3.ConnectionPool;
import okhttp3.Cookie;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;
import okhttp3.OkHttpClient.Builder;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
public abstract class HttpClient { public abstract class HttpClient {
private static final Logger LOG = LoggerFactory.getLogger(HttpClient.class); private static final Logger LOG = LoggerFactory.getLogger(HttpClient.class);
@ -293,4 +273,12 @@ public abstract class HttpClient {
} }
return result; return result;
} }
public static String bodyToJsonObject(Response response) {
return Optional.ofNullable(response.body()).map(Object::toString).orElse("{}");
}
public static String bodyToJsonArray(Response response) {
return Optional.ofNullable(response.body()).map(Object::toString).orElse("[]");
}
} }

View File

@ -35,15 +35,9 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
@Override @Override
public Model fromJson(JsonReader reader) throws IOException { public Model fromJson(JsonReader reader) throws IOException {
reader.beginObject(); reader.beginObject();
String name = null;
String description = null;
String url = null;
Object type = null; Object type = null;
int streamUrlIndex = -1;
int priority;
boolean suspended = false;
Model model = null; Model model = null;
while (reader.hasNext()) { while (reader.hasNext()) {
try { try {
Token token = reader.peek(); Token token = reader.peek();
@ -54,23 +48,19 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()).toString()); Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()).toString());
model = (Model) modelClass.getDeclaredConstructor().newInstance(); model = (Model) modelClass.getDeclaredConstructor().newInstance();
} else if (key.equals("name")) { } else if (key.equals("name")) {
name = reader.nextString(); model.setName(reader.nextString());
model.setName(name);
} else if (key.equals("description")) { } else if (key.equals("description")) {
description = reader.nextString(); model.setDescription(reader.nextString());
model.setDescription(description);
} else if (key.equals("url")) { } else if (key.equals("url")) {
url = reader.nextString(); model.setUrl(reader.nextString());
model.setUrl(url);
} else if (key.equals("priority")) { } else if (key.equals("priority")) {
priority = reader.nextInt(); model.setPriority(reader.nextInt());
model.setPriority(priority);
} else if (key.equals("streamUrlIndex")) { } else if (key.equals("streamUrlIndex")) {
streamUrlIndex = reader.nextInt(); model.setStreamUrlIndex(reader.nextInt());
model.setStreamUrlIndex(streamUrlIndex);
} else if (key.equals("suspended")) { } else if (key.equals("suspended")) {
suspended = reader.nextBoolean(); model.setSuspended(reader.nextBoolean());
model.setSuspended(suspended); } else if (key.equals("markedForLater")) {
model.setMarkedForLaterRecording(reader.nextBoolean());
} else if (key.equals("lastSeen")) { } else if (key.equals("lastSeen")) {
model.setLastSeen(Instant.ofEpochMilli(reader.nextLong())); model.setLastSeen(Instant.ofEpochMilli(reader.nextLong()));
} else if (key.equals("lastRecorded")) { } else if (key.equals("lastRecorded")) {
@ -92,7 +82,8 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
} else { } else {
reader.skipValue(); reader.skipValue();
} }
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e) {
throw new IOException("Couldn't instantiate model class [" + type + "]", e); throw new IOException("Couldn't instantiate model class [" + type + "]", e);
} }
} }
@ -118,6 +109,7 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
writer.name("priority").value(model.getPriority()); writer.name("priority").value(model.getPriority());
writer.name("streamUrlIndex").value(model.getStreamUrlIndex()); writer.name("streamUrlIndex").value(model.getStreamUrlIndex());
writer.name("suspended").value(model.isSuspended()); writer.name("suspended").value(model.isSuspended());
writer.name("markedForLater").value(model.isMarkedForLaterRecording());
writer.name("lastSeen").value(model.getLastSeen().toEpochMilli()); writer.name("lastSeen").value(model.getLastSeen().toEpochMilli());
writer.name("lastRecorded").value(model.getLastRecorded().toEpochMilli()); writer.name("lastRecorded").value(model.getLastRecorded().toEpochMilli());
writer.name("recordUntil").value(model.getRecordUntil().toEpochMilli()); writer.name("recordUntil").value(model.getRecordUntil().toEpochMilli());

View File

@ -244,8 +244,13 @@ public class NextGenLocalRecorder implements Recorder {
} }
@Override @Override
public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException { public void addModel(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
if (!models.contains(model)) { Optional<Model> existing = findModel(model);
if (existing.isPresent()) {
existing.get().setSuspended(model.isSuspended());
existing.get().setMarkedForLaterRecording(model.isMarkedForLaterRecording());
startRecordingProcess(existing.get());
} else {
LOG.info("Model {} added", model); LOG.info("Model {} added", model);
recorderLock.lock(); recorderLock.lock();
try { try {
@ -257,7 +262,6 @@ public class NextGenLocalRecorder implements Recorder {
} finally { } finally {
recorderLock.unlock(); recorderLock.unlock();
} }
startRecordingProcess(model); startRecordingProcess(model);
} }
} }
@ -418,16 +422,6 @@ public class NextGenLocalRecorder implements Recorder {
} }
} }
@Override
public boolean isTracked(Model model) {
recorderLock.lock();
try {
return models.contains(model);
} finally {
recorderLock.unlock();
}
}
@Override @Override
public List<Model> getModels() { public List<Model> getModels() {
recorderLock.lock(); recorderLock.lock();
@ -540,7 +534,9 @@ public class NextGenLocalRecorder implements Recorder {
int index = models.indexOf(model); int index = models.indexOf(model);
Model m = models.get(index); Model m = models.get(index);
m.setSuspended(false); m.setSuspended(false);
m.setMarkedForLaterRecording(false);
model.setSuspended(false); model.setSuspended(false);
model.setMarkedForLaterRecording(false);
config.save(); config.save();
startRecordingProcess(m); startRecordingProcess(m);
} else { } else {
@ -551,16 +547,31 @@ public class NextGenLocalRecorder implements Recorder {
} }
} }
@Override
public boolean isTracked(Model model) {
Optional<Model> m = findModel(model);
boolean markedForRecording = m.map(Model::isMarkedForLaterRecording).orElse(false);
return m.isPresent() && !markedForRecording;
}
@Override @Override
public boolean isSuspended(Model model) { public boolean isSuspended(Model model) {
return findModel(model).map(Model::isSuspended).orElse(false);
}
@Override
public boolean isMarkedForLaterRecording(Model model) {
return findModel(model).map(Model::isMarkedForLaterRecording).orElse(false);
}
private Optional<Model> findModel(Model m) {
recorderLock.lock(); recorderLock.lock();
try { try {
int index = models.indexOf(model); int index = models.indexOf(m);
if (index >= 0) { if (index >= 0) {
Model m = models.get(index); return Optional.of(models.get(index));
return m.isSuspended();
} else { } else {
return false; return Optional.empty();
} }
} finally { } finally {
recorderLock.unlock(); recorderLock.unlock();
@ -763,4 +774,9 @@ public class NextGenLocalRecorder implements Recorder {
LOG.info("Resuming recorder"); LOG.info("Resuming recorder");
recording = true; recording = true;
} }
@Override
public int getModelCount() {
return (int) models.stream().filter(m -> !m.isMarkedForLaterRecording()).count();
}
} }

View File

@ -82,7 +82,9 @@ public class OnlineMonitor extends Thread {
// submit online check jobs to the executor for the model's site // submit online check jobs to the executor for the model's site
List<Future<?>> futures = new LinkedList<>(); List<Future<?>> futures = new LinkedList<>();
for (Model model : models) { for (Model model : models) {
if (config.getSettings().onlineCheckSkipsPausedModels && model.isSuspended()) { boolean skipCheckForSuspended = config.getSettings().onlineCheckSkipsPausedModels && model.isSuspended();
boolean skipCheckForMarkedAsLater = model.isMarkedForLaterRecording();
if (skipCheckForSuspended || skipCheckForMarkedAsLater) {
continue; continue;
} else { } else {
futures.add(updateModel(model)); futures.add(updateModel(model));

View File

@ -1,17 +1,17 @@
package ctbrec.recorder; package ctbrec.recorder;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.io.HttpClient;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.io.HttpClient;
public interface Recorder { public interface Recorder {
public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException; public void addModel(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException; public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
public void stopRecordingAt(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException; public void stopRecordingAt(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
@ -59,6 +59,8 @@ public interface Recorder {
public boolean isSuspended(Model model); public boolean isSuspended(Model model);
public boolean isMarkedForLaterRecording(Model model);
/** /**
* Returns only the models from getModels(), which are online * Returns only the models from getModels(), which are online
* @return * @return
@ -140,4 +142,6 @@ public interface Recorder {
* @throws InvalidKeyException * @throws InvalidKeyException
*/ */
public void resume() throws InvalidKeyException, NoSuchAlgorithmException, IOException; public void resume() throws InvalidKeyException, NoSuchAlgorithmException, IOException;
public int getModelCount();
} }

View File

@ -29,6 +29,7 @@ public class RecordingPreconditions {
void check(Model model) throws IOException { void check(Model model) throws IOException {
ensureRecorderIsActive(); ensureRecorderIsActive();
ensureModelIsNotSuspended(model); ensureModelIsNotSuspended(model);
ensureModelIsNotMarkedForLaterRecording(model);
ensureRecordUntilIsInFuture(model); ensureRecordUntilIsInFuture(model);
ensureNoRecordingRunningForModel(model); ensureNoRecordingRunningForModel(model);
ensureModelShouldBeRecorded(model); ensureModelShouldBeRecorded(model);
@ -113,6 +114,12 @@ public class RecordingPreconditions {
} }
} }
private void ensureModelIsNotMarkedForLaterRecording(Model model) {
if (model.isMarkedForLaterRecording()) {
throw new PreconditionNotMetException("Model " + model + " is marked for later recording");
}
}
private void ensureRecorderIsActive() { private void ensureRecorderIsActive() {
if (!recorder.isRecording()) { if (!recorder.isRecording()) {
throw new PreconditionNotMetException("Recorder is not in recording mode"); throw new PreconditionNotMetException("Recorder is not in recording mode");

View File

@ -1,5 +1,25 @@
package ctbrec.recorder; package ctbrec.recorder;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.Hmac;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.event.EventBusHolder;
import ctbrec.event.NoSpaceLeftEvent;
import ctbrec.event.RecordingStateChangedEvent;
import ctbrec.io.*;
import ctbrec.sites.Site;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.Request.Builder;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
@ -7,38 +27,7 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.Hmac;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.event.EventBusHolder;
import ctbrec.event.NoSpaceLeftEvent;
import ctbrec.event.RecordingStateChangedEvent;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.FileJsonAdapter;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.sites.Site;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.Request.Builder;
import okhttp3.RequestBody;
import okhttp3.Response;
public class RemoteRecorder implements Recorder { public class RemoteRecorder implements Recorder {
@ -86,7 +75,7 @@ public class RemoteRecorder implements Recorder {
} }
@Override @Override
public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException { public void addModel(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
sendRequest("start", model); sendRequest("start", model);
} }
@ -179,17 +168,27 @@ public class RemoteRecorder implements Recorder {
@Override @Override
public boolean isTracked(Model model) { public boolean isTracked(Model model) {
return models != null && models.contains(model); Optional<Model> m = findModel(model);
boolean markedForRecording = m.map(Model::isMarkedForLaterRecording).orElse(false);
return m.isPresent() && !markedForRecording;
} }
@Override @Override
public boolean isSuspended(Model model) { public boolean isSuspended(Model model) {
int index = models.indexOf(model); return findModel(model).map(Model::isSuspended).orElse(false);
}
@Override
public boolean isMarkedForLaterRecording(Model model) {
return findModel(model).map(Model::isMarkedForLaterRecording).orElse(false);
}
private Optional<Model> findModel(Model m) {
int index = Optional.ofNullable(models).map(list -> list.indexOf(m)).orElse(-1);
if (index >= 0) { if (index >= 0) {
Model m = models.get(index); return Optional.of(models.get(index));
return m.isSuspended();
} else { } else {
return false; return Optional.empty();
} }
} }
@ -588,4 +587,9 @@ public class RemoteRecorder implements Recorder {
public void resume() throws InvalidKeyException, NoSuchAlgorithmException, IOException { public void resume() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
sendRequest("resumeRecorder"); sendRequest("resumeRecorder");
} }
@Override
public int getModelCount() {
return (int) models.stream().filter(m -> !m.isMarkedForLaterRecording()).count();
}
} }

View File

@ -272,6 +272,7 @@ public class DashDownload extends AbstractDownload {
return this; return this;
} }
@SuppressWarnings("deprecation")
private boolean splitRecording() { private boolean splitRecording() {
if (splittingStrategy.splitNecessary(this)) { if (splittingStrategy.splitNecessary(this)) {
internalStop(); internalStop();

View File

@ -1,17 +1,5 @@
package ctbrec.sites.cam4; package ctbrec.sites.cam4;
import static ctbrec.io.HttpConstants.*;
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;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.StringUtil; import ctbrec.StringUtil;
@ -20,6 +8,18 @@ import ctbrec.io.HttpException;
import ctbrec.sites.AbstractSite; import ctbrec.sites.AbstractSite;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject;
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 static ctbrec.io.HttpClient.bodyToJsonObject;
import static ctbrec.io.HttpConstants.USER_AGENT;
public class Cam4 extends AbstractSite { public class Cam4 extends AbstractSite {
@ -47,8 +47,9 @@ public class Cam4 extends AbstractSite {
public Cam4Model createModel(String name) { public Cam4Model createModel(String name) {
Cam4Model m = new Cam4Model(); Cam4Model m = new Cam4Model();
m.setSite(this); m.setSite(this);
m.setName(name); m.setDisplayName(name);
m.setUrl(getBaseUrl() + '/' + name + '/'); m.setName(name.toLowerCase());
m.setUrl(getBaseUrl() + '/' + m.getName() + '/');
return m; return m;
} }
@ -129,7 +130,7 @@ public class Cam4 extends AbstractSite {
.build(); .build();
try(Response response = getHttpClient().execute(req)) { try(Response response = getHttpClient().execute(req)) {
if(response.isSuccessful()) { if(response.isSuccessful()) {
String body = response.body().string(); String body = bodyToJsonObject(response);
JSONArray results = new JSONArray(body); JSONArray results = new JSONArray(body);
for (int i = 0; i < results.length(); i++) { for (int i = 0; i < results.length(); i++) {
JSONObject result = results.getJSONObject(i); JSONObject result = results.getJSONObject(i);

View File

@ -1,6 +1,7 @@
package ctbrec.sites.cam4; package ctbrec.sites.cam4;
import static ctbrec.Model.State.*; import static ctbrec.Model.State.*;
import static ctbrec.io.HttpClient.*;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
import static java.util.regex.Pattern.*; import static java.util.regex.Pattern.*;
@ -8,7 +9,10 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -36,6 +40,8 @@ import ctbrec.Config;
import ctbrec.StringUtil; import ctbrec.StringUtil;
import ctbrec.io.HtmlParser; import ctbrec.io.HtmlParser;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.HttpHeaderFactory;
import ctbrec.recorder.download.HttpHeaderFactoryImpl;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import okhttp3.FormBody; import okhttp3.FormBody;
import okhttp3.Request; import okhttp3.Request;
@ -103,10 +109,7 @@ public class Cam4Model extends AbstractModel {
@Override @Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException { public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if(failFast) { if (!failFast && onlineState == UNKNOWN) {
return onlineState;
} else {
if(onlineState == UNKNOWN) {
try { try {
loadModelDetails(); loadModelDetails();
} catch (Exception e) { } catch (Exception e) {
@ -115,7 +118,6 @@ public class Cam4Model extends AbstractModel {
} }
return onlineState; return onlineState;
} }
}
private String getPlaylistUrl() throws IOException { private String getPlaylistUrl() throws IOException {
String page = loadModelPage(); String page = loadModelPage();
@ -146,8 +148,8 @@ public class Cam4Model extends AbstractModel {
.build(); // @formatter:on .build(); // @formatter:on
try (Response response = site.getHttpClient().execute(req)) { try (Response response = site.getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string()); JSONObject json = new JSONObject(bodyToJsonObject(response));
LOG.trace(json.toString(2)); if (LOG.isTraceEnabled()) LOG.trace(json.toString(2));
if (json.has("canUseCDN")) { if (json.has("canUseCDN")) {
if (json.getBoolean("canUseCDN")) { if (json.getBoolean("canUseCDN")) {
playlistUrl = json.getString("cdnURL"); playlistUrl = json.getString("cdnURL");
@ -191,8 +193,7 @@ public class Cam4Model extends AbstractModel {
src.height = Optional.ofNullable(playlist.getStreamInfo()).map(StreamInfo::getResolution).map(res -> res.height).orElse(0); src.height = Optional.ofNullable(playlist.getStreamInfo()).map(StreamInfo::getResolution).map(res -> res.height).orElse(0);
String masterUrl = getPlaylistUrl(); String masterUrl = getPlaylistUrl();
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
String segmentUri = baseUrl + playlist.getUri(); src.mediaPlaylistUrl = baseUrl + playlist.getUri();
src.mediaPlaylistUrl = segmentUri;
LOG.trace("Media playlist {}", src.mediaPlaylistUrl); LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
sources.add(src); sources.add(src);
} }
@ -201,19 +202,20 @@ public class Cam4Model extends AbstractModel {
} }
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException { private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
String playlistUrl = getPlaylistUrl(); String masterPlaylistUrl = getPlaylistUrl();
LOG.trace("Loading master playlist [{}]", playlistUrl); LOG.trace("Loading master playlist [{}]", masterPlaylistUrl);
Request req = new Request.Builder().url(playlistUrl).build(); Request.Builder builder = new Request.Builder().url(masterPlaylistUrl);
getHttpHeaderFactory().createMasterPlaylistHeaders().forEach(builder::header);
Request req = builder.build();
try (Response response = site.getHttpClient().execute(req)) { try (Response response = site.getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
InputStream inputStream = response.body().byteStream(); InputStream inputStream = response.body().byteStream();
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse(); Playlist playlist = parser.parse();
MasterPlaylist master = playlist.getMasterPlaylist(); return playlist.getMasterPlaylist();
return master;
} else { } else {
throw new HttpException(response.code(), "Couldn't download HLS playlist " + playlistUrl); throw new HttpException(response.code(), "Couldn't download HLS playlist " + masterPlaylistUrl);
} }
} }
} }
@ -321,12 +323,6 @@ public class Cam4Model extends AbstractModel {
this.playlistUrl = playlistUrl; this.playlistUrl = playlistUrl;
} }
public class ModelDetailsEmptyException extends Exception {
public ModelDetailsEmptyException(String msg) {
super(msg);
}
}
@Override @Override
public void setUrl(String url) { public void setUrl(String url) {
String normalizedUrl = url.toLowerCase(); String normalizedUrl = url.toLowerCase();
@ -335,4 +331,22 @@ public class Cam4Model extends AbstractModel {
} }
super.setUrl(normalizedUrl); super.setUrl(normalizedUrl);
} }
@Override
public HttpHeaderFactory getHttpHeaderFactory() {
HttpHeaderFactoryImpl fac = new HttpHeaderFactoryImpl();
Map<String, String> headers = new HashMap<>();
headers.put(ACCEPT, "*/*");
headers.put(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage());
headers.put(CONNECTION, KEEP_ALIVE);
if (getSite() != null) {
headers.put(ORIGIN, getSite().getBaseUrl());
headers.put(REFERER, getSite().getBaseUrl());
}
headers.put(USER_AGENT, Config.getInstance().getSettings().httpUserAgent);
fac.setMasterPlaylistHeaders(headers);
fac.setSegmentPlaylistHeaders(headers);
fac.setSegmentHeaders(headers);
return fac;
}
} }

View File

@ -3,6 +3,8 @@ package ctbrec.sites.manyvids;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -258,13 +260,19 @@ public class MVLive extends AbstractSite {
@Override @Override
public Model createModelFromUrl(String url) { public Model createModelFromUrl(String url) {
try {
Matcher m = Pattern.compile("https://live.manyvids.com/(?:stream/)?(.*?)(?:/.*?)?").matcher(url.trim()); Matcher m = Pattern.compile("https://live.manyvids.com/(?:stream/)?(.*?)(?:/.*?)?").matcher(url.trim());
if (m.matches()) { if (m.matches()) {
return createModel(m.group(1)); String modelName = URLDecoder.decode(m.group(1), "utf-8");
return createModel(modelName);
} }
m = Pattern.compile("https://www.manyvids.com/MVLive/(.*?)/\\d+/?").matcher(url.trim()); m = Pattern.compile("https://www.manyvids.com/MVLive/(.*?)/\\d+/?").matcher(url.trim());
if (m.matches()) { if (m.matches()) {
return createModel(m.group(1)); String modelName = URLDecoder.decode(m.group(1), "utf-8");
return createModel(modelName);
}
} catch (UnsupportedEncodingException e) {
LOG.error("Couldn't decode model name from URL", e);
} }
return super.createModelFromUrl(url); return super.createModelFromUrl(url);

View File

@ -5,7 +5,7 @@ After=network.target
[Service] [Service]
WorkingDirectory=/opt/ctbrec WorkingDirectory=/opt/ctbrec
SyslogIdentifier=ctbrec SyslogIdentifier=ctbrec
ExecStart=-/usr/bin/java -Xmx256m -cp ${name.final}.jar -Dctbrec.config=server.json ctbrec.recorder.server.HttpServer ExecStart=-/usr/bin/java -Xmx256m -cp ${name.final}.jar -Dfile.encoding=utf-8 -Dctbrec.config=server.json ctbrec.recorder.server.HttpServer
User=ctbrec User=ctbrec
Type=simple Type=simple
TimeoutSec=900 TimeoutSec=900

View File

@ -18,7 +18,7 @@ start() {
# start ctbrec # start ctbrec
$JAVA -version $JAVA -version
$JAVA -Xmx256m -cp "${DIR}:${DIR}/${name.final}.jar" -Dctbrec.config=server.json ctbrec.recorder.server.HttpServer & $JAVA -Xmx256m -cp "${DIR}:${DIR}/${name.final}.jar" -Dfile.encoding=utf-8 -Dctbrec.config=server.json ctbrec.recorder.server.HttpServer &
# write a pid file # write a pid file
echo $! > "${PIDFILE}" echo $! > "${PIDFILE}"

View File

@ -4,5 +4,5 @@ DIR=$(dirname $0)
pushd $DIR pushd $DIR
JAVA=java JAVA=java
$JAVA -version $JAVA -version
$JAVA -Xmx192m -cp "${DIR}:${name.final}.jar" -Dctbrec.config=server.json ctbrec.recorder.server.HttpServer $JAVA -Xmx192m -cp "${DIR}:${name.final}.jar" -Dfile.encoding=utf-8 -Dctbrec.config=server.json ctbrec.recorder.server.HttpServer
popd popd

View File

@ -1 +1 @@
java -Xmx192m -cp .;${name.final}.jar -Dctbrec.config=server.json ctbrec.recorder.server.HttpServer java -Xmx192m -cp .;${name.final}.jar -Dfile.encoding=utf-8 -Dctbrec.config=server.json ctbrec.recorder.server.HttpServer

View File

@ -1,27 +1,7 @@
package ctbrec.recorder.server; package ctbrec.recorder.server;
import static javax.servlet.http.HttpServletResponse.*;
import java.io.File;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi; import com.squareup.moshi.Moshi;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
@ -31,6 +11,23 @@ import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter; import ctbrec.io.ModelJsonAdapter;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import static javax.servlet.http.HttpServletResponse.*;
public class RecorderServlet extends AbstractCtbrecServlet { public class RecorderServlet extends AbstractCtbrecServlet {
@ -73,7 +70,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
switch (request.action) { switch (request.action) {
case "start": case "start":
LOG.debug("Starting recording for model {} - {}", request.model.getName(), request.model.getUrl()); LOG.debug("Starting recording for model {} - {}", request.model.getName(), request.model.getUrl());
recorder.startRecording(request.model); recorder.addModel(request.model);
String response = "{\"status\": \"success\", \"msg\": \"Recording started\"}"; String response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
resp.getWriter().write(response); resp.getWriter().write(response);
break; break;
@ -262,7 +259,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
for (Site site : sites) { for (Site site : sites) {
Model model = site.createModelFromUrl(url); Model model = site.createModelFromUrl(url);
if (model != null) { if (model != null) {
recorder.startRecording(model); recorder.addModel(model);
return; return;
} }
} }
@ -279,7 +276,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
for (Site site : sites) { for (Site site : sites) {
if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) { if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) {
Model m = site.createModel(modelName); Model m = site.createModel(modelName);
recorder.startRecording(m); recorder.addModel(m);
return; return;
} }
} }

View File

@ -35,12 +35,12 @@
</root> </root>
<logger name="ctbrec.LoggingInterceptor" level="INFO"/> <logger name="ctbrec.LoggingInterceptor" level="INFO"/>
<logger name="ctbrec.io.CookieJarImpl" level="INFO"/>
<logger name="ctbrec.recorder.Chaturbate" level="INFO" /> <logger name="ctbrec.recorder.Chaturbate" level="INFO" />
<logger name="ctbrec.recorder.OnlineMonitor" level="INFO"/>
<logger name="ctbrec.recorder.server.HlsServlet" level="INFO"/> <logger name="ctbrec.recorder.server.HlsServlet" level="INFO"/>
<logger name="ctbrec.recorder.server.RecorderServlet" level="INFO"/> <logger name="ctbrec.recorder.server.RecorderServlet" level="INFO"/>
<logger name="ctbrec.recorder.ThreadPoolScaler" level="DEBUG"/> <logger name="ctbrec.recorder.ThreadPoolScaler" level="DEBUG"/>
<logger name="ctbrec.io.CookieJarImpl" level="INFO"/>
<logger name="org.eclipse.jetty" level="INFO" /> <logger name="org.eclipse.jetty" level="INFO" />
<logger name="streamer" level="ERROR" />
</configuration> </configuration>