forked from j62/ctbrec
1
0
Fork 0

Merge branch 'dev' into v4

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

View File

@ -1,9 +1,21 @@
NEXT
========================
* Added "record later" tab to "bookmark" models
* Added config option to show the total number of models in the title bar
* Fixed problem with Cam4 playlist URLs, thanks @gohufrapoc
3.11.0
========================
* 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
========================

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -1,6 +1,8 @@
package ctbrec.ui;
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));
}

View File

@ -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());

View File

@ -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);
}
}

View File

@ -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();
}
}
});
}
}

View File

@ -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();
}
}

View File

@ -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));
}

View File

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

View File

@ -31,13 +31,6 @@
*/
package ctbrec.ui.controls;
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);

View File

@ -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()
),

View File

@ -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));

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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));

View File

@ -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;
}

View File

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

View File

@ -1,57 +1,14 @@
package ctbrec.ui.tabs;
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) {

View File

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

View File

@ -45,11 +45,11 @@ import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.FileDownload;
import ctbrec.ui.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");

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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
}
}

View File

@ -18,6 +18,7 @@ import ch.qos.logback.classic.spi.IThrowableProxy;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.classic.spi.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;
}

View File

@ -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"/>

View File

@ -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"/>

View File

@ -11,6 +11,6 @@ org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.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

View File

@ -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;

View File

@ -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) {

View File

@ -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);

View File

@ -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;

View File

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

View File

@ -1,7 +1,15 @@
package ctbrec.io;
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("[]");
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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));

View File

@ -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();
}

View File

@ -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");

View File

@ -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();
}
}

View File

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

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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

View File

@ -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}"

View File

@ -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

View File

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

View File

@ -1,27 +1,7 @@
package ctbrec.recorder.server;
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;
}
}

View File

@ -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>