Merge branch 'dev' into v4

This commit is contained in:
0xb00bface 2021-01-16 21:01:43 +01:00
commit fa7f1e5f57
53 changed files with 992 additions and 540 deletions

View File

@ -1,6 +1,20 @@
3.13.0
========================
* Added "Recently watched" tab. Can be disabled in Settings -> General
* Recording size now takes all associated files into account
* Removed restriction of download thread pool size (was 100 before)
3.12.2
========================
* Fix: Some Cam4 URLs were broken
* Fix: Cam4 search didn't work
* Stop hlsdl if the recording size didn't change for 90 seconds
3.12.1
========================
* Fix: "Resume all" started the recordings of models marked for later recording
* Fix: Login dialogs don't open
* Use 16:9 thumbnail format for MFC
3.12.0
========================

View File

@ -63,6 +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.RecentlyWatchedTab;
import ctbrec.ui.tabs.RecordedTab;
import ctbrec.ui.tabs.RecordingsTab;
import ctbrec.ui.tabs.SiteTab;
@ -216,6 +217,9 @@ public class CamrecApplication extends Application {
tabPane.getTabs().add(modelsTab);
recordingsTab = new RecordingsTab("Recordings", recorder, config);
tabPane.getTabs().add(recordingsTab);
if (config.getSettings().recentlyWatched) {
tabPane.getTabs().add(new RecentlyWatchedTab(recorder, sites));
}
tabPane.getTabs().add(new SettingsTab(sites, recorder));
tabPane.getTabs().add(new NewsTab());
tabPane.getTabs().add(new DonateTabFx());
@ -299,8 +303,11 @@ public class CamrecApplication extends Application {
final boolean immediately = shutdownNow;
new Thread(() -> {
modelsTab.saveState();
recordingsTab.saveState();
for (Tab tab : tabPane.getTabs()) {
if (tab instanceof ShutdownListener) {
((ShutdownListener) tab).onShutdown();
}
}
onlineMonitor.shutdown();
recorder.shutdown(immediately);
for (Site site : sites) {

View File

@ -28,11 +28,10 @@ public class ExternalBrowser implements AutoCloseable {
private static final ExternalBrowser INSTANCE = new ExternalBrowser();
private Lock lock = new ReentrantLock();
private Process p;
private Consumer<String> messageListener;
private InputStream in;
private OutputStream out;
private Socket socket;
private Socket socket; // NOSONAR
private Thread reader;
private volatile boolean stopped = true;
private volatile boolean browserReady = false;
@ -53,10 +52,10 @@ public class ExternalBrowser implements AutoCloseable {
File configDir = new File(Config.getInstance().getConfigDir(), "ctbrec-minimal-browser");
String[] cmdline = OS.getBrowserCommand(configDir.getCanonicalPath());
p = new ProcessBuilder(cmdline).start();
Process p = new ProcessBuilder(cmdline).start();
if (LOG.isTraceEnabled()) {
new Thread(new StreamRedirector(p.getInputStream(), System.out)).start();
new Thread(new StreamRedirector(p.getErrorStream(), System.err)).start();
new Thread(new StreamRedirector(p.getInputStream(), System.out)).start(); // NOSONAR
new Thread(new StreamRedirector(p.getErrorStream(), System.err)).start(); // NOSONAR
} else {
new Thread(new StreamRedirector(p.getInputStream(), OutputStream.nullOutputStream())).start();
new Thread(new StreamRedirector(p.getErrorStream(), OutputStream.nullOutputStream())).start();
@ -81,7 +80,6 @@ public class ExternalBrowser implements AutoCloseable {
LOG.debug("Waiting for browser to terminate");
p.waitFor();
int exitValue = p.exitValue();
p = null;
reader = null;
in = null;
out = null;
@ -119,7 +117,6 @@ public class ExternalBrowser implements AutoCloseable {
}
public void executeJavaScript(String javaScript) throws IOException {
//LOG.debug("Executing JS {}", javaScript);
JSONObject script = new JSONObject();
script.put("execute", javaScript);
out.write(script.toString().getBytes(UTF_8));
@ -141,8 +138,7 @@ public class ExternalBrowser implements AutoCloseable {
private void readBrowserOutput() {
LOG.debug("Browser output reader started");
try {
BufferedReader br = new BufferedReader(new InputStreamReader(in));
try (BufferedReader br = new BufferedReader(new InputStreamReader(in))) {
String line;
synchronized (browserReadyLock) {
browserReady = true;
@ -150,11 +146,8 @@ public class ExternalBrowser implements AutoCloseable {
}
while( !Thread.interrupted() && (line = br.readLine()) != null ) {
LOG.debug("Browser output: {}", line);
if(!line.startsWith("{")) {
} else {
if(messageListener != null) {
messageListener.accept(line);
}
if (line.startsWith("{") && messageListener != null) {
messageListener.accept(line);
}
}
} catch (IOException e) {

View File

@ -25,10 +25,12 @@ import ctbrec.Config;
import ctbrec.Model;
import ctbrec.OS;
import ctbrec.Recording;
import ctbrec.event.EventBusHolder;
import ctbrec.io.StreamRedirector;
import ctbrec.io.UrlUtil;
import ctbrec.recorder.download.StreamSource;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.event.PlayerStartedEvent;
import javafx.scene.Scene;
public class Player {
@ -85,6 +87,7 @@ public class Player {
}
String playlistUrl = getPlaylistUrl(model);
LOG.debug("Playing {}", playlistUrl);
EventBusHolder.BUS.post(new PlayerStartedEvent(model));
return Player.play(playlistUrl, async);
} else {
Dialogs.showError(scene, "Room not public", "Room is currently not public", null);
@ -166,12 +169,10 @@ public class Player {
// create threads, which read stdout and stderr of the player process. these are needed,
// because otherwise the internal buffer for these streams fill up and block the process
Thread std = new Thread(new StreamRedirector(playerProcess.getInputStream(), OutputStream.nullOutputStream()));
//Thread std = new Thread(new StreamRedirectThread(playerProcess.getInputStream(), System.out));
std.setName("Player stdout pipe");
std.setDaemon(true);
std.start();
Thread err = new Thread(new StreamRedirector(playerProcess.getErrorStream(), OutputStream.nullOutputStream()));
//Thread err = new Thread(new StreamRedirectThread(playerProcess.getErrorStream(), System.err));
err.setName("Player stderr pipe");
err.setDaemon(true);
err.start();

View File

@ -22,7 +22,7 @@ import javafx.scene.layout.StackPane;
import javafx.stage.Popup;
public class PreviewPopupHandler implements EventHandler<MouseEvent> {
private static final transient Logger LOG = LoggerFactory.getLogger(PreviewPopupHandler.class);
private static final Logger LOG = LoggerFactory.getLogger(PreviewPopupHandler.class);
private static final int offset = 10;
private long timeForPopupOpen = TimeUnit.SECONDS.toMillis(1);
@ -67,11 +67,11 @@ public class PreviewPopupHandler implements EventHandler<MouseEvent> {
} else if(event.getEventType() == MouseEvent.MOUSE_ENTERED) {
popup.setX(event.getScreenX()+ offset);
popup.setY(event.getScreenY()+ offset);
JavaFxModel model = getModel(event);
if(model != null) {
JavaFxModel newModel = getModel(event);
if(newModel != null) {
closeCountdown = -1;
boolean modelChanged = model != this.model;
this.model = model;
boolean modelChanged = newModel != this.model;
this.model = newModel;
if(popup.isShowing()) {
openCountdown = -1;
if(modelChanged) {
@ -97,15 +97,15 @@ public class PreviewPopupHandler implements EventHandler<MouseEvent> {
@SuppressWarnings("unchecked")
TableRow<JavaFxModel> row = (TableRow<JavaFxModel>) event.getSource();
TableView<JavaFxModel> table = row.getTableView();
double offset = 0;
double columnOffset = 0;
double width = 0;
for (TableColumn<JavaFxModel, ?> col : table.getColumns()) {
offset += width;
columnOffset += width;
width = col.getWidth();
if(Objects.equals(col.getId(), "preview")) {
Point2D screenToLocal = table.screenToLocal(event.getScreenX(), event.getScreenY());
double x = screenToLocal.getX();
return x >= offset && x <= offset + width;
return x >= columnOffset && x <= columnOffset + width;
}
}
return false;
@ -176,6 +176,7 @@ public class PreviewPopupHandler implements EventHandler<MouseEvent> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.error("PreviewPopupTimer interrupted");
break;
}

View File

@ -0,0 +1,5 @@
package ctbrec.ui;
public interface ShutdownListener {
void onShutdown();
}

View File

@ -7,6 +7,7 @@ import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.recorder.download.StreamSource;
import ctbrec.ui.controls.Dialogs;
@ -19,9 +20,10 @@ import javafx.stage.Stage;
public class StreamSourceSelectionDialog {
private static final StreamSource BEST = new BestStreamSource();
private StreamSourceSelectionDialog() {}
private StreamSourceSelectionDialog() {
}
public static void show(Scene parent, Model model, Function<Model,Void> onSuccess, Function<Throwable, Void> onFail) {
public static void show(Scene parent, Model model, Function<Model, Void> onSuccess, Function<Throwable, Void> onFail) {
Task<List<StreamSource>> selectStreamSource = new Task<List<StreamSource>>() {
@Override
protected List<StreamSource> call() throws Exception {
@ -35,7 +37,7 @@ public class StreamSourceSelectionDialog {
List<StreamSource> sources;
try {
sources = selectStreamSource.get();
int selectedIndex = model.getStreamUrlIndex() > -1 ? Math.min(model.getStreamUrlIndex(), sources.size()-1) : sources.size()-1;
int selectedIndex = model.getStreamUrlIndex() > -1 ? Math.min(model.getStreamUrlIndex(), sources.size() - 1) : sources.size() - 1;
ChoiceDialog<StreamSource> choiceDialog = new ChoiceDialog<>(sources.get(selectedIndex), sources);
choiceDialog.setTitle("Stream Quality");
choiceDialog.setHeaderText("Select your preferred stream quality");
@ -45,7 +47,7 @@ public class StreamSourceSelectionDialog {
InputStream icon = Dialogs.class.getResourceAsStream("/icon.png");
stage.getIcons().add(new Image(icon));
Optional<StreamSource> selectedSource = choiceDialog.showAndWait();
if(selectedSource.isPresent()) {
if (selectedSource.isPresent()) {
int index = -1;
if (selectedSource.get() != BEST) {
index = sources.indexOf(selectedSource.get());
@ -61,7 +63,7 @@ public class StreamSourceSelectionDialog {
}
});
selectStreamSource.setOnFailed(e -> onFail.apply(selectStreamSource.getException()));
new Thread(selectStreamSource).start();
GlobalThreadPool.submit(selectStreamSource);
}
private static class BestStreamSource extends StreamSource {

View File

@ -7,7 +7,7 @@ import java.util.concurrent.ExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Model;
import ctbrec.GlobalThreadPool;
import ctbrec.sites.Site;
import javafx.application.Platform;
import javafx.concurrent.Task;
@ -19,11 +19,11 @@ import javafx.stage.Stage;
public class TipDialog extends TextInputDialog {
private static final transient Logger LOG = LoggerFactory.getLogger(TipDialog.class);
private static final Logger LOG = LoggerFactory.getLogger(TipDialog.class);
private Site site;
private Scene parent;
public TipDialog(Scene parent, Site site, Model model) {
public TipDialog(Scene parent, Site site) {
this.parent = parent;
this.site = site;
setTitle("Send Tip");
@ -32,7 +32,7 @@ public class TipDialog extends TextInputDialog {
setContentText("Amount of tokens to tip:");
setResizable(true);
getEditor().setDisable(true);
if(parent != null) {
if (parent != null) {
Stage stage = (Stage) getDialogPane().getScene().getWindow();
stage.getScene().getStylesheets().addAll(parent.getStylesheets());
}
@ -56,14 +56,14 @@ public class TipDialog extends TextInputDialog {
double tokens = get();
Platform.runLater(() -> {
if (tokens <= 0) {
String msg = "Do you want to buy tokens now?\n\nIf you agree, "+site.getName()+" will open in a browser. "
String msg = "Do you want to buy tokens now?\n\nIf you agree, " + site.getName() + " will open in a browser. "
+ "The used address is an affiliate link, which supports me, but doesn't cost you anything more.";
Alert buyTokens = new AutosizeAlert(Alert.AlertType.CONFIRMATION, msg, parent, ButtonType.NO, ButtonType.YES);
buyTokens.setTitle("No tokens");
buyTokens.setHeaderText("You don't have any tokens");
buyTokens.showAndWait();
TipDialog.this.close();
if(buyTokens.getResult() == ButtonType.YES) {
if (buyTokens.getResult() == ButtonType.YES) {
DesktopIntegration.open(site.getAffiliateLink());
}
} else {
@ -72,13 +72,20 @@ public class TipDialog extends TextInputDialog {
setHeaderText("Current token balance: " + df.format(tokens));
}
});
} catch (InterruptedException | ExecutionException e) {
LOG.error("Couldn't retrieve account balance", e);
showErrorDialog(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
handleExcetion(e);
} catch (ExecutionException e) {
handleExcetion(e);
}
}
};
new Thread(task).start();
GlobalThreadPool.submit(task);
}
private void handleExcetion(Exception e) {
LOG.error("Couldn't retrieve account balance", e);
showErrorDialog(e);
}
private void showErrorDialog(Throwable throwable) {

View File

@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe;
import ctbrec.GlobalThreadPool;
import ctbrec.event.EventBusHolder;
import ctbrec.sites.Site;
import javafx.application.Platform;
@ -19,7 +20,7 @@ import javafx.scene.control.Tooltip;
public class TokenLabel extends Label {
private static final transient Logger LOG = LoggerFactory.getLogger(TokenLabel.class);
private static final Logger LOG = LoggerFactory.getLogger(TokenLabel.class);
private double tokens = -1;
private Site site;
@ -72,17 +73,24 @@ public class TokenLabel extends Label {
@Override
protected void done() {
try {
double tokens = get();
tokens = get();
update(tokens);
} catch (InterruptedException | ExecutionException e) {
LOG.error("Couldn't retrieve account balance", e);
Platform.runLater(() -> {
setText("Tokens: error");
setTooltip(new Tooltip(e.getMessage()));
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
handleException(e);
} catch (ExecutionException e) {
handleException(e);
}
}
private void handleException(Exception e) {
LOG.error("Couldn't retrieve account balance", e);
Platform.runLater(() -> {
setText("Tokens: error");
setTooltip(new Tooltip(e.getMessage()));
});
}
};
new Thread(task).start();
GlobalThreadPool.submit(task);
}
}

View File

@ -3,13 +3,13 @@ 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;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs;
@ -32,7 +32,7 @@ public class CheckModelAccountAction {
public void execute(Predicate<Model> filter) {
String buttonText = b.getText();
b.setDisable(true);
CompletableFuture.runAsync(() -> {
Runnable checker = (() -> {
List<Model> deletedAccounts = new ArrayList<>();
try {
List<Model> models = recorder.getModels().stream() //
@ -71,5 +71,6 @@ public class CheckModelAccountAction {
});
}
});
GlobalThreadPool.submit(checker);
}
}

View File

@ -10,7 +10,6 @@ import ctbrec.Config;
import ctbrec.Model;
import ctbrec.ui.JavaFxModel;
import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.TableView;
@ -30,23 +29,21 @@ public class EditNotesAction {
public void execute() {
source.setCursor(Cursor.WAIT);
new Thread(() -> Platform.runLater(() -> {
String notes = Config.getInstance().getSettings().modelNotes.getOrDefault(model.getUrl(), "");
Optional<String> newNotes = Dialogs.showTextInput(source.getScene(), "Model Notes", "Notes for " + model.getName(), notes);
newNotes.ifPresent(n -> {
if (!n.trim().isEmpty()) {
Config.getInstance().getSettings().modelNotes.put(model.getUrl(), n);
} else {
Config.getInstance().getSettings().modelNotes.remove(model.getUrl());
}
try {
Config.getInstance().save();
} catch (IOException e) {
LOG.warn("Couldn't save config", e);
}
});
table.refresh();
source.setCursor(Cursor.DEFAULT);
})).start();
String notes = Config.getInstance().getSettings().modelNotes.getOrDefault(model.getUrl(), "");
Optional<String> newNotes = Dialogs.showTextInput(source.getScene(), "Model Notes", "Notes for " + model.getName(), notes);
newNotes.ifPresent(n -> {
if (!n.trim().isEmpty()) {
Config.getInstance().getSettings().modelNotes.put(model.getUrl(), n);
} else {
Config.getInstance().getSettings().modelNotes.remove(model.getUrl());
}
try {
Config.getInstance().save();
} catch (IOException e) {
LOG.warn("Couldn't save config", e);
}
});
table.refresh();
source.setCursor(Cursor.DEFAULT);
}
}

View File

@ -2,13 +2,9 @@ package ctbrec.ui.action;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import javafx.application.Platform;
import javafx.scene.Cursor;
@ -16,9 +12,6 @@ import javafx.scene.Node;
public class ModelMassEditAction {
static BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
static ExecutorService threadPool = new ThreadPoolExecutor(2, 10, 10, TimeUnit.MINUTES, queue);
protected List<? extends Model> models;
protected Consumer<Model> action;
protected Node source;
@ -42,7 +35,7 @@ public class ModelMassEditAction {
Consumer<Model> cb = Objects.requireNonNull(callback, "Callback is null, call execute() instead");
source.setCursor(Cursor.WAIT);
for (Model model : models) {
threadPool.submit(() -> {
GlobalThreadPool.submit(() -> {
action.accept(model);
cb.accept(model);
Platform.runLater(() -> source.setCursor(Cursor.DEFAULT));

View File

@ -4,6 +4,7 @@ import java.io.File;
import java.time.Instant;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.Settings.DirectoryStructure;
import ctbrec.ui.DesktopIntegration;
@ -26,7 +27,7 @@ public class OpenRecordingsDir {
File fileForRecording = Config.getInstance().getFileForRecording(selectedModel, ".mp4", Instant.now());
final File dir = getModelDirectory(fileForRecording);
if (dir.exists()) {
new Thread(() -> DesktopIntegration.open(dir)).start();
GlobalThreadPool.submit(() -> DesktopIntegration.open(dir));
} else {
Dialogs.showError(source.getScene(), "Directory does not exist", "There are no recordings for this model", null);
}

View File

@ -6,6 +6,7 @@ import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.concurrent.CompletableFuture;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs;
@ -36,6 +37,6 @@ public class RemoveTimeLimitAction {
Dialogs.showError(source.getScene(), "Error", "Couln't remove stop date", e);
return false;
}
}).whenComplete((r,e) -> source.setCursor(Cursor.DEFAULT));
}, GlobalThreadPool.get()).whenComplete((r,e) -> source.setCursor(Cursor.DEFAULT));
}
}

View File

@ -13,6 +13,7 @@ import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.SubsequentAction;
import ctbrec.recorder.Recorder;
@ -82,7 +83,7 @@ public class SetStopDateAction {
}
}
return true;
}).whenComplete((r, e) -> {
}, GlobalThreadPool.get()).whenComplete((r, e) -> {
source.setCursor(Cursor.DEFAULT);
if (e != null) {
LOG.error("Error", e);

View File

@ -1,5 +1,6 @@
package ctbrec.ui.action;
import ctbrec.GlobalThreadPool;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
@ -20,7 +21,7 @@ public class ToggleRecordingAction {
public void execute() {
toggleButton.setCursor(Cursor.WAIT);
Thread t = new Thread(() -> {
GlobalThreadPool.submit(() -> {
try {
if (pause) {
recorder.pause();
@ -36,7 +37,5 @@ public class ToggleRecordingAction {
Platform.runLater(() -> toggleButton.setCursor(Cursor.DEFAULT));
}
});
t.setDaemon(true);
t.start();
}
}

View File

@ -31,6 +31,14 @@
*/
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.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.action.PlayAction;
@ -40,18 +48,15 @@ import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
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.
@ -163,7 +168,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
follow = new Button("Follow");
follow.setOnAction(evt -> {
setCursor(Cursor.WAIT);
CompletableFuture.runAsync(new Task<Boolean>() {
GlobalThreadPool.submit(new Task<Boolean>() {
@Override
protected Boolean call() throws Exception {
model.getSite().login();
@ -184,7 +189,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
record = new Button("Record");
record.setOnAction(evt -> {
setCursor(Cursor.WAIT);
CompletableFuture.runAsync(new Task<Void>() {
GlobalThreadPool.submit(new Task<Void>() {
@Override
protected Void call() throws Exception {
recorder.addModel(model);
@ -292,12 +297,12 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
@Override
protected double computePrefHeight(double width) {
return thumbSize + 20;
return thumbSize + 20.0;
}
@Override
protected double computeMaxHeight(double width) {
return thumbSize + 20;
return thumbSize + 20.0;
}
@Override

View File

@ -12,6 +12,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
@ -79,7 +80,7 @@ public class StreamPreview extends StackPane {
double w = Config.getInstance().getSettings().thumbWidth;
double h = w / aspect;
resizeTo(w, h);
} catch (Exception e) {}
} catch (Exception e) { /* nothing to do */ }
}
if(future != null && !future.isDone()) {
@ -157,14 +158,14 @@ public class StreamPreview extends StackPane {
running = false;
MediaPlayer old = videoPlayer;
Future<?> oldFuture = future;
new Thread(() -> {
GlobalThreadPool.submit(() -> {
if(oldFuture != null && !oldFuture.isDone()) {
oldFuture.cancel(true);
}
if(old != null) {
old.dispose();
}
}).start();
});
}
private void onError(MediaPlayer videoPlayer) {

View File

@ -1,5 +1,6 @@
package ctbrec.ui.controls;
import ctbrec.GlobalThreadPool;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
@ -13,6 +14,8 @@ import javafx.stage.StageStyle;
import javafx.util.Duration;
public final class Toast {
private Toast() {}
public static void makeText(Scene owner, String toastMsg, int toastDelay, int fadeInDelay, int fadeOutDelay) {
Stage toastStage = new Stage();
toastStage.initOwner(owner.getWindow());
@ -35,20 +38,18 @@ public final class Toast {
Timeline fadeInTimeline = new Timeline();
KeyFrame fadeInKey1 = new KeyFrame(Duration.millis(fadeInDelay), new KeyValue(toastStage.getScene().getRoot().opacityProperty(), 1));
fadeInTimeline.getKeyFrames().add(fadeInKey1);
fadeInTimeline.setOnFinished((ae) -> {
new Thread(() -> {
try {
Thread.sleep(toastDelay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Timeline fadeOutTimeline = new Timeline();
KeyFrame fadeOutKey1 = new KeyFrame(Duration.millis(fadeOutDelay), new KeyValue(toastStage.getScene().getRoot().opacityProperty(), 0));
fadeOutTimeline.getKeyFrames().add(fadeOutKey1);
fadeOutTimeline.setOnFinished((aeb) -> toastStage.close());
fadeOutTimeline.play();
}).start();
});
fadeInTimeline.setOnFinished(ae -> GlobalThreadPool.submit(() -> {
try {
Thread.sleep(toastDelay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
Timeline fadeOutTimeline = new Timeline();
KeyFrame fadeOutKey1 = new KeyFrame(Duration.millis(fadeOutDelay), new KeyValue(toastStage.getScene().getRoot().opacityProperty(), 0));
fadeOutTimeline.getKeyFrames().add(fadeOutKey1);
fadeOutTimeline.setOnFinished(aeb -> toastStage.close());
fadeOutTimeline.play();
}));
fadeInTimeline.play();
}
}

View File

@ -0,0 +1,60 @@
package ctbrec.ui.event;
import java.time.Instant;
import java.util.Objects;
import ctbrec.Model;
import ctbrec.ui.JavaFxModel;
public class PlayerStartedEvent {
private Model model;
private Instant timestamp;
public PlayerStartedEvent(Model model) {
this(model, Instant.now());
}
public PlayerStartedEvent(Model model, Instant timestamp) {
this.model = unwrap(model);
this.timestamp = timestamp;
}
public Model getModel() {
return model;
}
public Instant getTimestamp() {
return timestamp;
}
@Override
public int hashCode() {
return Objects.hash(timestamp);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PlayerStartedEvent other = (PlayerStartedEvent) obj;
return Objects.equals(timestamp, other.timestamp);
}
@Override
public String toString() {
return "PlayerStartedEvent [model=" + model + ", timestamp=" + timestamp + "]";
}
private Model unwrap(Model model) {
if (model instanceof JavaFxModel) {
return ((JavaFxModel) model).getDelegate();
} else {
return model;
}
}
}

View File

@ -1,7 +1,16 @@
package ctbrec.ui.news;
import static ctbrec.io.HttpConstants.*;
import java.io.IOException;
import java.util.Objects;
import org.json.JSONObject;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.GlobalThreadPool;
import ctbrec.io.HttpException;
import ctbrec.ui.CamrecApplication;
import ctbrec.ui.controls.Dialogs;
@ -14,12 +23,6 @@ import javafx.scene.control.Tab;
import javafx.scene.layout.VBox;
import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONObject;
import java.io.IOException;
import java.util.Objects;
import static ctbrec.io.HttpConstants.USER_AGENT;
public class NewsTab extends Tab implements TabSelectionListener {
private static final String ACCESS_TOKEN = "a2804d73a89951a22e0f8483a6fcec8943afd88b7ba17c459c095aa9e6f94fd0";
@ -36,7 +39,7 @@ public class NewsTab extends Tab implements TabSelectionListener {
@Override
public void selected() {
new Thread(this::loadToots).start();
GlobalThreadPool.submit(this::loadToots);
}
private void loadToots() {

View File

@ -9,13 +9,13 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Hmac;
import ctbrec.Settings;
import ctbrec.Settings.DirectoryStructure;
@ -129,6 +129,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private SimpleBooleanProperty transportLayerSecurity;
private SimpleBooleanProperty fastScrollSpeed;
private SimpleBooleanProperty useHlsdl;
private SimpleBooleanProperty recentlyWatched;
private SimpleFileProperty hlsdlExecutable;
private ExclusiveSelectionProperty recordLocal;
private SimpleIntegerProperty postProcessingThreads;
@ -191,6 +192,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
confirmationDialogs = new SimpleBooleanProperty(null, "confirmationForDangerousActions", settings.confirmationForDangerousActions);
useHlsdl = new SimpleBooleanProperty(null, "useHlsdl", settings.useHlsdl);
hlsdlExecutable = new SimpleFileProperty(null, "hlsdlExecutable", settings.hlsdlExecutable);
recentlyWatched = new SimpleBooleanProperty(null, "recentlyWatched", settings.recentlyWatched);
}
private void createGui() {
@ -214,6 +216,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Setting.of("Display stream resolution in overview", determineResolution),
Setting.of("Manually select stream quality", chooseStreamQuality, "Opens a dialog to select the video resolution before recording"),
Setting.of("Enable live previews (experimental)", livePreviews),
Setting.of("Enable recently watched tab", recentlyWatched).needsRestart(),
Setting.of("Add models from clipboard", monitorClipboard, "Monitor clipboard for model URLs and automatically add them to the recorder").needsRestart(),
Setting.of("Fast scroll speed", fastScrollSpeed, "Makes the thumbnail overviews scroll faster with the mouse wheel").needsRestart(),
Setting.of("Show confirmation dialogs", confirmationDialogs, "Show confirmation dialogs for irreversible actions"),
@ -439,7 +442,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
}
public void saveConfig() {
CompletableFuture.runAsync(() -> {
GlobalThreadPool.submit(() -> {
try {
Config.getInstance().save();
} catch (IOException e) {

View File

@ -22,7 +22,7 @@ import okhttp3.HttpUrl;
public class BongaCamsElectronLoginDialog {
private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsElectronLoginDialog.class);
private static final Logger LOG = LoggerFactory.getLogger(BongaCamsElectronLoginDialog.class);
public static final String DOMAIN = "bongacams.com";
public static final String URL = BongaCams.baseUrl + "/login";
private CookieJar cookieJar;
@ -40,18 +40,18 @@ public class BongaCamsElectronLoginDialog {
msg.put("config", config);
browser.run(msg, msgHandler);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Couldn't wait for login dialog", e);
} finally {
browser.close();
}
}
private Consumer<String> msgHandler = (line) -> {
private Consumer<String> msgHandler = line -> {
if(!line.startsWith("{")) {
System.err.println(line);
LOG.error("Didn't received a JSON object {}", line);
} else {
JSONObject json = new JSONObject(line);
//LOG.debug("Browser: {}", json.toString(2));
if(json.has("url")) {
String url = json.getString("url");
if(url.endsWith("/login")) {
@ -82,16 +82,16 @@ public class BongaCamsElectronLoginDialog {
}
if(json.has("cookies")) {
JSONArray _cookies = json.getJSONArray("cookies");
for (int i = 0; i < _cookies.length(); i++) {
JSONObject cookie = _cookies.getJSONObject(i);
JSONArray cookiesFromBrowser = json.getJSONArray("cookies");
for (int i = 0; i < cookiesFromBrowser.length(); i++) {
JSONObject cookie = cookiesFromBrowser.getJSONObject(i);
if(cookie.getString("domain").contains(DOMAIN)) {
Builder b = new Cookie.Builder()
.path(cookie.getString("path"))
.domain(DOMAIN)
.name(cookie.getString("name"))
.value(cookie.getString("value"))
.expiresAt(Double.valueOf(cookie.optDouble("expirationDate")).longValue());
.expiresAt((long) cookie.optDouble("expirationDate"));
if(cookie.optBoolean("hostOnly")) {
b.hostOnlyDomain(DOMAIN);
}
@ -108,8 +108,7 @@ public class BongaCamsElectronLoginDialog {
}
try {
URL _url = new URL(url);
if (Objects.equals(_url.getPath(), "/")) {
if (Objects.equals(new URL(url).getPath(), "/")) {
browser.close();
}
} catch (MalformedURLException e) {

View File

@ -1,17 +1,16 @@
package ctbrec.ui.sites.bonga;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.bonga.BongaCamsHttpClient;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.sites.AbstractSiteUi;
import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class BongaCamsSiteUi extends AbstractSiteUi {
@ -39,37 +38,20 @@ public class BongaCamsSiteUi extends AbstractSiteUi {
@Override
public synchronized boolean login() throws IOException {
boolean automaticLogin = bongaCams.login();
if(automaticLogin) {
if (automaticLogin) {
return true;
} else {
BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>();
// login with external browser window
try {
new Thread(() -> {
// login with external browser window
try {
new BongaCamsElectronLoginDialog(bongaCams.getHttpClient().getCookieJar());
} catch (Exception e1) {
LOG.error("Error logging in with external browser", e1);
Dialogs.showError("Login error", "Couldn't login to " + bongaCams.getName(), e1);
}
try {
queue.put(true);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.error("Error while signaling termination", e);
}
}).start();
queue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.error("Error while waiting for login dialog to close", e);
throw new IOException(e);
new BongaCamsElectronLoginDialog(bongaCams.getHttpClient().getCookieJar());
} catch (Exception e1) {
LOG.error("Error logging in with external browser", e1);
Dialogs.showError("Login error", "Couldn't login to " + bongaCams.getName(), e1);
}
BongaCamsHttpClient httpClient = (BongaCamsHttpClient)bongaCams.getHttpClient();
BongaCamsHttpClient httpClient = (BongaCamsHttpClient) bongaCams.getHttpClient();
boolean loggedIn = httpClient.checkLoginSuccess();
if(loggedIn) {
if (loggedIn) {
LOG.info("Logged in. User ID is {}", httpClient.getUserId());
} else {
LOG.info("Login failed");

View File

@ -22,7 +22,7 @@ import okhttp3.HttpUrl;
public class Cam4ElectronLoginDialog {
private static final transient Logger LOG = LoggerFactory.getLogger(Cam4ElectronLoginDialog.class);
private static final Logger LOG = LoggerFactory.getLogger(Cam4ElectronLoginDialog.class);
public static final String DOMAIN = "cam4.com";
public static final String URL = Cam4.BASE_URI + "/login";
private CookieJar cookieJar;
@ -40,15 +40,16 @@ public class Cam4ElectronLoginDialog {
msg.put("config", config);
browser.run(msg, msgHandler);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Couldn't wait for login dialog", e);
} finally {
browser.close();
}
}
private Consumer<String> msgHandler = (line) -> {
private Consumer<String> msgHandler = line -> {
if(!line.startsWith("{")) {
System.err.println(line);
LOG.error("Didn't received a JSON object {}", line);
} else {
JSONObject json = new JSONObject(line);
if(json.has("url")) {
@ -75,11 +76,10 @@ public class Cam4ElectronLoginDialog {
}
if(json.has("cookies")) {
JSONArray _cookies = json.getJSONArray("cookies");
JSONArray cookiesFromBrowser = json.getJSONArray("cookies");
try {
URL _url = new URL(url);
for (int i = 0; i < _cookies.length(); i++) {
JSONObject cookie = _cookies.getJSONObject(i);
for (int i = 0; i < cookiesFromBrowser.length(); i++) {
JSONObject cookie = cookiesFromBrowser.getJSONObject(i);
if(cookie.getString("domain").contains("cam4")) {
String domain = cookie.getString("domain");
if(domain.startsWith(".")) {
@ -91,12 +91,8 @@ public class Cam4ElectronLoginDialog {
cookieJar.saveFromResponse(HttpUrl.parse(Cam4.BASE_URI), Collections.singletonList(c));
}
}
if (Objects.equals(_url.getPath(), "/")) {
try {
browser.close();
} catch(IOException e) {
LOG.error("Couldn't send close request to browser", e);
}
if (Objects.equals(new URL(url).getPath(), "/")) {
closeBrowser();
}
} catch (MalformedURLException e) {
LOG.error("Couldn't parse new url {}", url, e);
@ -112,7 +108,7 @@ public class Cam4ElectronLoginDialog {
.domain(domain)
.name(cookie.getString("name"))
.value(cookie.getString("value"))
.expiresAt(Double.valueOf(cookie.optDouble("expirationDate")).longValue());
.expiresAt((long) cookie.optDouble("expirationDate"));
if(cookie.optBoolean("hostOnly")) {
b.hostOnlyDomain(domain);
}
@ -124,4 +120,12 @@ public class Cam4ElectronLoginDialog {
}
return b.build();
}
private void closeBrowser() {
try {
browser.close();
} catch(IOException e) {
LOG.error("Couldn't send close request to browser", e);
}
}
}

View File

@ -1,8 +1,6 @@
package ctbrec.ui.sites.cam4;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -13,10 +11,9 @@ import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.sites.AbstractSiteUi;
import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabProvider;
import javafx.application.Platform;
public class Cam4SiteUi extends AbstractSiteUi {
private static final transient Logger LOG = LoggerFactory.getLogger(Cam4SiteUi.class);
private static final Logger LOG = LoggerFactory.getLogger(Cam4SiteUi.class);
private Cam4TabProvider tabProvider;
private Cam4ConfigUI configUI;
@ -44,33 +41,13 @@ public class Cam4SiteUi extends AbstractSiteUi {
if (automaticLogin) {
return true;
} else {
BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>();
Runnable showDialog = () -> {
// login with external browser
try {
new Cam4ElectronLoginDialog(cam4.getHttpClient().getCookieJar());
} catch (Exception e1) {
LOG.error("Error logging in with external browser", e1);
Dialogs.showError("Login error", "Couldn't login to " + cam4.getName(), e1);
}
try {
queue.put(true);
} catch (InterruptedException e) {
LOG.error("Error while signaling termination", e);
}
};
Platform.runLater(showDialog);
// login with external browser
try {
queue.take();
} catch (InterruptedException e) {
LOG.error("Error while waiting for login dialog to close", e);
throw new IOException(e);
new Cam4ElectronLoginDialog(cam4.getHttpClient().getCookieJar());
} catch (Exception e1) {
LOG.error("Error logging in with external browser", e1);
Dialogs.showError("Login error", "Couldn't login to " + cam4.getName(), e1);
}
Cam4HttpClient httpClient = (Cam4HttpClient) cam4.getHttpClient();
boolean loggedIn = httpClient.checkLoginSuccess();
return loggedIn;

View File

@ -10,7 +10,6 @@ import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import org.json.JSONObject;
import org.jsoup.nodes.Element;
@ -43,14 +42,11 @@ public class Cam4UpdateService extends PaginatedScheduledService {
this.url = url;
this.loginRequired = loginRequired;
ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("ThumbOverviewTab UpdateService");
return t;
}
ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("ThumbOverviewTab UpdateService");
return t;
});
setExecutor(executor);
}
@ -60,16 +56,16 @@ public class Cam4UpdateService extends PaginatedScheduledService {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException {
if(loginRequired && StringUtil.isBlank(Config.getInstance().getSettings().cam4Username)) {
if (loginRequired && StringUtil.isBlank(Config.getInstance().getSettings().cam4Username)) {
return Collections.emptyList();
} else {
String url = Cam4UpdateService.this.url + "&page=" + page;
LOG.debug("Fetching page {}", url);
if(loginRequired) {
String pageUrl = Cam4UpdateService.this.url + "&page=" + page;
LOG.debug("Fetching page {}", pageUrl);
if (loginRequired) {
SiteUiFactory.getUi(site).login();
}
Request request = new Request.Builder()
.url(url)
.url(pageUrl)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
@ -91,7 +87,7 @@ public class Cam4UpdateService extends PaginatedScheduledService {
model.setPreview("https://snapshots.xcdnpro.com/thumbnails/" + model.getName() + "?s=" + System.currentTimeMillis());
model.setDescription(parseDesription(boxHtml));
model.setOnlineState(ONLINE);
if(boxHtml.contains("In private show")) {
if (boxHtml.contains("In private show")) {
model.setOnlineState(PRIVATE);
}
models.add(model);

View File

@ -1,32 +1,5 @@
package ctbrec.ui.sites.camsoda;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.tabs.TabSelectionListener;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
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;
@ -40,7 +13,42 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.tabs.TabSelectionListener;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
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.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import okhttp3.Request;
import okhttp3.Response;
public class CamsodaShowsTab extends Tab implements TabSelectionListener {
@ -129,7 +137,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
});
}
};
CompletableFuture.runAsync(task);
GlobalThreadPool.submit(task);
}
@Override
@ -173,17 +181,17 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
grid.add(createLabel(formatter.format(endTime), false), 1, 1);
Button record = new Button("Record Model");
record.setTooltip(new Tooltip(record.getText()));
record.setOnAction((evt) -> record(model));
record.setOnAction(evt -> record(model));
grid.add(record, 1, 2);
GridPane.setMargin(record, new Insets(10));
Button follow = new Button("Follow");
follow.setTooltip(new Tooltip(follow.getText()));
follow.setOnAction((evt) -> follow(model));
follow.setOnAction(evt -> follow(model));
grid.add(follow, 1, 3);
GridPane.setMargin(follow, new Insets(10));
Button openInBrowser = new Button("Open in Browser");
openInBrowser.setTooltip(new Tooltip(openInBrowser.getText()));
openInBrowser.setOnAction((evt) -> DesktopIntegration.open(model.getUrl()));
openInBrowser.setOnAction(evt -> DesktopIntegration.open(model.getUrl()));
grid.add(openInBrowser, 1, 4);
GridPane.setMargin(openInBrowser, new Insets(10));
root.setCenter(grid);
@ -195,7 +203,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
private void follow(Model model) {
setCursor(Cursor.WAIT);
CompletableFuture.runAsync(() -> {
GlobalThreadPool.submit(() -> {
try {
SiteUiFactory.getUi(model.getSite()).login();
model.follow();
@ -203,30 +211,26 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
LOG.error("Couldn't follow model {}", model, e);
showErrorDialog("Oh no!", "Couldn't follow model", e.getMessage());
} finally {
Platform.runLater(() -> {
setCursor(Cursor.DEFAULT);
});
Platform.runLater(() -> setCursor(Cursor.DEFAULT));
}
});
}
private void record(Model model) {
setCursor(Cursor.WAIT);
CompletableFuture.runAsync(() -> {
GlobalThreadPool.submit(() -> {
try {
recorder.addModel(model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
showErrorDialog("Oh no!", "Couldn't add model to the recorder", "Recorder error: " + e.getMessage());
} finally {
Platform.runLater(() -> {
setCursor(Cursor.DEFAULT);
});
Platform.runLater(() -> setCursor(Cursor.DEFAULT));
}
});
}
private void loadImage(Model model, ImageView thumb) {
CompletableFuture.runAsync(() -> {
GlobalThreadPool.submit(() -> {
try {
String url = camsoda.getBaseUrl() + "/api/v1/user/" + model.getName();
Request detailRequest = new Request.Builder().url(url).build();

View File

@ -5,7 +5,6 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -21,7 +20,7 @@ import okhttp3.Response;
public class ChaturbateUpdateService extends PaginatedScheduledService {
private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateUpdateService.class);
private static final Logger LOG = LoggerFactory.getLogger(ChaturbateUpdateService.class);
private String url;
private boolean loginRequired;
private Chaturbate chaturbate;
@ -31,14 +30,11 @@ public class ChaturbateUpdateService extends PaginatedScheduledService {
this.loginRequired = loginRequired;
this.chaturbate = chaturbate;
ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("ThumbOverviewTab UpdateService");
return t;
}
ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("ThumbOverviewTab UpdateService");
return t;
});
setExecutor(executor);
}
@ -51,12 +47,12 @@ public class ChaturbateUpdateService extends PaginatedScheduledService {
if(loginRequired && !chaturbate.credentialsAvailable()) {
return Collections.emptyList();
} else {
String url = ChaturbateUpdateService.this.url + "?page="+page+"&keywords=&_=" + System.currentTimeMillis();
LOG.debug("Fetching page {}", url);
String pageUrl = ChaturbateUpdateService.this.url + "?page="+page+"&keywords=&_=" + System.currentTimeMillis();
LOG.debug("Fetching page {}", pageUrl);
if(loginRequired) {
SiteUiFactory.getUi(chaturbate).login();
}
Request request = new Request.Builder().url(url).build();
Request request = new Request.Builder().url(pageUrl).build();
Response response = chaturbate.getHttpClient().execute(request);
if (response.isSuccessful()) {
List<Model> models = ChaturbateModelParser.parseModels(chaturbate, response.body().string());

View File

@ -5,6 +5,7 @@ import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.fc2live.Fc2Model;
@ -16,7 +17,7 @@ import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabProvider;
public class Fc2LiveSiteUi extends AbstractSiteUi {
private static final transient Logger LOG = LoggerFactory.getLogger(Fc2LiveSiteUi.class);
private static final Logger LOG = LoggerFactory.getLogger(Fc2LiveSiteUi.class);
private Fc2Live fc2live;
private Fc2TabProvider tabProvider;
private Fc2LiveConfigUI configUi;
@ -44,10 +45,10 @@ public class Fc2LiveSiteUi extends AbstractSiteUi {
@Override
public boolean play(Model model) {
new Thread(() -> {
GlobalThreadPool.submit(() -> {
Fc2Model m;
if(model instanceof JavaFxModel) {
m = (Fc2Model) ((JavaFxModel)model).getDelegate();
if (model instanceof JavaFxModel) {
m = (Fc2Model) ((JavaFxModel) model).getDelegate();
} else {
m = (Fc2Model) model;
}
@ -55,12 +56,20 @@ public class Fc2LiveSiteUi extends AbstractSiteUi {
m.openWebsocket();
LOG.debug("Starting player for {}", model);
Player.play(model, false);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
handleException(e);
} catch (IOException e) {
handleException(e);
} finally {
m.closeWebsocket();
} catch (InterruptedException | IOException e) {
LOG.error("Error playing the stream", e);
Dialogs.showError("Player", "Error playing the stream", e);
}
}).start();
});
return true;
}
private void handleException(Exception e) {
LOG.error("Error playing the stream", e);
Dialogs.showError("Player", "Error playing the stream", e);
}
}

View File

@ -1,8 +1,6 @@
package ctbrec.ui.sites.jasmin;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
@ -17,7 +15,7 @@ import ctbrec.ui.tabs.TabProvider;
public class LiveJasminSiteUi extends AbstractSiteUi {
private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminSiteUi.class);
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminSiteUi.class);
private LiveJasmin liveJasmin;
private LiveJasminTabProvider tabProvider;
private LiveJasminConfigUi configUi;
@ -44,43 +42,27 @@ public class LiveJasminSiteUi extends AbstractSiteUi {
// renew login every 30 min
long now = System.currentTimeMillis();
boolean renew = false;
if((now - lastLoginTime) > TimeUnit.MINUTES.toMillis(30)) {
if ((now - lastLoginTime) > TimeUnit.MINUTES.toMillis(30)) {
renew = true;
}
boolean automaticLogin = liveJasmin.login();
if(automaticLogin && !renew) {
if (automaticLogin && !renew) {
return true;
} else {
lastLoginTime = System.currentTimeMillis();
BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>();
new Thread (() -> {
// login with external browser window
try {
new LiveJasminElectronLoginDialog(liveJasmin.getHttpClient().getCookieJar());
} catch (Exception e1) {
LOG.error("Error logging in with external browser", e1);
Dialogs.showError("Login error", "Couldn't login to " + liveJasmin.getName(), e1);
}
try {
queue.put(true);
} catch (InterruptedException e) {
LOG.error("Error while signaling termination", e);
}
}).start();
// login with external browser window
try {
queue.take();
} catch (InterruptedException e) {
LOG.error("Error while waiting for login dialog to close", e);
throw new IOException(e);
new LiveJasminElectronLoginDialog(liveJasmin.getHttpClient().getCookieJar());
} catch (Exception e1) {
LOG.error("Error logging in with external browser", e1);
Dialogs.showError("Login error", "Couldn't login to " + liveJasmin.getName(), e1);
}
LiveJasminHttpClient httpClient = (LiveJasminHttpClient)liveJasmin.getHttpClient();
LiveJasminHttpClient httpClient = (LiveJasminHttpClient) liveJasmin.getHttpClient();
boolean loggedIn = httpClient.checkLoginSuccess();
if(loggedIn) {
if (loggedIn) {
LOG.info("Logged in");
} else {
LOG.info("Login failed");

View File

@ -26,19 +26,16 @@ public class MyFreeCamsTabProvider extends TabProvider {
List<Tab> tabs = new ArrayList<>();
PaginatedScheduledService updateService = new OnlineCamsUpdateService();
ThumbOverviewTab online = new ThumbOverviewTab("Online", updateService, myFreeCams);
online.setRecorder(recorder);
tabs.add(online);
tabs.add(createTab("Online", updateService));
friends = new MyFreeCamsFriendsTab(myFreeCams);
friends.setRecorder(recorder);
friends.setImageAspectRatio(9.0 / 16.0);
friends.preserveAspectRatioProperty().set(false);
tabs.add(friends);
updateService = new HDCamsUpdateService();
ThumbOverviewTab hd = createTab("HD", updateService);
hd.setImageAspectRatio(9.0 / 16.0);
hd.preserveAspectRatioProperty().set(false);
tabs.add(hd);
tabs.add(createTab("HD", updateService));
updateService = new PopularModelService();
tabs.add(createTab("Most Popular", updateService));
@ -54,6 +51,8 @@ public class MyFreeCamsTabProvider extends TabProvider {
private ThumbOverviewTab createTab(String title, PaginatedScheduledService updateService) {
ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, myFreeCams);
tab.setImageAspectRatio(9.0 / 16.0);
tab.preserveAspectRatioProperty().set(false);
tab.setRecorder(recorder);
return tab;
}

View File

@ -33,6 +33,7 @@ import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.Settings;
import ctbrec.StringUtil;
@ -353,7 +354,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
if (Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
MenuItem debug = new MenuItem("debug");
debug.setOnAction(e -> new Thread(() -> {
debug.setOnAction(e -> GlobalThreadPool.submit(() -> {
for (Model m : selectedModels) {
try {
List<StreamSource> sources = m.getStreamSources();
@ -365,7 +366,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
LOG.error("Couldn't get stream sources", e1);
}
}
}).start());
}));
menu.getItems().add(debug);
}
@ -464,7 +465,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
private String escape(Property<?> prop) {
String value = prop.getValue() != null ? prop.getValue().toString() : "";
return "\"" + value.replaceAll("\"", "\"\"") + "\"";
return "\"" + value.replace("\"", "\"\"") + "\"";
}
private void showColumnSelection(ActionEvent evt) {
@ -566,7 +567,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
if(!file.exists()) {
return;
}
String json = new String(Files.readAllBytes(file.toPath()), "utf-8");
String json = new String(Files.readAllBytes(file.toPath()), UTF_8);
JSONArray data = new JSONArray(json);
for (int i = 0; i < data.length(); i++) {
try {
@ -701,7 +702,6 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
setProperty(continent, Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getContinent));
setProperty(occupation, Optional.ofNullable(st.getU()).map(User::getOccupation));
int flags = Optional.ofNullable(st.getM()).map(ctbrec.sites.mfc.Model::getFlags).orElse(0);
//isHd.set((flags & 1024) == 1024);
isWebrtc.set((flags & 524288) == 524288);
isHd.set(Optional.ofNullable(st.getU()).map(User::getPhase).orElse("z").equalsIgnoreCase("a"));
flagsProperty.setValue(flags);

View File

@ -1,8 +1,6 @@
package ctbrec.ui.sites.showup;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -44,33 +42,17 @@ public class ShowupSiteUi extends AbstractSiteUi {
if (automaticLogin) {
return true;
} else {
BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>();
// login with external browser window
try {
new Thread(() -> {
// login with external browser window
try {
new ShowupElectronLoginDialog(site.getHttpClient().getCookieJar());
} catch (Exception e1) {
LOG.error("Error logging in with external browser", e1);
Dialogs.showError("Login error", "Couldn't login to " + site.getName(), e1);
}
try {
queue.put(true);
} catch (InterruptedException e) {
LOG.error("Error while signaling termination", e);
Thread.currentThread().interrupt();
}
}).start();
queue.take();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
new ShowupElectronLoginDialog(site.getHttpClient().getCookieJar());
} catch (Exception e1) {
LOG.error("Error logging in with external browser", e1);
Dialogs.showError("Login error", "Couldn't login to " + site.getName(), e1);
}
ShowupHttpClient httpClient = (ShowupHttpClient)site.getHttpClient();
ShowupHttpClient httpClient = (ShowupHttpClient) site.getHttpClient();
boolean loggedIn = httpClient.checkLoginSuccess();
if(loggedIn) {
if (loggedIn) {
LOG.info("Logged in");
} else {
LOG.info("Login failed");
@ -78,5 +60,4 @@ public class ShowupSiteUi extends AbstractSiteUi {
return loggedIn;
}
}
}

View File

@ -1,8 +1,6 @@
package ctbrec.ui.sites.stripchat;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -13,7 +11,6 @@ import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.sites.AbstractSiteUi;
import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabProvider;
import javafx.application.Platform;
public class StripchatSiteUi extends AbstractSiteUi {
@ -45,31 +42,12 @@ public class StripchatSiteUi extends AbstractSiteUi {
if (automaticLogin) {
return true;
} else {
BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>();
Runnable showDialog = () -> {
// login with external browser
try {
new StripchatElectronLoginDialog(site.getHttpClient().getCookieJar());
} catch (Exception e1) {
LOG.error("Error logging in with external browser", e1);
Dialogs.showError("Login error", "Couldn't login to " + site.getName(), e1);
}
try {
queue.put(true);
} catch (InterruptedException e) {
LOG.error("Error while signaling termination", e);
}
};
Platform.runLater(showDialog);
// login with external browser
try {
queue.take();
} catch (InterruptedException e) {
LOG.error("Error while waiting for login dialog to close", e);
throw new IOException(e);
new StripchatElectronLoginDialog(site.getHttpClient().getCookieJar());
} catch (Exception e1) {
LOG.error("Error logging in with external browser", e1);
Dialogs.showError("Login error", "Couldn't login to " + site.getName(), e1);
}
StripchatHttpClient httpClient = (StripchatHttpClient) site.getHttpClient();

View File

@ -0,0 +1,346 @@
package ctbrec.ui.tabs;
import static java.nio.charset.StandardCharsets.*;
import static java.nio.file.StandardOpenOption.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.event.EventBusHolder;
import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.ShutdownListener;
import ctbrec.ui.action.FollowAction;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.action.StartRecordingAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.DateTimeCellFactory;
import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.event.PlayerStartedEvent;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Tab;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableView;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.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.HBox;
import javafx.scene.layout.Priority;
import javafx.util.Callback;
public class RecentlyWatchedTab extends Tab implements ShutdownListener {
private static final Logger LOG = LoggerFactory.getLogger(RecentlyWatchedTab.class);
private ObservableList<PlayerStartedEvent> filteredModels = FXCollections.observableArrayList();
private ObservableList<PlayerStartedEvent> observableModels = FXCollections.observableArrayList();
private TableView<PlayerStartedEvent> table = new TableView<>();
private ContextMenu popup;
private ReentrantLock lock = new ReentrantLock();
private Recorder recorder;
private List<Site> sites;
public RecentlyWatchedTab(Recorder recorder, List<Site> sites) {
this.recorder = recorder;
this.sites = sites;
setText("Recently Watched");
createGui();
loadHistory();
subscribeToPlayerEvents();
setOnClosed(evt -> onShutdown());
}
private void createGui() {
BorderPane layout = new BorderPane();
layout.setPadding(new Insets(5, 10, 10, 10));
SearchBox filterInput = new SearchBox(false);
filterInput.setPromptText("Filter");
filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> {
String filter = filterInput.getText();
lock.lock();
try {
filter(filter);
} finally {
lock.unlock();
}
});
filterInput.getStyleClass().remove("search-box-icon");
HBox.setHgrow(filterInput, Priority.ALWAYS);
HBox topBar = new HBox(5);
topBar.getChildren().addAll(filterInput);
layout.setTop(topBar);
BorderPane.setMargin(topBar, new Insets(0, 0, 5, 0));
table.setItems(observableModels);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
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<PlayerStartedEvent> selectedModels = table.getSelectionModel().getSelectedItems();
if (event.getCode() == KeyCode.DELETE) {
delete(selectedModels);
}
});
int idx = 0;
TableColumn<PlayerStartedEvent, String> name = createTableColumn("Model", 200, idx++);
name.setId("name");
name.setCellValueFactory(cdf -> new SimpleStringProperty(cdf.getValue().getModel().getDisplayName()));
name.setCellFactory(new ClickableCellFactory<>());
table.getColumns().add(name);
TableColumn<PlayerStartedEvent, String> url = createTableColumn("URL", 400, idx);
url.setCellValueFactory(cdf -> new SimpleStringProperty(cdf.getValue().getModel().getUrl()));
url.setCellFactory(new ClickableCellFactory<>());
url.setEditable(false);
url.setId("url");
table.getColumns().add(url);
TableColumn<PlayerStartedEvent, Instant> timestamp = createTableColumn("Timestamp", 150, idx);
timestamp.setId("timestamp");
timestamp.setCellValueFactory(cdf -> new SimpleObjectProperty<Instant>(cdf.getValue().getTimestamp()));
timestamp.setCellFactory(new DateTimeCellFactory<>());
timestamp.setEditable(false);
timestamp.setSortType(SortType.DESCENDING);
table.getColumns().add(timestamp);
table.getSortOrder().add(timestamp);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
scrollPane.setContent(table);
scrollPane.setStyle("-fx-background-color: -fx-background");
layout.setCenter(scrollPane);
setContent(layout);
}
private <T> TableColumn<PlayerStartedEvent, T> createTableColumn(String text, int width, int idx) {
TableColumn<PlayerStartedEvent, T> tc = new TableColumn<>(text);
tc.setPrefWidth(width);
tc.setUserData(idx);
return tc;
}
private void filter(String filter) {
lock.lock();
try {
if (StringUtil.isBlank(filter)) {
observableModels.addAll(filteredModels);
filteredModels.clear();
return;
}
String[] tokens = filter.split(" ");
observableModels.addAll(filteredModels);
filteredModels.clear();
for (int i = 0; i < table.getItems().size(); i++) {
StringBuilder sb = new StringBuilder();
for (TableColumn<PlayerStartedEvent, ?> 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) {
PlayerStartedEvent sessionState = table.getItems().get(i);
filteredModels.add(sessionState);
}
}
observableModels.removeAll(filteredModels);
} finally {
lock.unlock();
}
}
private ContextMenu createContextMenu() {
ObservableList<PlayerStartedEvent> selectedRows = table.getSelectionModel().getSelectedItems();
if (selectedRows.isEmpty()) {
return null;
}
List<Model> selectedModels = selectedRows.stream().map(PlayerStartedEvent::getModel).collect(Collectors.toList());
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 startRecording = new MenuItem("Start Recording");
startRecording.setOnAction(e -> startRecording(selectedModels));
MenuItem openInBrowser = new MenuItem("Open in Browser");
openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl()));
MenuItem openInPlayer = new MenuItem("Open in Player");
openInPlayer.setOnAction(e -> openInPlayer(selectedModels.get(0)));
MenuItem follow = new MenuItem("Follow");
follow.setOnAction(e -> new FollowAction(getTabPane(), selectedModels).execute());
MenuItem delete = new MenuItem("Delete");
delete.setOnAction(e -> delete(selectedRows));
MenuItem clearHistory = new MenuItem("Clear history");
clearHistory.setOnAction(e -> clearHistory());
ContextMenu menu = new CustomMouseBehaviorContextMenu();
menu.getItems().addAll(startRecording, copyUrl, openInPlayer, openInBrowser, follow, delete, clearHistory);
if (selectedModels.size() > 1) {
copyUrl.setDisable(true);
openInPlayer.setDisable(true);
openInBrowser.setDisable(true);
}
return menu;
}
private void clearHistory() {
observableModels.clear();
filteredModels.clear();
}
private void delete(List<PlayerStartedEvent> selectedRows) {
observableModels.removeAll(selectedRows);
}
private void startRecording(List<Model> selectedModels) {
new StartRecordingAction(getTabPane(), selectedModels, recorder).execute();
}
private void openInPlayer(Model selectedModel) {
new PlayAction(getTabPane(), selectedModel).execute();
}
private void subscribeToPlayerEvents() {
EventBusHolder.BUS.register(new Object() {
@Subscribe
public void handleEvent(PlayerStartedEvent evt) {
table.getItems().add(evt);
table.sort();
}
});
}
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) {
Model selectedModel = table.getSelectionModel().getSelectedItem().getModel();
if(selectedModel != null) {
new PlayAction(table, selectedModel).execute();
}
}
});
return cell;
}
}
private void saveHistory() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter(sites))
.add(Instant.class, new InstantJsonAdapter())
.build();
Type type = Types.newParameterizedType(List.class, PlayerStartedEvent.class);
JsonAdapter<List<PlayerStartedEvent>> adapter = moshi.adapter(type);
String json = adapter.indent(" ").toJson(observableModels);
File recentlyWatched = new File(Config.getInstance().getConfigDir(), "recently_watched.json");
LOG.debug("Saving recently watched models to {}", recentlyWatched.getAbsolutePath());
Files.createDirectories(recentlyWatched.getParentFile().toPath());
Files.write(recentlyWatched.toPath(), json.getBytes(UTF_8), CREATE, WRITE, TRUNCATE_EXISTING);
}
private void loadHistory() {
File recentlyWatched = new File(Config.getInstance().getConfigDir(), "recently_watched.json");
if(!recentlyWatched.exists()) {
return;
}
LOG.debug("Loading recently watched models from {}", recentlyWatched.getAbsolutePath());
Moshi moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter(sites))
.add(Instant.class, new InstantJsonAdapter())
.build();
Type type = Types.newParameterizedType(List.class, PlayerStartedEvent.class);
JsonAdapter<List<PlayerStartedEvent>> adapter = moshi.adapter(type);
try {
List<PlayerStartedEvent> fromJson = adapter.fromJson(Files.readString(recentlyWatched.toPath(), UTF_8));
observableModels.addAll(fromJson);
} catch (IOException e) {
LOG.error("Couldn't load recently watched models", e);
}
}
@Override
public void onShutdown() {
try {
saveHistory();
} catch (IOException e) {
LOG.error("Couldn't safe recently watched models", e);
}
}
}

View File

@ -4,12 +4,13 @@ import java.util.List;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.ShutdownListener;
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 {
public class RecordedTab extends Tab implements TabSelectionListener, ShutdownListener {
private TabPane tabPane;
private RecordedModelsTab recordedModelsTab;
@ -54,7 +55,8 @@ public class RecordedTab extends Tab implements TabSelectionListener {
}
}
public void saveState() {
@Override
public void onShutdown() {
recordedModelsTab.saveState();
recordLaterTab.saveState();
}

View File

@ -28,6 +28,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.Recording.State;
@ -45,6 +46,7 @@ import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.FileDownload;
import ctbrec.ui.JavaFxRecording;
import ctbrec.ui.Player;
import ctbrec.ui.ShutdownListener;
import ctbrec.ui.action.FollowAction;
import ctbrec.ui.action.PauseAction;
import ctbrec.ui.action.PlayAction;
@ -90,7 +92,7 @@ import javafx.scene.text.Font;
import javafx.stage.FileChooser;
import javafx.util.Duration;
public class RecordingsTab extends Tab implements TabSelectionListener {
public class RecordingsTab extends Tab implements TabSelectionListener, ShutdownListener {
private static final String ERROR_WHILE_DOWNLOADING_RECORDING = "Error while downloading recording";
private static final Logger LOG = LoggerFactory.getLogger(RecordingsTab.class);
@ -496,9 +498,9 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
private void openContactSheet(JavaFxRecording recording) {
if (config.getSettings().localRecording) {
recording.getContactSheet().ifPresent(f -> new Thread(() -> DesktopIntegration.open(f)).start());
recording.getContactSheet().ifPresent(f -> GlobalThreadPool.submit(() -> DesktopIntegration.open(f)));
} else {
recording.getContactSheet().ifPresent(f -> new Thread(() -> {
recording.getContactSheet().ifPresent(f -> GlobalThreadPool.submit(() -> {
File target;
try {
target = File.createTempFile("cs_", ".jpg");
@ -516,7 +518,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) {
Dialogs.showError(getTabPane().getScene(), "Download Error", "An error occurred while downloading the contact sheet", e);
}
}).start());
}));
}
}
@ -526,7 +528,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
Optional<String> newNote = Dialogs.showTextInput(source.getScene(), "Recording Notes", "", notes);
if (newNote.isPresent()) {
table.setCursor(Cursor.WAIT);
Thread backgroundThread = new Thread(() -> {
GlobalThreadPool.submit(() -> {
List<Exception> exceptions = new ArrayList<>();
try {
recording.setNote(newNote.get());
@ -542,13 +544,12 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
});
}
});
backgroundThread.start();
}
}
private void pin(List<JavaFxRecording> recordings) {
table.setCursor(Cursor.WAIT);
Thread backgroundThread = new Thread(() -> {
GlobalThreadPool.submit(() -> {
List<Exception> exceptions = new ArrayList<>();
try {
for (JavaFxRecording javaFxRecording : recordings) {
@ -569,12 +570,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
});
}
});
backgroundThread.start();
}
private void unpin(List<JavaFxRecording> recordings) {
table.setCursor(Cursor.WAIT);
Thread backgroundThread = new Thread(() -> {
GlobalThreadPool.submit(() -> {
List<Exception> exceptions = new ArrayList<>();
try {
for (JavaFxRecording javaFxRecording : recordings) {
@ -595,7 +595,6 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
});
}
});
backgroundThread.start();
}
private void jumpToNextModel(KeyCode code) {
@ -646,11 +645,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
private void onOpenDirectory(JavaFxRecording first) {
File tsFile = first.getAbsoluteFile();
new Thread(() -> DesktopIntegration.open(tsFile.getParent())).start();
GlobalThreadPool.submit(() -> DesktopIntegration.open(tsFile.getParent()));
}
private void triggerPostProcessing(List<JavaFxRecording> recs) {
new Thread(() -> {
GlobalThreadPool.submit(() -> {
for (JavaFxRecording rec : recs) {
try {
recorder.rerunPostProcessing(rec.getDelegate());
@ -659,7 +658,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
LOG.error("Error while starting post-processing", e1);
}
}
}).start();
});
}
private void download(Recording recording) {
@ -761,12 +760,12 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
}
private void play(Recording recording) {
new Thread(() -> {
GlobalThreadPool.submit(() -> {
boolean started = Player.play(recording);
if (started && Config.getInstance().getSettings().showPlayerStarting) {
Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500));
}
}).start();
});
}
private void play(Model model) {
@ -795,7 +794,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
}
private void deleteAsync(List<JavaFxRecording> recordings) {
Thread deleteThread = new Thread(() -> {
GlobalThreadPool.submit(() -> {
recordingsLock.lock();
try {
List<Recording> deleted = new ArrayList<>();
@ -821,10 +820,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
Platform.runLater(() -> table.setCursor(Cursor.DEFAULT));
}
});
deleteThread.start();
}
public void saveState() {
@Override
public void onShutdown() {
if (!table.getSortOrder().isEmpty()) {
TableColumn<JavaFxRecording, ?> col = table.getSortOrder().get(0);
Config.getInstance().getSettings().recordingsSortColumn = col.getText();

View File

@ -8,8 +8,6 @@ import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@ -21,6 +19,7 @@ import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.Model.State;
import ctbrec.io.HttpException;
@ -106,7 +105,6 @@ public class ThumbCell extends StackPane {
private ObservableList<Node> thumbCellList;
private boolean mouseHovering = false;
private boolean recording = false;
private static ExecutorService imageLoadingThreadPool = Executors.newFixedThreadPool(30);
static LoadingCache<Model, int[]> resolutionCache = CacheBuilder.newBuilder()
.expireAfterAccess(5, TimeUnit.MINUTES)
.maximumSize(10000)
@ -285,7 +283,7 @@ public class ThumbCell extends StackPane {
Thread.currentThread().interrupt();
return false;
}
}).whenComplete((result, exception) -> {
}, GlobalThreadPool.get()).whenComplete((result, exception) -> {
startPreview = null;
if (result.booleanValue()) {
setPreviewVisible(previewTrigger, true);
@ -387,7 +385,7 @@ public class ThumbCell extends StackPane {
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
boolean updateThumbs = Config.getInstance().getSettings().updateThumbnails;
if (updateThumbs || iv.getImage() == null) {
imageLoadingThreadPool.submit(createThumbDownload(url));
GlobalThreadPool.submit(createThumbDownload(url));
}
}
}
@ -512,7 +510,7 @@ public class ThumbCell extends StackPane {
void pauseResumeAction(boolean pause) {
setCursor(Cursor.WAIT);
new Thread(() -> {
GlobalThreadPool.submit(() -> {
try {
if (pause) {
recorder.suspendRecording(model);
@ -532,11 +530,11 @@ public class ThumbCell extends StackPane {
} finally {
Platform.runLater(() -> setCursor(Cursor.DEFAULT));
}
}).start();
});
}
private void startStopActionAsync(Model model, boolean start) {
new Thread(() -> {
GlobalThreadPool.submit(() -> {
try {
if (start) {
recorder.addModel(model);
@ -552,7 +550,7 @@ public class ThumbCell extends StackPane {
} finally {
Platform.runLater(() -> setCursor(Cursor.DEFAULT));
}
}).start();
});
}
CompletableFuture<Boolean> follow(boolean follow) {
@ -587,7 +585,7 @@ public class ThumbCell extends StackPane {
} finally {
Platform.runLater(() -> setCursor(Cursor.DEFAULT));
}
});
}, GlobalThreadPool.get());
}
void recordLater(boolean recordLater) {

View File

@ -27,6 +27,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.event.EventBusHolder;
import ctbrec.recorder.Recorder;
@ -299,7 +300,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
return;
}
searchTask = new ThumbOverviewTabSearchTask(site, popover, popoverTreeList, newValue);
new Thread(searchTask).start();
GlobalThreadPool.submit(searchTask);
};
}
@ -602,7 +603,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
private MenuItem createTipMenuItem(ThumbCell cell) {
MenuItem sendTip = new MenuItem("Send Tip");
sendTip.setOnAction(e -> {
TipDialog tipDialog = new TipDialog(getTabPane().getScene(), site, cell.getModel());
TipDialog tipDialog = new TipDialog(getTabPane().getScene(), site);
tipDialog.showAndWait();
String tipText = tipDialog.getResult();
if(tipText != null) {

View File

@ -1,10 +1,9 @@
package ctbrec.ui.tabs;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.GlobalThreadPool;
import ctbrec.io.HttpException;
import ctbrec.ui.CamrecApplication;
import ctbrec.ui.CamrecApplication.Release;
@ -47,7 +46,7 @@ public class UpdateTab extends Tab implements TabSelectionListener {
}
public void loadChangeLog() {
CompletableFuture.runAsync(() -> {
GlobalThreadPool.submit(() -> {
Request req = new Request.Builder().url("https://pastebin.com/raw/fiAPtM0s").build();
try (Response resp = CamrecApplication.httpClient.execute(req)) {
if (resp.isSuccessful()) {

View File

@ -0,0 +1,32 @@
package ctbrec;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class GlobalThreadPool {
private static ExecutorService threadPool = Executors.newFixedThreadPool(30, r -> {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("GlobalWorker-" + UUID.randomUUID().toString().substring(0, 8));
return t;
});
private GlobalThreadPool() {
}
public static Future<?> submit(Runnable runnable) { // NOSONAR
return threadPool.submit(runnable);
}
public static <T> Future<T> submit(Callable<T> callable) {
return threadPool.submit(callable);
}
public static ExecutorService get() {
return threadPool;
}
}

View File

@ -3,6 +3,7 @@ package ctbrec;
import static ctbrec.Recording.State.*;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.time.Duration;
import java.time.Instant;
@ -14,6 +15,9 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.event.EventBusHolder;
import ctbrec.event.RecordingStateChangedEvent;
import ctbrec.io.IoUtils;
@ -21,6 +25,8 @@ import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.VideoLengthDetector;
public class Recording implements Serializable, Callable<Recording> {
private static final transient Logger LOG = LoggerFactory.getLogger(Recording.class);
private String id;
private Model model;
private transient Download download;
@ -104,10 +110,6 @@ public class Recording implements Serializable, Callable<Recording> {
this.progress = progress;
}
// public String getPath() {
// return path;
// }
public void setPath(String path) {
this.path = path;
}
@ -246,22 +248,43 @@ public class Recording implements Serializable, Callable<Recording> {
}
private long getSize() {
File rec = getAbsoluteFile();
if (rec.isDirectory()) {
return IoUtils.getDirectorySize(rec);
} else {
if (!rec.exists()) {
if (rec.getName().endsWith(".m3u8")) {
return IoUtils.getDirectorySize(rec.getParentFile());
try {
Set<File> files = getAllRecordingFiles();
long sum = 0;
for (File file : files) {
if (file.isDirectory()) {
sum += IoUtils.getDirectorySize(file);
} else {
return -1;
if (!file.exists()) {
if (file.getName().endsWith(".m3u8")) {
sum += IoUtils.getDirectorySize(file.getParentFile());
}
} else {
sum += file.length();
}
}
} else {
return rec.length();
}
return sum;
} catch (IOException e) {
LOG.error("Couldn't determine recording size", e);
return -1;
}
}
private Set<File> getAllRecordingFiles() throws IOException {
Set<File> files = new HashSet<>();
if (absoluteFile != null) {
files.add(absoluteFile.getCanonicalFile());
}
if (postProcessedFile != null) {
files.add(postProcessedFile.getCanonicalFile());
}
for (String associatedFile : associatedFiles) {
files.add(new File(associatedFile).getCanonicalFile());
}
return files;
}
public void refresh() {
sizeInByte = getSize();
}

View File

@ -120,6 +120,7 @@ public class Settings {
public String proxyPort;
public ProxyType proxyType = ProxyType.DIRECT;
public String proxyUser;
public boolean recentlyWatched = true;
public double[] recordLaterColumnWidths = new double[0];
public String[] recordLaterColumnIds = new String[0];
public String recordLaterSortColumn = "";

View File

@ -1,15 +1,7 @@
package ctbrec.io;
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 static java.nio.charset.StandardCharsets.*;
import javax.net.ssl.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@ -21,11 +13,40 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static java.nio.charset.StandardCharsets.UTF_8;
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;
public abstract class HttpClient {
private static final Logger LOG = LoggerFactory.getLogger(HttpClient.class);
@ -275,10 +296,22 @@ public abstract class HttpClient {
}
public static String bodyToJsonObject(Response response) {
return Optional.ofNullable(response.body()).map(Object::toString).orElse("{}");
return Optional.ofNullable(response.body()).map(b -> {
try {
return b.string();
} catch (IOException e) {
return "{}";
}
}).orElse("{}");
}
public static String bodyToJsonArray(Response response) {
return Optional.ofNullable(response.body()).map(Object::toString).orElse("[]");
return Optional.ofNullable(response.body()).map(b -> {
try {
return b.string();
} catch (IOException e) {
return "[]";
}
}).orElse("[]");
}
}

View File

@ -28,7 +28,6 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
@ -216,6 +215,7 @@ public class NextGenLocalRecorder implements Recorder {
break;
}
}
recording.refresh();
if (recording.getStatus() != State.DELETED) {
setRecordingStatus(recording, State.FINISHED);
recordingManager.saveRecording(recording);
@ -448,58 +448,40 @@ public class NextGenLocalRecorder implements Recorder {
recording = false;
if (!immediately) {
LOG.debug("Stopping all recording processes");
recorderLock.lock();
try {
// make a copy to avoid ConcurrentModificationException
List<Recording> toStop = new ArrayList<>(recordingProcesses.values());
if (!toStop.isEmpty()) {
ExecutorService shutdownPool = Executors.newFixedThreadPool(toStop.size());
List<Future<?>> shutdownFutures = new ArrayList<>(toStop.size());
for (Recording rec : toStop) {
Optional.ofNullable(rec.getDownload()).ifPresent(d -> {
shutdownFutures.add(shutdownPool.submit(() -> d.stop()));
});
}
shutdownPool.shutdown();
try {
shutdownPool.awaitTermination(10, TimeUnit.MINUTES);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} finally {
recorderLock.unlock();
}
stopRecordingProcesses();
awaitDownloadsFinish();
shutdownThreadPools();
}
}
// wait for downloads to finish
LOG.info("Waiting for downloads to finish");
for (int i = 0; i < 60; i++) {
if (!recordingProcesses.isEmpty()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.error("Error while waiting for downloads to finish", e);
}
private void awaitDownloadsFinish() {
LOG.info("Waiting for downloads to finish");
for (int i = 0; i < 60; i++) {
if (!recordingProcesses.isEmpty()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.error("Error while waiting for downloads to finish", e);
}
}
}
}
// shutdown threadpools
try {
LOG.info("Shutting down download pool");
downloadPool.shutdown();
client.shutdown();
downloadPool.awaitTermination(1, TimeUnit.MINUTES);
LOG.info("Shutting down post-processing pool");
ppPool.shutdown();
int minutesToWait = 10;
LOG.info("Waiting {} minutes (max) for post-processing to finish", minutesToWait);
ppPool.awaitTermination(minutesToWait, TimeUnit.MINUTES);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.error("Error while waiting for pools to finish", e);
}
private void shutdownThreadPools() {
try {
LOG.info("Shutting down download pool");
downloadPool.shutdown();
client.shutdown();
downloadPool.awaitTermination(1, TimeUnit.MINUTES);
LOG.info("Shutting down post-processing pool");
ppPool.shutdown();
int minutesToWait = 10;
LOG.info("Waiting {} minutes (max) for post-processing to finish", minutesToWait);
ppPool.awaitTermination(minutesToWait, TimeUnit.MINUTES);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.error("Error while waiting for pools to finish", e);
}
}
@ -703,7 +685,6 @@ public class NextGenLocalRecorder implements Recorder {
config.save();
} else {
LOG.warn("Couldn't change priority for model {}. Not found in list", model.getName());
return;
}
} catch (IOException e) {
LOG.error("Couldn't save config", e);

View File

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

View File

@ -1,10 +1,12 @@
package ctbrec.recorder.download.hls;
import static ctbrec.recorder.download.StreamSource.*;
import static java.util.concurrent.TimeUnit.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Duration;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
@ -41,6 +43,8 @@ public class HlsdlDownload extends AbstractDownload {
private transient Hlsdl hlsdl;
protected transient Process hlsdlProcess;
protected transient boolean running = true;
protected transient Instant lastSizeChange = Instant.now();
protected transient long lastSize = 0;
@Override
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
@ -73,6 +77,17 @@ public class HlsdlDownload extends AbstractDownload {
if (splittingStrategy.splitNecessary(this)) {
stop();
}
long size = getSizeInByte();
if (size != lastSize) {
lastSize = size;
lastSizeChange = Instant.now();
} else {
int seconds = 90;
if (Duration.between(lastSizeChange, Instant.now()).toMillis() > SECONDS.toMillis(seconds)) {
LOG.info("Recording size didn't change for {} secs. Stopping recording for {}", seconds, model);
stop();
}
}
} catch (ProcessExitedUncleanException e) {
LOG.error("hlsdl exited unclean", e);
}

View File

@ -1,5 +1,18 @@
package ctbrec.sites.cam4;
import static ctbrec.io.HttpClient.*;
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;
@ -8,18 +21,6 @@ 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 {
@ -121,27 +122,27 @@ public class Cam4 extends AbstractSite {
private void search(String q, boolean offline, List<Model> models) throws IOException {
String url = BASE_URI + "/usernameSearch?username=" + URLEncoder.encode(q, "utf-8");
if(offline) {
if (offline) {
url += "&offline=true";
}
Request req = new Request.Builder()
.url(url)
.addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
try(Response response = getHttpClient().execute(req)) {
if(response.isSuccessful()) {
String body = bodyToJsonObject(response);
try (Response response = getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = bodyToJsonArray(response);
JSONArray results = new JSONArray(body);
for (int i = 0; i < results.length(); i++) {
JSONObject result = results.getJSONObject(i);
Model model = createModel(result.getString("username"));
String thumb = null;
if(result.has("thumbnailId")) {
if (result.has("thumbnailId")) {
thumb = "https://snapshots.xcdnpro.com/thumbnails/" + model.getName() + "?s=" + result.getString("thumbnailId");
} else {
thumb = result.getString("profileImageLink");
}
if(StringUtil.isNotBlank(thumb)) {
if (StringUtil.isNotBlank(thumb)) {
model.setPreview(thumb);
}
models.add(model);

View File

@ -191,9 +191,13 @@ public class Cam4Model extends AbstractModel {
StreamSource src = new StreamSource();
src.bandwidth = playlist.getStreamInfo().getBandwidth();
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);
src.mediaPlaylistUrl = baseUrl + playlist.getUri();
if (playlist.getUri().startsWith("http")) {
src.mediaPlaylistUrl = playlist.getUri();
} else {
String masterUrl = getPlaylistUrl();
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
src.mediaPlaylistUrl = baseUrl + playlist.getUri();
}
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
sources.add(src);
}

View File

@ -11,6 +11,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.io.HttpClient;
import okhttp3.Request;
@ -78,9 +79,9 @@ public class LiveJasminTippingWebSocket {
LOG.trace("relay <-- {} T{}", model.getName(), text);
JSONObject event = new JSONObject(text);
if (event.optString("event").equals("accept")) {
new Thread(() -> {
GlobalThreadPool.submit(() -> {
sendToRelay("{\"event\":\"connectSharedObject\",\"name\":\"data/chat_so\"}");
}).start();
});
} else if(event.optString("event").equals("call")) {
String func = event.optString("funcName");
if (func.equals("setName")) {

View File

@ -1,5 +1,6 @@
package ctbrec.recorder.server;
import static java.nio.charset.StandardCharsets.*;
import static javax.servlet.http.HttpServletResponse.*;
import java.io.BufferedReader;
@ -53,6 +54,7 @@ import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;
import ctbrec.Config;
import ctbrec.NotLoggedInExcetion;
import ctbrec.Version;
import ctbrec.event.EventBusHolder;
import ctbrec.event.EventHandler;
@ -124,7 +126,7 @@ public class HttpServer {
if (success) {
LOG.info("Successfully logged in to {}", site.getName());
} else {
throw new RuntimeException("Login returned false");
throw new NotLoggedInExcetion("Login returned false");
}
} catch (Exception e) {
LOG.info("Login to {} failed", site.getName());
@ -243,7 +245,7 @@ public class HttpServer {
byte[] hmac = Optional.ofNullable(HttpServer.this.config.getSettings().key).orElse(new byte[0]);
try {
JSONObject response = new JSONObject();
response.put("hmac", new String(hmac, "utf-8"));
response.put("hmac", new String(hmac, UTF_8));
resp.getOutputStream().println(response.toString());
} catch (Exception e) {
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

View File

@ -1,23 +1,7 @@
package ctbrec.recorder.server;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.FileJsonAdapter;
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 static javax.servlet.http.HttpServletResponse.*;
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;
@ -27,7 +11,27 @@ import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import static javax.servlet.http.HttpServletResponse.*;
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.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.FileJsonAdapter;
import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
public class RecorderServlet extends AbstractCtbrecServlet {
@ -87,24 +91,24 @@ public class RecorderServlet extends AbstractCtbrecServlet {
resp.getWriter().write(response);
break;
case "stop":
new Thread(() -> {
GlobalThreadPool.submit(() -> {
try {
recorder.stopRecording(request.model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
LOG.error("Couldn't stop recording for model {}", request.model, e);
}
}).start();
});
response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
resp.getWriter().write(response);
break;
case "stopAt":
new Thread(() -> {
GlobalThreadPool.submit(() -> {
try {
recorder.stopRecordingAt(request.model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
LOG.error("Couldn't stop recording for model {}", request.model, e);
}
}).start();
});
response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
resp.getWriter().write(response);
break;
@ -189,13 +193,13 @@ public class RecorderServlet extends AbstractCtbrecServlet {
break;
case "suspend":
LOG.debug("Suspend recording for model {} - {}", request.model.getName(), request.model.getUrl());
new Thread(() -> {
GlobalThreadPool.submit(() -> {
try {
recorder.suspendRecording(request.model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
LOG.error("Couldn't suspend recording for model {}", request.model, e);
}
}).start();
});
response = "{\"status\": \"success\", \"msg\": \"Suspending recording\"}";
resp.getWriter().write(response);
break;