forked from j62/ctbrec
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:
commit
cdf582ad8f
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -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
|
||||
========================
|
||||
* Added config option for faster scroll speed
|
||||
* 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 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
|
||||
========================
|
||||
|
|
|
@ -132,6 +132,7 @@
|
|||
<bundledJre64Bit>true</bundledJre64Bit>
|
||||
<minVersion>15</minVersion>
|
||||
<maxHeapSize>512</maxHeapSize>
|
||||
<opt>-Dfile.encoding=utf-8</opt>
|
||||
</jre>
|
||||
<versionInfo>
|
||||
<fileVersion>4.0.0.0</fileVersion>
|
||||
|
|
|
@ -4,5 +4,5 @@ DIR="$(dirname "$0")"
|
|||
pushd "${DIR}"
|
||||
JAVA=./jre/bin/java
|
||||
$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
|
||||
|
|
|
@ -4,5 +4,5 @@ DIR="$(dirname "$0")"
|
|||
pushd "${DIR}"
|
||||
JAVA=java
|
||||
$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
|
||||
|
|
|
@ -5,5 +5,5 @@ pushd "$DIR"
|
|||
JAVA_HOME="$DIR/jre/Contents/Home"
|
||||
JAVA="$JAVA_HOME/bin/java"
|
||||
$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
|
|
@ -4,5 +4,5 @@ DIR=$(dirname $0)
|
|||
pushd "$DIR"
|
||||
JAVA=java
|
||||
$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
|
|
@ -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
|
|
@ -1,6 +1,8 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
|
||||
import static ctbrec.event.Event.Type.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
|
@ -61,7 +63,7 @@ import ctbrec.ui.news.NewsTab;
|
|||
import ctbrec.ui.settings.SettingsTab;
|
||||
import ctbrec.ui.tabs.DonateTabFx;
|
||||
import ctbrec.ui.tabs.HelpTab;
|
||||
import ctbrec.ui.tabs.RecordedModelsTab;
|
||||
import ctbrec.ui.tabs.RecordedTab;
|
||||
import ctbrec.ui.tabs.RecordingsTab;
|
||||
import ctbrec.ui.tabs.SiteTab;
|
||||
import ctbrec.ui.tabs.TabSelectionListener;
|
||||
|
@ -102,7 +104,7 @@ public class CamrecApplication extends Application {
|
|||
public static HttpClient httpClient;
|
||||
public static String title;
|
||||
private Stage primaryStage;
|
||||
private RecordedModelsTab modelsTab;
|
||||
private RecordedTab modelsTab;
|
||||
private RecordingsTab recordingsTab;
|
||||
private ScheduledExecutorService scheduler;
|
||||
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);
|
||||
recordingsTab = new RecordingsTab("Recordings", recorder, config);
|
||||
tabPane.getTabs().add(recordingsTab);
|
||||
|
@ -348,11 +350,12 @@ public class CamrecApplication extends Application {
|
|||
EventBusHolder.BUS.register(new Object() {
|
||||
@Subscribe
|
||||
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 {
|
||||
List<Model> models = recorder.getCurrentlyRecording();
|
||||
activeRecordings = models.size();
|
||||
String windowTitle = activeRecordings > 0 ? "(" + activeRecordings + ") " + title : title;
|
||||
int modelCount = recorder.getModelCount();
|
||||
List<Model> currentlyRecording = recorder.getCurrentlyRecording();
|
||||
activeRecordings = currentlyRecording.size();
|
||||
String windowTitle = getActiveRecordings(activeRecordings, modelCount) + title;
|
||||
Platform.runLater(() -> primaryStage.setTitle(windowTitle));
|
||||
updateStatus();
|
||||
} 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;
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
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.recorder.Recorder;
|
||||
import ctbrec.sites.Site;
|
||||
import javafx.application.Platform;
|
||||
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 {
|
||||
|
||||
|
@ -54,7 +53,7 @@ public class ClipboardListener implements Runnable {
|
|||
Model m = site.createModelFromUrl(url);
|
||||
if (m != null) {
|
||||
try {
|
||||
recorder.startRecording(m);
|
||||
recorder.addModel(m);
|
||||
DesktopIntegration.notification("Add from clipboard", "Model added", "Model " + m.getDisplayName() + " added");
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
|
||||
DesktopIntegration.notification("Add from clipboard", "Error", "Couldn't add URL from clipboard: " + e.getLocalizedMessage());
|
||||
|
|
|
@ -319,4 +319,16 @@ public class JavaFxModel implements Model {
|
|||
public boolean isRecordingTimeLimited() {
|
||||
return delegate.isRecordingTimeLimited();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMarkedForLaterRecording() {
|
||||
return delegate.isMarkedForLaterRecording();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkedForLaterRecording(boolean marked) {
|
||||
delegate.setMarkedForLaterRecording(marked);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.input.MouseButton;
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@ package ctbrec.ui.action;
|
|||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
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.LoggerFactory;
|
||||
|
@ -26,13 +29,15 @@ public class CheckModelAccountAction {
|
|||
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
public void execute(Predicate<Model> filter) {
|
||||
String buttonText = b.getText();
|
||||
b.setDisable(true);
|
||||
Runnable checker = (() -> {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
List<Model> deletedAccounts = new ArrayList<>();
|
||||
try {
|
||||
List<Model> models = recorder.getModels();
|
||||
List<Model> models = recorder.getModels().stream() //
|
||||
.filter(filter) //
|
||||
.collect(Collectors.toList());
|
||||
int total = models.size();
|
||||
for (int i = 0; i < total; i++) {
|
||||
final int counter = i+1;
|
||||
|
@ -66,6 +71,5 @@ public class CheckModelAccountAction {
|
|||
});
|
||||
}
|
||||
});
|
||||
new Thread(checker).start();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
package ctbrec.ui.action;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class StartRecordingAction extends ModelMassEditAction {
|
||||
|
||||
public StartRecordingAction(Node source, List<? extends Model> models, Recorder recorder) {
|
||||
super(source, models);
|
||||
action = (m) -> {
|
||||
try {
|
||||
recorder.startRecording(m);
|
||||
} catch(Exception e) {
|
||||
recorder.addModel(m);
|
||||
} catch (Exception e) {
|
||||
Platform.runLater(() ->
|
||||
Dialogs.showError(source.getScene(), "Couldn't start recording", "Starting recording of " + m.getName() + " failed", e));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -31,13 +31,6 @@
|
|||
*/
|
||||
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.recorder.Recorder;
|
||||
import ctbrec.ui.action.PlayAction;
|
||||
|
@ -47,15 +40,18 @@ import javafx.event.EventHandler;
|
|||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
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.
|
||||
|
@ -167,7 +163,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
|
|||
follow = new Button("Follow");
|
||||
follow.setOnAction(evt -> {
|
||||
setCursor(Cursor.WAIT);
|
||||
new Thread(new Task<Boolean>() {
|
||||
CompletableFuture.runAsync(new Task<Boolean>() {
|
||||
@Override
|
||||
protected Boolean call() throws Exception {
|
||||
model.getSite().login();
|
||||
|
@ -183,15 +179,15 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
|
|||
}
|
||||
Platform.runLater(() -> setCursor(Cursor.DEFAULT));
|
||||
}
|
||||
}).start();
|
||||
});
|
||||
});
|
||||
record = new Button("Record");
|
||||
record.setOnAction(evt -> {
|
||||
setCursor(Cursor.WAIT);
|
||||
new Thread(new Task<Void>() {
|
||||
CompletableFuture.runAsync(new Task<Void>() {
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
recorder.startRecording(model);
|
||||
recorder.addModel(model);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -199,7 +195,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
|
|||
protected void done() {
|
||||
Platform.runLater(() -> setCursor(Cursor.DEFAULT));
|
||||
}
|
||||
}).start();
|
||||
});
|
||||
});
|
||||
getChildren().addAll(thumb, title, follow, record);
|
||||
|
||||
|
|
|
@ -123,6 +123,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
private SimpleStringProperty path;
|
||||
private SimpleStringProperty downloadFilename;
|
||||
private SimpleBooleanProperty requireAuthentication;
|
||||
private SimpleBooleanProperty totalModelCountInTitle;
|
||||
private SimpleBooleanProperty transportLayerSecurity;
|
||||
private SimpleBooleanProperty fastScrollSpeed;
|
||||
private ExclusiveSelectionProperty recordLocal;
|
||||
|
@ -176,6 +177,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
downloadFilename = new SimpleStringProperty(null, "downloadFilename", settings.downloadFilename);
|
||||
requireAuthentication = new SimpleBooleanProperty(null, "requireAuthentication", settings.requireAuthentication);
|
||||
requireAuthentication.addListener(this::requireAuthenticationChanged);
|
||||
totalModelCountInTitle = new SimpleBooleanProperty(null, "totalModelCountInTitle", settings.totalModelCountInTitle);
|
||||
transportLayerSecurity = new SimpleBooleanProperty(null, "transportLayerSecurity", settings.transportLayerSecurity);
|
||||
recordLocal = new ExclusiveSelectionProperty(null, "localRecording", settings.localRecording, "Local", "Remote");
|
||||
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("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("Total model count in title", totalModelCountInTitle, "Show the total number of models in the title bar"),
|
||||
Setting.of("Start Tab", startTab),
|
||||
Setting.of("Colors (Base / Accent)", new ColorSettingsPane(Config.getInstance())).needsRestart()
|
||||
),
|
||||
|
|
|
@ -33,7 +33,7 @@ import okhttp3.Response;
|
|||
|
||||
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 Cam4 site;
|
||||
private boolean loginRequired;
|
||||
|
@ -86,6 +86,7 @@ public class Cam4UpdateService extends PaginatedScheduledService {
|
|||
String slug = path.substring(1);
|
||||
Cam4Model model = site.createModel(slug);
|
||||
String playlistUrl = profileLink.attr("data-hls-preview-url");
|
||||
model.setDisplayName(HtmlParser.getText(boxHtml, "div.profileBoxTitle a").trim());
|
||||
model.setPlaylistUrl(playlistUrl);
|
||||
model.setPreview("https://snapshots.xcdnpro.com/thumbnails/" + model.getName() + "?s=" + System.currentTimeMillis());
|
||||
model.setDescription(parseDesription(boxHtml));
|
||||
|
|
|
@ -1,24 +1,5 @@
|
|||
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.recorder.Recorder;
|
||||
import ctbrec.sites.camsoda.Camsoda;
|
||||
|
@ -32,14 +13,7 @@ import javafx.concurrent.Task;
|
|||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Alert;
|
||||
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.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
|
@ -48,6 +22,25 @@ import javafx.scene.text.Font;
|
|||
import javafx.scene.text.FontWeight;
|
||||
import okhttp3.Request;
|
||||
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 {
|
||||
|
||||
|
@ -136,7 +129,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
|
|||
});
|
||||
}
|
||||
};
|
||||
new Thread(task).start();
|
||||
CompletableFuture.runAsync(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -202,7 +195,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
private void follow(Model model) {
|
||||
setCursor(Cursor.WAIT);
|
||||
new Thread(() -> {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
SiteUiFactory.getUi(model.getSite()).login();
|
||||
model.follow();
|
||||
|
@ -214,14 +207,14 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
|
|||
setCursor(Cursor.DEFAULT);
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
});
|
||||
}
|
||||
|
||||
private void record(Model model) {
|
||||
setCursor(Cursor.WAIT);
|
||||
new Thread(() -> {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
recorder.startRecording(model);
|
||||
recorder.addModel(model);
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
|
||||
showErrorDialog("Oh no!", "Couldn't add model to the recorder", "Recorder error: " + e.getMessage());
|
||||
} finally {
|
||||
|
@ -229,11 +222,11 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
|
|||
setCursor(Cursor.DEFAULT);
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
});
|
||||
}
|
||||
|
||||
private void loadImage(Model model, ImageView thumb) {
|
||||
new Thread(() -> {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
String url = camsoda.getBaseUrl() + "/api/v1/user/" + model.getName();
|
||||
Request detailRequest = new Request.Builder().url(url).build();
|
||||
|
@ -270,7 +263,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
|
|||
} catch (Exception e) {
|
||||
LOG.error("Couldn't load model details", e);
|
||||
}
|
||||
}).start();
|
||||
});
|
||||
}
|
||||
|
||||
private Node createLabel(String string, boolean bold) {
|
||||
|
|
|
@ -30,12 +30,10 @@ public class LiveJasminFollowedUpdateService extends PaginatedScheduledService {
|
|||
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminFollowedUpdateService.class);
|
||||
private LiveJasmin liveJasmin;
|
||||
private String url;
|
||||
private boolean showOnline = true;
|
||||
|
||||
public LiveJasminFollowedUpdateService(LiveJasmin liveJasmin) {
|
||||
this.liveJasmin = liveJasmin;
|
||||
this.url = liveJasmin.getBaseUrl() + "/en/member/favorite";
|
||||
//this.url = liveJasmin.getBaseUrl() + "/en/free/favourite/get-favourite-list?_dc=" + ts;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -43,15 +41,15 @@ public class LiveJasminFollowedUpdateService extends PaginatedScheduledService {
|
|||
return new Task<List<Model>>() {
|
||||
@Override
|
||||
public List<Model> call() throws IOException {
|
||||
if(!liveJasmin.credentialsAvailable()) {
|
||||
if (!liveJasmin.credentialsAvailable()) {
|
||||
throw new NotLoggedInExcetion("Credentials missing");
|
||||
}
|
||||
|
||||
boolean loggedIn = SiteUiFactory.getUi(liveJasmin).login();
|
||||
if(!loggedIn) {
|
||||
throw new RuntimeException("Couldn't login to livejasmin");
|
||||
if (!loggedIn) {
|
||||
throw new NotLoggedInExcetion("Couldn't login to livejasmin");
|
||||
}
|
||||
//LOG.debug("Fetching page {}", url);
|
||||
LOG.debug("Fetching page {}", url);
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
|
@ -80,8 +78,4 @@ public class LiveJasminFollowedUpdateService extends PaginatedScheduledService {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void setShowOnline(boolean showOnline) {
|
||||
this.showOnline = showOnline;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ import ctbrec.ui.DesktopIntegration;
|
|||
import ctbrec.ui.action.FollowAction;
|
||||
import ctbrec.ui.action.PlayAction;
|
||||
import ctbrec.ui.action.StartRecordingAction;
|
||||
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
|
||||
import ctbrec.ui.controls.SearchBox;
|
||||
import ctbrec.ui.tabs.TabSelectionListener;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
|
@ -341,7 +342,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
|||
MenuItem follow = new MenuItem("Follow");
|
||||
follow.setOnAction(e -> new FollowAction(getTabPane(), selectedModels).execute());
|
||||
|
||||
ContextMenu menu = new ContextMenu();
|
||||
ContextMenu menu = new CustomMouseBehaviorContextMenu();
|
||||
menu.getItems().addAll(startRecording, copyUrl, openInPlayer, openInBrowser, follow);
|
||||
|
||||
if (selectedModels.size() > 1) {
|
||||
|
@ -467,7 +468,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private void showColumnSelection(ActionEvent evt) {
|
||||
ContextMenu menu = new ContextMenu();
|
||||
ContextMenu menu = new CustomMouseBehaviorContextMenu();
|
||||
for (TableColumn<ModelTableRow, ?> tc : columns) {
|
||||
CheckMenuItem item = new CheckMenuItem(tc.getText());
|
||||
item.setSelected(isColumnEnabled(tc));
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.json.JSONObject;
|
|||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.UnexpectedResponseException;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.sites.showup.Showup;
|
||||
import ctbrec.sites.showup.ShowupModel;
|
||||
|
@ -55,7 +56,7 @@ public class ShowupFollowedUpdateService extends PaginatedScheduledService {
|
|||
.filter(m -> onlineModels.containsKey(m.getName()) == showOnline)
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
throw new RuntimeException("Request was not successful: " + body);
|
||||
throw new UnexpectedResponseException("Request was not successful: " + body);
|
||||
}
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
|
@ -83,7 +84,7 @@ public class ShowupFollowedUpdateService extends PaginatedScheduledService {
|
|||
for (int i = 0; i < online.length(); i++) {
|
||||
JSONObject m = online.getJSONObject(i);
|
||||
String preview = site.getBaseUrl() + "/files/" + m.optString("big_img") + ".jpg";
|
||||
//onlineModels.put(m.optLong("uid"), preview);
|
||||
onlineModels.put(String.valueOf(m.optLong("uid")), preview);
|
||||
}
|
||||
return onlineModels;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +1,14 @@
|
|||
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.Model;
|
||||
import ctbrec.Recording;
|
||||
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.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.*;
|
||||
import ctbrec.ui.action.*;
|
||||
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
|
||||
import ctbrec.ui.controls.DateTimeCellFactory;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import ctbrec.ui.controls.SearchBox;
|
||||
|
@ -69,33 +26,13 @@ import javafx.concurrent.WorkerStateEvent;
|
|||
import javafx.event.ActionEvent;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Alert;
|
||||
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.*;
|
||||
import javafx.scene.control.TableColumn.CellEditEvent;
|
||||
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.PropertyValueFactory;
|
||||
import javafx.scene.control.cell.TextFieldTableCell;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
@ -104,6 +41,30 @@ import javafx.util.Callback;
|
|||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
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 {
|
||||
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.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());
|
||||
checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder)
|
||||
.execute(Predicate.not(Model::isMarkedForLaterRecording)));
|
||||
|
||||
HBox filterContainer = new HBox();
|
||||
filterContainer.setSpacing(0);
|
||||
|
@ -404,7 +366,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
Model newModel = site.createModelFromUrl(url);
|
||||
if (newModel != null) {
|
||||
try {
|
||||
recorder.startRecording(newModel);
|
||||
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);
|
||||
}
|
||||
|
@ -429,7 +391,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) {
|
||||
try {
|
||||
Model m = site.createModel(modelName);
|
||||
recorder.startRecording(m);
|
||||
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);
|
||||
}
|
||||
|
@ -584,6 +546,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
List<Model> onlineModels = recorder.getOnlineModels();
|
||||
return recorder.getModels()
|
||||
.stream()
|
||||
.filter(Predicate.not(Model::isMarkedForLaterRecording))
|
||||
.map(JavaFxModel::new)
|
||||
.peek(fxm -> { // NOSONAR
|
||||
for (Recording recording : recordings) {
|
||||
|
@ -641,6 +604,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
MenuItem stop = new MenuItem("Remove Model");
|
||||
stop.setOnAction(e -> stopAction(selectedModels));
|
||||
MenuItem recordLater = new MenuItem("Record Later");
|
||||
recordLater.setOnAction(e -> recordLater(selectedModels));
|
||||
|
||||
MenuItem copyUrl = new MenuItem("Copy URL");
|
||||
copyUrl.setOnAction(e -> {
|
||||
|
@ -675,8 +640,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
MenuItem openRecDir = new MenuItem("Open recording directory");
|
||||
openRecDir.setOnAction(e -> new OpenRecordingsDir(table, selectedModels.get(0)).execute());
|
||||
|
||||
ContextMenu menu = new ContextMenu(stop);
|
||||
UiUtils.disableRightClickFor(menu);
|
||||
ContextMenu menu = new CustomMouseBehaviorContextMenu(stop, recordLater);
|
||||
if (selectedModels.size() == 1) {
|
||||
menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording);
|
||||
menu.getItems().add(stopRecordingAt);
|
||||
|
@ -773,7 +737,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
alert.showAndWait();
|
||||
}
|
||||
|
||||
private void stopAction(List<JavaFxModel> selectedModels) {
|
||||
private boolean stopAction(List<JavaFxModel> selectedModels) {
|
||||
boolean confirmed = true;
|
||||
if (Config.getInstance().getSettings().confirmationForDangerousActions) {
|
||||
int n = selectedModels.size();
|
||||
|
@ -788,6 +752,21 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -45,11 +45,11 @@ import ctbrec.ui.DesktopIntegration;
|
|||
import ctbrec.ui.FileDownload;
|
||||
import ctbrec.ui.JavaFxRecording;
|
||||
import ctbrec.ui.Player;
|
||||
import ctbrec.ui.UiUtils;
|
||||
import ctbrec.ui.action.FollowAction;
|
||||
import ctbrec.ui.action.PauseAction;
|
||||
import ctbrec.ui.action.PlayAction;
|
||||
import ctbrec.ui.action.StopRecordingAction;
|
||||
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
|
||||
import ctbrec.ui.controls.DateTimeCellFactory;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import ctbrec.ui.controls.Toast;
|
||||
|
@ -389,11 +389,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private ContextMenu createContextMenu(List<JavaFxRecording> recordings) {
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
ContextMenu contextMenu = new CustomMouseBehaviorContextMenu();
|
||||
contextMenu.setHideOnEscape(true);
|
||||
contextMenu.setAutoHide(true);
|
||||
contextMenu.setAutoFix(true);
|
||||
UiUtils.disableRightClickFor(contextMenu);
|
||||
|
||||
JavaFxRecording first = recordings.get(0);
|
||||
MenuItem openInPlayer = new MenuItem("Open in Player");
|
||||
|
|
|
@ -511,12 +511,13 @@ public class ThumbCell extends StackPane {
|
|||
new Thread(() -> {
|
||||
try {
|
||||
if (start) {
|
||||
recorder.startRecording(model);
|
||||
setRecording(true);
|
||||
recorder.addModel(model);
|
||||
setRecording(!model.isMarkedForLaterRecording());
|
||||
} else {
|
||||
recorder.stopRecording(model);
|
||||
setRecording(false);
|
||||
}
|
||||
update();
|
||||
} catch (Exception e1) {
|
||||
LOG.error(COULDNT_START_STOP_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() {
|
||||
return model;
|
||||
}
|
||||
|
@ -684,6 +690,7 @@ public class ThumbCell extends StackPane {
|
|||
|
||||
void addInPausedState() {
|
||||
model.setSuspended(true);
|
||||
model.setMarkedForLaterRecording(false);
|
||||
startStopAction(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,5 @@
|
|||
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.Model;
|
||||
import ctbrec.event.EventBusHolder;
|
||||
|
@ -33,24 +7,12 @@ import ctbrec.recorder.Recorder;
|
|||
import ctbrec.sites.Site;
|
||||
import ctbrec.sites.mfc.MyFreeCamsClient;
|
||||
import ctbrec.sites.mfc.MyFreeCamsModel;
|
||||
import ctbrec.ui.AutosizeAlert;
|
||||
import ctbrec.ui.DesktopIntegration;
|
||||
import ctbrec.ui.SiteUiFactory;
|
||||
import ctbrec.ui.TipDialog;
|
||||
import ctbrec.ui.TokenLabel;
|
||||
import ctbrec.ui.UiUtils;
|
||||
import ctbrec.ui.*;
|
||||
import ctbrec.ui.action.IgnoreModelsAction;
|
||||
import ctbrec.ui.action.OpenRecordingsDir;
|
||||
import ctbrec.ui.action.SetStopDateAction;
|
||||
import ctbrec.ui.controls.FasterVerticalScrollPaneSkin;
|
||||
import ctbrec.ui.controls.SearchBox;
|
||||
import ctbrec.ui.controls.SearchPopover;
|
||||
import ctbrec.ui.controls.SearchPopoverTreeList;
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.ParallelTransition;
|
||||
import javafx.animation.ScaleTransition;
|
||||
import javafx.animation.TranslateTransition;
|
||||
import ctbrec.ui.controls.*;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
@ -66,33 +28,24 @@ import javafx.geometry.Insets;
|
|||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.Alert;
|
||||
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.control.*;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.transform.Transform;
|
||||
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 {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ThumbOverviewTab.class);
|
||||
|
@ -123,6 +76,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
SearchPopoverTreeList popoverTreeList = new SearchPopoverTreeList();
|
||||
double imageAspectRatio = 3.0 / 4.0;
|
||||
private final SimpleBooleanProperty preserveAspectRatio = new SimpleBooleanProperty(true);
|
||||
ProgressIndicator progressIndicator;
|
||||
Label noResultsFound = new Label("Nothing found!");
|
||||
|
||||
private ComboBox<Integer> thumbWidth;
|
||||
|
||||
|
@ -140,6 +95,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
grid.setHgap(5);
|
||||
grid.setVgap(5);
|
||||
|
||||
progressIndicator = new ProgressIndicator();
|
||||
progressIndicator.setPrefSize(100, 100);
|
||||
|
||||
SearchBox filterInput = new SearchBox(false);
|
||||
filterInput.setPromptText("Filter models on this page");
|
||||
filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> {
|
||||
|
@ -361,6 +319,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
gridLock.lock();
|
||||
try {
|
||||
ObservableList<Node> nodes = grid.getChildren();
|
||||
nodes.remove(progressIndicator);
|
||||
nodes.remove(noResultsFound);
|
||||
|
||||
// first remove models, which are not in the updated list
|
||||
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
|
||||
moveActiveRecordingsToFront();
|
||||
|
||||
// show "empty" label, if grid is still empty
|
||||
if (grid.getChildren().isEmpty()) {
|
||||
nodes.add(noResultsFound);
|
||||
}
|
||||
} finally {
|
||||
gridLock.unlock();
|
||||
}
|
||||
|
@ -460,28 +425,29 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
private ContextMenu createContextMenu(ThumbCell cell) {
|
||||
Model model = cell.getModel();
|
||||
boolean modelIsTrackedByRecorder = recorder.isTracked(model);
|
||||
|
||||
MenuItem openInPlayer = new MenuItem("Open in Player");
|
||||
openInPlayer.setOnAction(e -> startPlayer(getSelectedThumbCells(cell)));
|
||||
|
||||
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");
|
||||
stop.setOnAction(e -> startStopAction(e, getSelectedThumbCells(cell), false));
|
||||
MenuItem startStop = recorder.isTracked(cell.getModel()) ? stop : start;
|
||||
stop.setOnAction(e -> startStopAction(getSelectedThumbCells(cell), false));
|
||||
MenuItem startStop = recorder.isTracked(model) ? stop : start;
|
||||
|
||||
MenuItem recordUntil = new MenuItem("Start Recording Until");
|
||||
recordUntil.setOnAction(e -> startRecordingWithTimeLimit(getSelectedThumbCells(cell)));
|
||||
MenuItem addPaused = new MenuItem("Add in paused state");
|
||||
addPaused.setOnAction(e -> addPaused(getSelectedThumbCells(cell)));
|
||||
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");
|
||||
pause.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), true));
|
||||
MenuItem resume = new MenuItem("Resume Recording");
|
||||
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");
|
||||
follow.setOnAction(e -> follow(getSelectedThumbCells(cell), true));
|
||||
|
@ -495,45 +461,54 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
refresh.setOnAction(e -> refresh());
|
||||
|
||||
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 sendTip = createTipMenuItem(cell);
|
||||
|
||||
configureItemsForSelection(cell, openInPlayer, copyUrl, sendTip);
|
||||
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
ContextMenu contextMenu = new CustomMouseBehaviorContextMenu();
|
||||
contextMenu.setAutoHide(true);
|
||||
contextMenu.setHideOnEscape(true);
|
||||
contextMenu.setAutoFix(true);
|
||||
contextMenu.getItems().addAll(openInPlayer, new SeparatorMenuItem(), startStop);
|
||||
UiUtils.disableRightClickFor(contextMenu);
|
||||
if(modelIsTrackedByRecorder) {
|
||||
contextMenu.getItems().add(pauseResume);
|
||||
if (modelIsTrackedByRecorder) {
|
||||
contextMenu.getItems().addAll(pauseResume, recordLater);
|
||||
} 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());
|
||||
if(site.supportsFollow()) {
|
||||
if (site.supportsFollow()) {
|
||||
MenuItem followOrUnFollow = (this instanceof FollowedTab) ? unfollow : follow;
|
||||
followOrUnFollow.setDisable(!site.credentialsAvailable());
|
||||
contextMenu.getItems().add(followOrUnFollow);
|
||||
}
|
||||
if(site.supportsTips()) {
|
||||
if (site.supportsTips()) {
|
||||
contextMenu.getItems().add(sendTip);
|
||||
}
|
||||
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");
|
||||
debug.setOnAction(e -> MyFreeCamsClient.getInstance().getSessionState(cell.getModel()));
|
||||
debug.setOnAction(e -> MyFreeCamsClient.getInstance().getSessionState(model));
|
||||
contextMenu.getItems().add(debug);
|
||||
}
|
||||
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
private void recordLater(List<ThumbCell> list) {
|
||||
for (ThumbCell cell : list) {
|
||||
cell.recordLater();
|
||||
}
|
||||
}
|
||||
|
||||
private void startRecordingWithTimeLimit(List<ThumbCell> list) {
|
||||
for (ThumbCell cell : list) {
|
||||
cell.getModel().setMarkedForLaterRecording(false);
|
||||
cell.startStopAction(true);
|
||||
new SetStopDateAction(cell, cell.getModel(), recorder).execute();
|
||||
}
|
||||
|
@ -691,8 +666,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
return 0;
|
||||
}
|
||||
|
||||
private void startStopAction(ActionEvent e, List<ThumbCell> selection, boolean start) {
|
||||
private void startStopAction(List<ThumbCell> selection, boolean start) {
|
||||
for (ThumbCell thumbCell : selection) {
|
||||
thumbCell.getModel().setSuspended(false);
|
||||
thumbCell.getModel().setMarkedForLaterRecording(false);
|
||||
thumbCell.startStopAction(start);
|
||||
}
|
||||
}
|
||||
|
@ -768,12 +745,14 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
// remove the ones from grid, which don't match
|
||||
for (Iterator<Node> iterator = grid.getChildren().iterator(); iterator.hasNext();) {
|
||||
Node node = iterator.next();
|
||||
ThumbCell cell = (ThumbCell) node;
|
||||
Model m = cell.getModel();
|
||||
if(!matches(m, filter)) {
|
||||
iterator.remove();
|
||||
filteredThumbCells.add(cell);
|
||||
cell.setSelected(false);
|
||||
if (node instanceof ThumbCell) {
|
||||
ThumbCell cell = (ThumbCell) node;
|
||||
Model m = cell.getModel();
|
||||
if (!matches(m, filter)) {
|
||||
iterator.remove();
|
||||
filteredThumbCells.add(cell);
|
||||
cell.setSelected(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -881,8 +860,12 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
|
||||
@Override
|
||||
public void selected() {
|
||||
grid.getChildren().remove(noResultsFound);
|
||||
if (grid.getChildren().isEmpty()) {
|
||||
grid.getChildren().add(progressIndicator);
|
||||
}
|
||||
queue.clear();
|
||||
if(updateService != null) {
|
||||
if (updateService != null) {
|
||||
State s = updateService.getState();
|
||||
if (s != State.SCHEDULED && s != State.RUNNING) {
|
||||
updateService.reset();
|
||||
|
@ -901,7 +884,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
|
||||
for (Iterator<Node> iterator = grid.getChildren().iterator(); iterator.hasNext();) {
|
||||
Node node = iterator.next();
|
||||
if(node instanceof ThumbCell) {
|
||||
if (node instanceof ThumbCell) {
|
||||
ThumbCell thumbCell = (ThumbCell) node;
|
||||
thumbCell.releaseResources();
|
||||
iterator.remove();
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package ctbrec.ui.tabs;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.ui.CamrecApplication;
|
||||
import ctbrec.ui.CamrecApplication.Release;
|
||||
|
@ -15,10 +20,8 @@ import javafx.scene.layout.Priority;
|
|||
import javafx.scene.layout.VBox;
|
||||
import okhttp3.Request;
|
||||
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);
|
||||
|
||||
|
@ -37,14 +40,17 @@ public class UpdateTab extends Tab {
|
|||
vbox.setAlignment(Pos.CENTER);
|
||||
changelog = new TextArea();
|
||||
changelog.setEditable(false);
|
||||
changelog.setText("Loading changelog...");
|
||||
vbox.getChildren().add(changelog);
|
||||
VBox.setVgrow(changelog, Priority.ALWAYS);
|
||||
setContent(vbox);
|
||||
}
|
||||
|
||||
new Thread(() -> {
|
||||
public void loadChangeLog() {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
Request req = new Request.Builder().url("https://pastebin.com/raw/fiAPtM0s").build();
|
||||
try(Response resp = CamrecApplication.httpClient.execute(req)) {
|
||||
if(resp.isSuccessful()) {
|
||||
try (Response resp = CamrecApplication.httpClient.execute(req)) {
|
||||
if (resp.isSuccessful()) {
|
||||
changelog.setText(resp.body().string());
|
||||
} else {
|
||||
throw new HttpException(resp.code(), resp.message());
|
||||
|
@ -53,6 +59,16 @@ public class UpdateTab extends Tab {
|
|||
LOG.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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import ch.qos.logback.classic.spi.IThrowableProxy;
|
|||
import ch.qos.logback.classic.spi.LoggingEvent;
|
||||
import ch.qos.logback.classic.spi.StackTraceElementProxy;
|
||||
import ctbrec.event.EventBusHolder;
|
||||
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
|
||||
import ctbrec.ui.controls.SearchBox;
|
||||
import javafx.application.Platform;
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,9 @@
|
|||
|
||||
<logger name="ctbrec.LoggingInterceptor" 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.RecorderServlet" level="INFO"/>
|
||||
<logger name="ctbrec.recorder.ThreadPoolScaler" level="DEBUG"/>
|
||||
|
|
|
@ -6,11 +6,7 @@
|
|||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<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.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
|
|
|
@ -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.enumIdentifier=error
|
||||
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.source=1.8
|
||||
|
|
|
@ -33,6 +33,7 @@ public abstract class AbstractModel implements Model {
|
|||
private int streamUrlIndex = -1;
|
||||
private int priority = 50;
|
||||
private boolean suspended = false;
|
||||
private boolean markedForLaterRecording = false;
|
||||
protected transient Site site;
|
||||
protected State onlineState = State.UNKNOWN;
|
||||
private Instant lastSeen;
|
||||
|
@ -145,6 +146,16 @@ public abstract class AbstractModel implements Model {
|
|||
this.suspended = suspended;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMarkedForLaterRecording() {
|
||||
return markedForLaterRecording;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkedForLaterRecording(boolean marked) {
|
||||
this.markedForLaterRecording = marked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||
return onlineState;
|
||||
|
|
|
@ -174,6 +174,13 @@ public class Config {
|
|||
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) {
|
||||
|
|
|
@ -122,6 +122,10 @@ public interface Model extends Comparable<Model>, Serializable {
|
|||
|
||||
public void setSuspended(boolean suspended);
|
||||
|
||||
public boolean isMarkedForLaterRecording();
|
||||
|
||||
public void setMarkedForLaterRecording(boolean marked);
|
||||
|
||||
public Download createDownload();
|
||||
|
||||
public void setPriority(int priority);
|
||||
|
|
|
@ -118,6 +118,10 @@ public class Settings {
|
|||
public String proxyPort;
|
||||
public ProxyType proxyType = ProxyType.DIRECT;
|
||||
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 String[] recordedModelsColumnIds = new String[0];
|
||||
public String recordedModelsSortColumn = "";
|
||||
|
@ -128,6 +132,7 @@ public class Settings {
|
|||
public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
|
||||
public String recordingsSortColumn = "";
|
||||
public String recordingsSortType = "";
|
||||
public List<Model> recordLater = new ArrayList<>();
|
||||
public boolean recordSingleFile = false;
|
||||
public boolean removeRecordingAfterPostProcessing = false;
|
||||
public boolean requireAuthentication = false;
|
||||
|
@ -147,6 +152,7 @@ public class Settings {
|
|||
public String stripchatUsername = "";
|
||||
public String stripchatPassword = "";
|
||||
public boolean stripchatUseXhamster = false;
|
||||
public boolean totalModelCountInTitle = false;
|
||||
public boolean transportLayerSecurity = true;
|
||||
public int thumbWidth = 180;
|
||||
public boolean updateThumbnails = true;
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package ctbrec;
|
||||
|
||||
public class UnexpectedResponseException extends RuntimeException {
|
||||
|
||||
public UnexpectedResponseException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,15 @@
|
|||
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.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -13,39 +21,11 @@ import java.security.NoSuchAlgorithmException;
|
|||
import java.security.SecureRandom;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.KeyManager;
|
||||
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;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
public abstract class HttpClient {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HttpClient.class);
|
||||
|
@ -293,4 +273,12 @@ public abstract class HttpClient {
|
|||
}
|
||||
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("[]");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,51 +35,41 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
|||
@Override
|
||||
public Model fromJson(JsonReader reader) throws IOException {
|
||||
reader.beginObject();
|
||||
String name = null;
|
||||
String description = null;
|
||||
String url = null;
|
||||
Object type = null;
|
||||
int streamUrlIndex = -1;
|
||||
int priority;
|
||||
boolean suspended = false;
|
||||
|
||||
Model model = null;
|
||||
while(reader.hasNext()) {
|
||||
|
||||
while (reader.hasNext()) {
|
||||
try {
|
||||
Token token = reader.peek();
|
||||
if(token == Token.NAME) {
|
||||
if (token == Token.NAME) {
|
||||
String key = reader.nextName();
|
||||
if(key.equals("type")) {
|
||||
if (key.equals("type")) {
|
||||
type = reader.readJsonValue();
|
||||
Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()).toString());
|
||||
model = (Model) modelClass.getDeclaredConstructor().newInstance();
|
||||
} else if(key.equals("name")) {
|
||||
name = reader.nextString();
|
||||
model.setName(name);
|
||||
} else if(key.equals("description")) {
|
||||
description = reader.nextString();
|
||||
model.setDescription(description);
|
||||
} else if(key.equals("url")) {
|
||||
url = reader.nextString();
|
||||
model.setUrl(url);
|
||||
} else if(key.equals("priority")) {
|
||||
priority = reader.nextInt();
|
||||
model.setPriority(priority);
|
||||
} else if(key.equals("streamUrlIndex")) {
|
||||
streamUrlIndex = reader.nextInt();
|
||||
model.setStreamUrlIndex(streamUrlIndex);
|
||||
} else if(key.equals("suspended")) {
|
||||
suspended = reader.nextBoolean();
|
||||
model.setSuspended(suspended);
|
||||
} else if(key.equals("lastSeen")) {
|
||||
} else if (key.equals("name")) {
|
||||
model.setName(reader.nextString());
|
||||
} else if (key.equals("description")) {
|
||||
model.setDescription(reader.nextString());
|
||||
} else if (key.equals("url")) {
|
||||
model.setUrl(reader.nextString());
|
||||
} else if (key.equals("priority")) {
|
||||
model.setPriority(reader.nextInt());
|
||||
} else if (key.equals("streamUrlIndex")) {
|
||||
model.setStreamUrlIndex(reader.nextInt());
|
||||
} else if (key.equals("suspended")) {
|
||||
model.setSuspended(reader.nextBoolean());
|
||||
} else if (key.equals("markedForLater")) {
|
||||
model.setMarkedForLaterRecording(reader.nextBoolean());
|
||||
} else if (key.equals("lastSeen")) {
|
||||
model.setLastSeen(Instant.ofEpochMilli(reader.nextLong()));
|
||||
} else if(key.equals("lastRecorded")) {
|
||||
} else if (key.equals("lastRecorded")) {
|
||||
model.setLastRecorded(Instant.ofEpochMilli(reader.nextLong()));
|
||||
} else if(key.equals("recordUntil")) {
|
||||
} else if (key.equals("recordUntil")) {
|
||||
model.setRecordUntil(Instant.ofEpochMilli(reader.nextLong()));
|
||||
} else if(key.equals("recordUntilSubsequentAction")) {
|
||||
} else if (key.equals("recordUntilSubsequentAction")) {
|
||||
model.setRecordUntilSubsequentAction(SubsequentAction.valueOf(reader.nextString()));
|
||||
} else if(key.equals("siteSpecific")) {
|
||||
} else if (key.equals("siteSpecific")) {
|
||||
reader.beginObject();
|
||||
try {
|
||||
model.readSiteSpecificData(reader);
|
||||
|
@ -92,15 +82,16 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
|||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
|
||||
if(sites != null) {
|
||||
if (sites != null) {
|
||||
for (Site site : sites) {
|
||||
if(site.isSiteForModel(model)) {
|
||||
if (site.isSiteForModel(model)) {
|
||||
model.setSite(site);
|
||||
}
|
||||
}
|
||||
|
@ -118,6 +109,7 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
|||
writer.name("priority").value(model.getPriority());
|
||||
writer.name("streamUrlIndex").value(model.getStreamUrlIndex());
|
||||
writer.name("suspended").value(model.isSuspended());
|
||||
writer.name("markedForLater").value(model.isMarkedForLaterRecording());
|
||||
writer.name("lastSeen").value(model.getLastSeen().toEpochMilli());
|
||||
writer.name("lastRecorded").value(model.getLastRecorded().toEpochMilli());
|
||||
writer.name("recordUntil").value(model.getRecordUntil().toEpochMilli());
|
||||
|
@ -130,7 +122,7 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
|||
}
|
||||
|
||||
private void writeValueIfSet(JsonWriter writer, String name, String value) throws IOException {
|
||||
if(value != null) {
|
||||
if (value != null) {
|
||||
writer.name(name).value(value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -244,8 +244,13 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
if (!models.contains(model)) {
|
||||
public void addModel(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
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);
|
||||
recorderLock.lock();
|
||||
try {
|
||||
|
@ -257,7 +262,6 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
} finally {
|
||||
recorderLock.unlock();
|
||||
}
|
||||
|
||||
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
|
||||
public List<Model> getModels() {
|
||||
recorderLock.lock();
|
||||
|
@ -540,7 +534,9 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
int index = models.indexOf(model);
|
||||
Model m = models.get(index);
|
||||
m.setSuspended(false);
|
||||
m.setMarkedForLaterRecording(false);
|
||||
model.setSuspended(false);
|
||||
model.setMarkedForLaterRecording(false);
|
||||
config.save();
|
||||
startRecordingProcess(m);
|
||||
} 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
|
||||
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();
|
||||
try {
|
||||
int index = models.indexOf(model);
|
||||
int index = models.indexOf(m);
|
||||
if (index >= 0) {
|
||||
Model m = models.get(index);
|
||||
return m.isSuspended();
|
||||
return Optional.of(models.get(index));
|
||||
} else {
|
||||
return false;
|
||||
return Optional.empty();
|
||||
}
|
||||
} finally {
|
||||
recorderLock.unlock();
|
||||
|
@ -763,4 +774,9 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
LOG.info("Resuming recorder");
|
||||
recording = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModelCount() {
|
||||
return (int) models.stream().filter(m -> !m.isMarkedForLaterRecording()).count();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,7 +82,9 @@ public class OnlineMonitor extends Thread {
|
|||
// submit online check jobs to the executor for the model's site
|
||||
List<Future<?>> futures = new LinkedList<>();
|
||||
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;
|
||||
} else {
|
||||
futures.add(updateModel(model));
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
package ctbrec.recorder;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.io.HttpClient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.io.HttpClient;
|
||||
|
||||
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 stopRecordingAt(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
|
||||
|
@ -59,6 +59,8 @@ public interface Recorder {
|
|||
|
||||
public boolean isSuspended(Model model);
|
||||
|
||||
public boolean isMarkedForLaterRecording(Model model);
|
||||
|
||||
/**
|
||||
* Returns only the models from getModels(), which are online
|
||||
* @return
|
||||
|
@ -140,4 +142,6 @@ public interface Recorder {
|
|||
* @throws InvalidKeyException
|
||||
*/
|
||||
public void resume() throws InvalidKeyException, NoSuchAlgorithmException, IOException;
|
||||
|
||||
public int getModelCount();
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ public class RecordingPreconditions {
|
|||
void check(Model model) throws IOException {
|
||||
ensureRecorderIsActive();
|
||||
ensureModelIsNotSuspended(model);
|
||||
ensureModelIsNotMarkedForLaterRecording(model);
|
||||
ensureRecordUntilIsInFuture(model);
|
||||
ensureNoRecordingRunningForModel(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() {
|
||||
if (!recorder.isRecording()) {
|
||||
throw new PreconditionNotMetException("Recorder is not in recording mode");
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
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.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
@ -7,38 +27,7 @@ import java.security.InvalidKeyException;
|
|||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
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;
|
||||
import java.util.*;
|
||||
|
||||
public class RemoteRecorder implements Recorder {
|
||||
|
||||
|
@ -86,7 +75,7 @@ public class RemoteRecorder implements Recorder {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
public void addModel(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
sendRequest("start", model);
|
||||
}
|
||||
|
||||
|
@ -179,17 +168,27 @@ public class RemoteRecorder implements Recorder {
|
|||
|
||||
@Override
|
||||
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
|
||||
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) {
|
||||
Model m = models.get(index);
|
||||
return m.isSuspended();
|
||||
return Optional.of(models.get(index));
|
||||
} else {
|
||||
return false;
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -588,4 +587,9 @@ public class RemoteRecorder implements Recorder {
|
|||
public void resume() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
|
||||
sendRequest("resumeRecorder");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getModelCount() {
|
||||
return (int) models.stream().filter(m -> !m.isMarkedForLaterRecording()).count();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -272,6 +272,7 @@ public class DashDownload extends AbstractDownload {
|
|||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private boolean splitRecording() {
|
||||
if (splittingStrategy.splitNecessary(this)) {
|
||||
internalStop();
|
||||
|
|
|
@ -1,17 +1,5 @@
|
|||
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.Model;
|
||||
import ctbrec.StringUtil;
|
||||
|
@ -20,6 +8,18 @@ import ctbrec.io.HttpException;
|
|||
import ctbrec.sites.AbstractSite;
|
||||
import okhttp3.Request;
|
||||
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 {
|
||||
|
||||
|
@ -47,8 +47,9 @@ public class Cam4 extends AbstractSite {
|
|||
public Cam4Model createModel(String name) {
|
||||
Cam4Model m = new Cam4Model();
|
||||
m.setSite(this);
|
||||
m.setName(name);
|
||||
m.setUrl(getBaseUrl() + '/' + name + '/');
|
||||
m.setDisplayName(name);
|
||||
m.setName(name.toLowerCase());
|
||||
m.setUrl(getBaseUrl() + '/' + m.getName() + '/');
|
||||
return m;
|
||||
}
|
||||
|
||||
|
@ -129,7 +130,7 @@ public class Cam4 extends AbstractSite {
|
|||
.build();
|
||||
try(Response response = getHttpClient().execute(req)) {
|
||||
if(response.isSuccessful()) {
|
||||
String body = response.body().string();
|
||||
String body = bodyToJsonObject(response);
|
||||
JSONArray results = new JSONArray(body);
|
||||
for (int i = 0; i < results.length(); i++) {
|
||||
JSONObject result = results.getJSONObject(i);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package ctbrec.sites.cam4;
|
||||
|
||||
import static ctbrec.Model.State.*;
|
||||
import static ctbrec.io.HttpClient.*;
|
||||
import static ctbrec.io.HttpConstants.*;
|
||||
import static java.util.regex.Pattern.*;
|
||||
|
||||
|
@ -8,7 +9,10 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
@ -36,6 +40,8 @@ import ctbrec.Config;
|
|||
import ctbrec.StringUtil;
|
||||
import ctbrec.io.HtmlParser;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.recorder.download.HttpHeaderFactory;
|
||||
import ctbrec.recorder.download.HttpHeaderFactoryImpl;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.Request;
|
||||
|
@ -103,18 +109,14 @@ public class Cam4Model extends AbstractModel {
|
|||
|
||||
@Override
|
||||
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||
if(failFast) {
|
||||
return onlineState;
|
||||
} else {
|
||||
if(onlineState == UNKNOWN) {
|
||||
try {
|
||||
loadModelDetails();
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Couldn't load model details {}", e.getMessage());
|
||||
}
|
||||
if (!failFast && onlineState == UNKNOWN) {
|
||||
try {
|
||||
loadModelDetails();
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Couldn't load model details {}", e.getMessage());
|
||||
}
|
||||
return onlineState;
|
||||
}
|
||||
return onlineState;
|
||||
}
|
||||
|
||||
private String getPlaylistUrl() throws IOException {
|
||||
|
@ -146,8 +148,8 @@ public class Cam4Model extends AbstractModel {
|
|||
.build(); // @formatter:on
|
||||
try (Response response = site.getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
JSONObject json = new JSONObject(response.body().string());
|
||||
LOG.trace(json.toString(2));
|
||||
JSONObject json = new JSONObject(bodyToJsonObject(response));
|
||||
if (LOG.isTraceEnabled()) LOG.trace(json.toString(2));
|
||||
if (json.has("canUseCDN")) {
|
||||
if (json.getBoolean("canUseCDN")) {
|
||||
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);
|
||||
String masterUrl = getPlaylistUrl();
|
||||
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||
String segmentUri = baseUrl + playlist.getUri();
|
||||
src.mediaPlaylistUrl = segmentUri;
|
||||
src.mediaPlaylistUrl = baseUrl + playlist.getUri();
|
||||
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||
sources.add(src);
|
||||
}
|
||||
|
@ -201,19 +202,20 @@ public class Cam4Model extends AbstractModel {
|
|||
}
|
||||
|
||||
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
|
||||
String playlistUrl = getPlaylistUrl();
|
||||
LOG.trace("Loading master playlist [{}]", playlistUrl);
|
||||
Request req = new Request.Builder().url(playlistUrl).build();
|
||||
String masterPlaylistUrl = getPlaylistUrl();
|
||||
LOG.trace("Loading master playlist [{}]", masterPlaylistUrl);
|
||||
Request.Builder builder = new Request.Builder().url(masterPlaylistUrl);
|
||||
getHttpHeaderFactory().createMasterPlaylistHeaders().forEach(builder::header);
|
||||
Request req = builder.build();
|
||||
|
||||
try (Response response = site.getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
InputStream inputStream = response.body().byteStream();
|
||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||
Playlist playlist = parser.parse();
|
||||
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||
return master;
|
||||
return playlist.getMasterPlaylist();
|
||||
} 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;
|
||||
}
|
||||
|
||||
public class ModelDetailsEmptyException extends Exception {
|
||||
public ModelDetailsEmptyException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUrl(String url) {
|
||||
String normalizedUrl = url.toLowerCase();
|
||||
|
@ -335,4 +331,22 @@ public class Cam4Model extends AbstractModel {
|
|||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package ctbrec.sites.manyvids;
|
|||
import static ctbrec.io.HttpConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
@ -258,13 +260,19 @@ public class MVLive extends AbstractSite {
|
|||
|
||||
@Override
|
||||
public Model createModelFromUrl(String url) {
|
||||
Matcher m = Pattern.compile("https://live.manyvids.com/(?:stream/)?(.*?)(?:/.*?)?").matcher(url.trim());
|
||||
if(m.matches()) {
|
||||
return createModel(m.group(1));
|
||||
}
|
||||
m = Pattern.compile("https://www.manyvids.com/MVLive/(.*?)/\\d+/?").matcher(url.trim());
|
||||
if(m.matches()) {
|
||||
return createModel(m.group(1));
|
||||
try {
|
||||
Matcher m = Pattern.compile("https://live.manyvids.com/(?:stream/)?(.*?)(?:/.*?)?").matcher(url.trim());
|
||||
if (m.matches()) {
|
||||
String modelName = URLDecoder.decode(m.group(1), "utf-8");
|
||||
return createModel(modelName);
|
||||
}
|
||||
m = Pattern.compile("https://www.manyvids.com/MVLive/(.*?)/\\d+/?").matcher(url.trim());
|
||||
if (m.matches()) {
|
||||
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);
|
||||
|
|
|
@ -5,7 +5,7 @@ After=network.target
|
|||
[Service]
|
||||
WorkingDirectory=/opt/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
|
||||
Type=simple
|
||||
TimeoutSec=900
|
||||
|
|
|
@ -18,7 +18,7 @@ start() {
|
|||
|
||||
# start ctbrec
|
||||
$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
|
||||
echo $! > "${PIDFILE}"
|
||||
|
|
|
@ -4,5 +4,5 @@ DIR=$(dirname $0)
|
|||
pushd $DIR
|
||||
JAVA=java
|
||||
$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
|
||||
|
|
|
@ -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
|
|
@ -1,27 +1,7 @@
|
|||
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.Moshi;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.Recording;
|
||||
|
@ -31,6 +11,23 @@ import ctbrec.io.InstantJsonAdapter;
|
|||
import ctbrec.io.ModelJsonAdapter;
|
||||
import ctbrec.recorder.Recorder;
|
||||
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 {
|
||||
|
||||
|
@ -73,7 +70,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
|||
switch (request.action) {
|
||||
case "start":
|
||||
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\"}";
|
||||
resp.getWriter().write(response);
|
||||
break;
|
||||
|
@ -262,7 +259,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
|||
for (Site site : sites) {
|
||||
Model model = site.createModelFromUrl(url);
|
||||
if (model != null) {
|
||||
recorder.startRecording(model);
|
||||
recorder.addModel(model);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -279,7 +276,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
|||
for (Site site : sites) {
|
||||
if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) {
|
||||
Model m = site.createModel(modelName);
|
||||
recorder.startRecording(m);
|
||||
recorder.addModel(m);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,12 +35,12 @@
|
|||
</root>
|
||||
|
||||
<logger name="ctbrec.LoggingInterceptor" level="INFO"/>
|
||||
<logger name="ctbrec.io.CookieJarImpl" 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.RecorderServlet" level="INFO"/>
|
||||
<logger name="ctbrec.recorder.ThreadPoolScaler" level="DEBUG"/>
|
||||
<logger name="ctbrec.io.CookieJarImpl" level="INFO"/>
|
||||
<logger name="org.eclipse.jetty" level="INFO" />
|
||||
<logger name="streamer" level="ERROR" />
|
||||
|
||||
</configuration>
|
||||
|
|
Loading…
Reference in New Issue