forked from j62/ctbrec
1
0
Fork 0

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 3.12.1
======================== ========================
* Fix: "Resume all" started the recordings of models marked for later recording * 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 3.12.0
======================== ========================

View File

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

View File

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

View File

@ -25,10 +25,12 @@ import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.OS; import ctbrec.OS;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.event.EventBusHolder;
import ctbrec.io.StreamRedirector; import ctbrec.io.StreamRedirector;
import ctbrec.io.UrlUtil; import ctbrec.io.UrlUtil;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.event.PlayerStartedEvent;
import javafx.scene.Scene; import javafx.scene.Scene;
public class Player { public class Player {
@ -85,6 +87,7 @@ public class Player {
} }
String playlistUrl = getPlaylistUrl(model); String playlistUrl = getPlaylistUrl(model);
LOG.debug("Playing {}", playlistUrl); LOG.debug("Playing {}", playlistUrl);
EventBusHolder.BUS.post(new PlayerStartedEvent(model));
return Player.play(playlistUrl, async); return Player.play(playlistUrl, async);
} else { } else {
Dialogs.showError(scene, "Room not public", "Room is currently not public", null); 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, // 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 // 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 StreamRedirector(playerProcess.getInputStream(), OutputStream.nullOutputStream()));
//Thread std = new Thread(new StreamRedirectThread(playerProcess.getInputStream(), System.out));
std.setName("Player stdout pipe"); std.setName("Player stdout pipe");
std.setDaemon(true); std.setDaemon(true);
std.start(); std.start();
Thread err = new Thread(new StreamRedirector(playerProcess.getErrorStream(), OutputStream.nullOutputStream())); 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.setName("Player stderr pipe");
err.setDaemon(true); err.setDaemon(true);
err.start(); err.start();

View File

@ -22,7 +22,7 @@ import javafx.scene.layout.StackPane;
import javafx.stage.Popup; import javafx.stage.Popup;
public class PreviewPopupHandler implements EventHandler<MouseEvent> { 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 static final int offset = 10;
private long timeForPopupOpen = TimeUnit.SECONDS.toMillis(1); private long timeForPopupOpen = TimeUnit.SECONDS.toMillis(1);
@ -67,11 +67,11 @@ public class PreviewPopupHandler implements EventHandler<MouseEvent> {
} else if(event.getEventType() == MouseEvent.MOUSE_ENTERED) { } else if(event.getEventType() == MouseEvent.MOUSE_ENTERED) {
popup.setX(event.getScreenX()+ offset); popup.setX(event.getScreenX()+ offset);
popup.setY(event.getScreenY()+ offset); popup.setY(event.getScreenY()+ offset);
JavaFxModel model = getModel(event); JavaFxModel newModel = getModel(event);
if(model != null) { if(newModel != null) {
closeCountdown = -1; closeCountdown = -1;
boolean modelChanged = model != this.model; boolean modelChanged = newModel != this.model;
this.model = model; this.model = newModel;
if(popup.isShowing()) { if(popup.isShowing()) {
openCountdown = -1; openCountdown = -1;
if(modelChanged) { if(modelChanged) {
@ -97,15 +97,15 @@ public class PreviewPopupHandler implements EventHandler<MouseEvent> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
TableRow<JavaFxModel> row = (TableRow<JavaFxModel>) event.getSource(); TableRow<JavaFxModel> row = (TableRow<JavaFxModel>) event.getSource();
TableView<JavaFxModel> table = row.getTableView(); TableView<JavaFxModel> table = row.getTableView();
double offset = 0; double columnOffset = 0;
double width = 0; double width = 0;
for (TableColumn<JavaFxModel, ?> col : table.getColumns()) { for (TableColumn<JavaFxModel, ?> col : table.getColumns()) {
offset += width; columnOffset += width;
width = col.getWidth(); width = col.getWidth();
if(Objects.equals(col.getId(), "preview")) { if(Objects.equals(col.getId(), "preview")) {
Point2D screenToLocal = table.screenToLocal(event.getScreenX(), event.getScreenY()); Point2D screenToLocal = table.screenToLocal(event.getScreenX(), event.getScreenY());
double x = screenToLocal.getX(); double x = screenToLocal.getX();
return x >= offset && x <= offset + width; return x >= columnOffset && x <= columnOffset + width;
} }
} }
return false; return false;
@ -176,6 +176,7 @@ public class PreviewPopupHandler implements EventHandler<MouseEvent> {
try { try {
Thread.sleep(100); Thread.sleep(100);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.error("PreviewPopupTimer interrupted"); LOG.error("PreviewPopupTimer interrupted");
break; 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.concurrent.ExecutionException;
import java.util.function.Function; import java.util.function.Function;
import ctbrec.GlobalThreadPool;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
@ -19,9 +20,10 @@ import javafx.stage.Stage;
public class StreamSourceSelectionDialog { public class StreamSourceSelectionDialog {
private static final StreamSource BEST = new BestStreamSource(); 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>>() { Task<List<StreamSource>> selectStreamSource = new Task<List<StreamSource>>() {
@Override @Override
protected List<StreamSource> call() throws Exception { protected List<StreamSource> call() throws Exception {
@ -35,7 +37,7 @@ public class StreamSourceSelectionDialog {
List<StreamSource> sources; List<StreamSource> sources;
try { try {
sources = selectStreamSource.get(); 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<StreamSource> choiceDialog = new ChoiceDialog<>(sources.get(selectedIndex), sources);
choiceDialog.setTitle("Stream Quality"); choiceDialog.setTitle("Stream Quality");
choiceDialog.setHeaderText("Select your preferred stream quality"); choiceDialog.setHeaderText("Select your preferred stream quality");
@ -45,7 +47,7 @@ public class StreamSourceSelectionDialog {
InputStream icon = Dialogs.class.getResourceAsStream("/icon.png"); InputStream icon = Dialogs.class.getResourceAsStream("/icon.png");
stage.getIcons().add(new Image(icon)); stage.getIcons().add(new Image(icon));
Optional<StreamSource> selectedSource = choiceDialog.showAndWait(); Optional<StreamSource> selectedSource = choiceDialog.showAndWait();
if(selectedSource.isPresent()) { if (selectedSource.isPresent()) {
int index = -1; int index = -1;
if (selectedSource.get() != BEST) { if (selectedSource.get() != BEST) {
index = sources.indexOf(selectedSource.get()); index = sources.indexOf(selectedSource.get());
@ -61,7 +63,7 @@ public class StreamSourceSelectionDialog {
} }
}); });
selectStreamSource.setOnFailed(e -> onFail.apply(selectStreamSource.getException())); selectStreamSource.setOnFailed(e -> onFail.apply(selectStreamSource.getException()));
new Thread(selectStreamSource).start(); GlobalThreadPool.submit(selectStreamSource);
} }
private static class BestStreamSource extends StreamSource { private static class BestStreamSource extends StreamSource {

View File

@ -7,7 +7,7 @@ import java.util.concurrent.ExecutionException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Model; import ctbrec.GlobalThreadPool;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.concurrent.Task; import javafx.concurrent.Task;
@ -19,11 +19,11 @@ import javafx.stage.Stage;
public class TipDialog extends TextInputDialog { 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 Site site;
private Scene parent; private Scene parent;
public TipDialog(Scene parent, Site site, Model model) { public TipDialog(Scene parent, Site site) {
this.parent = parent; this.parent = parent;
this.site = site; this.site = site;
setTitle("Send Tip"); setTitle("Send Tip");
@ -32,7 +32,7 @@ public class TipDialog extends TextInputDialog {
setContentText("Amount of tokens to tip:"); setContentText("Amount of tokens to tip:");
setResizable(true); setResizable(true);
getEditor().setDisable(true); getEditor().setDisable(true);
if(parent != null) { if (parent != null) {
Stage stage = (Stage) getDialogPane().getScene().getWindow(); Stage stage = (Stage) getDialogPane().getScene().getWindow();
stage.getScene().getStylesheets().addAll(parent.getStylesheets()); stage.getScene().getStylesheets().addAll(parent.getStylesheets());
} }
@ -56,14 +56,14 @@ public class TipDialog extends TextInputDialog {
double tokens = get(); double tokens = get();
Platform.runLater(() -> { Platform.runLater(() -> {
if (tokens <= 0) { 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."; + "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); Alert buyTokens = new AutosizeAlert(Alert.AlertType.CONFIRMATION, msg, parent, ButtonType.NO, ButtonType.YES);
buyTokens.setTitle("No tokens"); buyTokens.setTitle("No tokens");
buyTokens.setHeaderText("You don't have any tokens"); buyTokens.setHeaderText("You don't have any tokens");
buyTokens.showAndWait(); buyTokens.showAndWait();
TipDialog.this.close(); TipDialog.this.close();
if(buyTokens.getResult() == ButtonType.YES) { if (buyTokens.getResult() == ButtonType.YES) {
DesktopIntegration.open(site.getAffiliateLink()); DesktopIntegration.open(site.getAffiliateLink());
} }
} else { } else {
@ -72,13 +72,20 @@ public class TipDialog extends TextInputDialog {
setHeaderText("Current token balance: " + df.format(tokens)); setHeaderText("Current token balance: " + df.format(tokens));
} }
}); });
} catch (InterruptedException | ExecutionException e) { } catch (InterruptedException e) {
LOG.error("Couldn't retrieve account balance", e); Thread.currentThread().interrupt();
showErrorDialog(e); 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) { private void showErrorDialog(Throwable throwable) {

View File

@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import ctbrec.GlobalThreadPool;
import ctbrec.event.EventBusHolder; import ctbrec.event.EventBusHolder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import javafx.application.Platform; import javafx.application.Platform;
@ -19,7 +20,7 @@ import javafx.scene.control.Tooltip;
public class TokenLabel extends Label { 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 double tokens = -1;
private Site site; private Site site;
@ -72,17 +73,24 @@ public class TokenLabel extends Label {
@Override @Override
protected void done() { protected void done() {
try { try {
double tokens = get(); tokens = get();
update(tokens); update(tokens);
} catch (InterruptedException | ExecutionException e) { } catch (InterruptedException e) {
LOG.error("Couldn't retrieve account balance", e); Thread.currentThread().interrupt();
Platform.runLater(() -> { handleException(e);
setText("Tokens: error"); } catch (ExecutionException e) {
setTooltip(new Tooltip(e.getMessage())); 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.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.GlobalThreadPool;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
@ -32,7 +32,7 @@ public class CheckModelAccountAction {
public void execute(Predicate<Model> filter) { public void execute(Predicate<Model> filter) {
String buttonText = b.getText(); String buttonText = b.getText();
b.setDisable(true); b.setDisable(true);
CompletableFuture.runAsync(() -> { Runnable checker = (() -> {
List<Model> deletedAccounts = new ArrayList<>(); List<Model> deletedAccounts = new ArrayList<>();
try { try {
List<Model> models = recorder.getModels().stream() // 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.Model;
import ctbrec.ui.JavaFxModel; import ctbrec.ui.JavaFxModel;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
@ -30,23 +29,21 @@ public class EditNotesAction {
public void execute() { public void execute() {
source.setCursor(Cursor.WAIT); source.setCursor(Cursor.WAIT);
new Thread(() -> Platform.runLater(() -> { String notes = Config.getInstance().getSettings().modelNotes.getOrDefault(model.getUrl(), "");
String notes = Config.getInstance().getSettings().modelNotes.getOrDefault(model.getUrl(), ""); Optional<String> newNotes = Dialogs.showTextInput(source.getScene(), "Model Notes", "Notes for " + model.getName(), notes);
Optional<String> newNotes = Dialogs.showTextInput(source.getScene(), "Model Notes", "Notes for " + model.getName(), notes); newNotes.ifPresent(n -> {
newNotes.ifPresent(n -> { if (!n.trim().isEmpty()) {
if (!n.trim().isEmpty()) { Config.getInstance().getSettings().modelNotes.put(model.getUrl(), n);
Config.getInstance().getSettings().modelNotes.put(model.getUrl(), n); } else {
} else { Config.getInstance().getSettings().modelNotes.remove(model.getUrl());
Config.getInstance().getSettings().modelNotes.remove(model.getUrl()); }
} try {
try { Config.getInstance().save();
Config.getInstance().save(); } catch (IOException e) {
} catch (IOException e) { LOG.warn("Couldn't save config", e);
LOG.warn("Couldn't save config", e); }
} });
}); table.refresh();
table.refresh(); source.setCursor(Cursor.DEFAULT);
source.setCursor(Cursor.DEFAULT);
})).start();
} }
} }

View File

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

View File

@ -4,6 +4,7 @@ import java.io.File;
import java.time.Instant; import java.time.Instant;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Settings.DirectoryStructure; import ctbrec.Settings.DirectoryStructure;
import ctbrec.ui.DesktopIntegration; import ctbrec.ui.DesktopIntegration;
@ -26,7 +27,7 @@ public class OpenRecordingsDir {
File fileForRecording = Config.getInstance().getFileForRecording(selectedModel, ".mp4", Instant.now()); File fileForRecording = Config.getInstance().getFileForRecording(selectedModel, ".mp4", Instant.now());
final File dir = getModelDirectory(fileForRecording); final File dir = getModelDirectory(fileForRecording);
if (dir.exists()) { if (dir.exists()) {
new Thread(() -> DesktopIntegration.open(dir)).start(); GlobalThreadPool.submit(() -> DesktopIntegration.open(dir));
} else { } else {
Dialogs.showError(source.getScene(), "Directory does not exist", "There are no recordings for this model", null); 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.time.Instant;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import ctbrec.GlobalThreadPool;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
@ -36,6 +37,6 @@ public class RemoveTimeLimitAction {
Dialogs.showError(source.getScene(), "Error", "Couln't remove stop date", e); Dialogs.showError(source.getScene(), "Error", "Couln't remove stop date", e);
return false; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.GlobalThreadPool;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.SubsequentAction; import ctbrec.SubsequentAction;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
@ -82,7 +83,7 @@ public class SetStopDateAction {
} }
} }
return true; return true;
}).whenComplete((r, e) -> { }, GlobalThreadPool.get()).whenComplete((r, e) -> {
source.setCursor(Cursor.DEFAULT); source.setCursor(Cursor.DEFAULT);
if (e != null) { if (e != null) {
LOG.error("Error", e); LOG.error("Error", e);

View File

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

View File

@ -31,6 +31,14 @@
*/ */
package ctbrec.ui.controls; package ctbrec.ui.controls;
import java.net.URL;
import java.util.Objects;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.GlobalThreadPool;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.PlayAction;
@ -40,18 +48,15 @@ import javafx.event.EventHandler;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.*; 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.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseEvent;
import javafx.scene.shape.Rectangle; import javafx.scene.shape.Rectangle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URL;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
/** /**
* Popover page that displays a list of samples and sample categories for a given SampleCategory. * Popover page that displays a list of samples and sample categories for a given SampleCategory.
@ -163,7 +168,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
follow = new Button("Follow"); follow = new Button("Follow");
follow.setOnAction(evt -> { follow.setOnAction(evt -> {
setCursor(Cursor.WAIT); setCursor(Cursor.WAIT);
CompletableFuture.runAsync(new Task<Boolean>() { GlobalThreadPool.submit(new Task<Boolean>() {
@Override @Override
protected Boolean call() throws Exception { protected Boolean call() throws Exception {
model.getSite().login(); model.getSite().login();
@ -184,7 +189,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
record = new Button("Record"); record = new Button("Record");
record.setOnAction(evt -> { record.setOnAction(evt -> {
setCursor(Cursor.WAIT); setCursor(Cursor.WAIT);
CompletableFuture.runAsync(new Task<Void>() { GlobalThreadPool.submit(new Task<Void>() {
@Override @Override
protected Void call() throws Exception { protected Void call() throws Exception {
recorder.addModel(model); recorder.addModel(model);
@ -292,12 +297,12 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
@Override @Override
protected double computePrefHeight(double width) { protected double computePrefHeight(double width) {
return thumbSize + 20; return thumbSize + 20.0;
} }
@Override @Override
protected double computeMaxHeight(double width) { protected double computeMaxHeight(double width) {
return thumbSize + 20; return thumbSize + 20.0;
} }
@Override @Override

View File

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

View File

@ -1,5 +1,6 @@
package ctbrec.ui.controls; package ctbrec.ui.controls;
import ctbrec.GlobalThreadPool;
import javafx.animation.KeyFrame; import javafx.animation.KeyFrame;
import javafx.animation.KeyValue; import javafx.animation.KeyValue;
import javafx.animation.Timeline; import javafx.animation.Timeline;
@ -13,6 +14,8 @@ import javafx.stage.StageStyle;
import javafx.util.Duration; import javafx.util.Duration;
public final class Toast { public final class Toast {
private Toast() {}
public static void makeText(Scene owner, String toastMsg, int toastDelay, int fadeInDelay, int fadeOutDelay) { public static void makeText(Scene owner, String toastMsg, int toastDelay, int fadeInDelay, int fadeOutDelay) {
Stage toastStage = new Stage(); Stage toastStage = new Stage();
toastStage.initOwner(owner.getWindow()); toastStage.initOwner(owner.getWindow());
@ -35,20 +38,18 @@ public final class Toast {
Timeline fadeInTimeline = new Timeline(); Timeline fadeInTimeline = new Timeline();
KeyFrame fadeInKey1 = new KeyFrame(Duration.millis(fadeInDelay), new KeyValue(toastStage.getScene().getRoot().opacityProperty(), 1)); KeyFrame fadeInKey1 = new KeyFrame(Duration.millis(fadeInDelay), new KeyValue(toastStage.getScene().getRoot().opacityProperty(), 1));
fadeInTimeline.getKeyFrames().add(fadeInKey1); fadeInTimeline.getKeyFrames().add(fadeInKey1);
fadeInTimeline.setOnFinished((ae) -> { fadeInTimeline.setOnFinished(ae -> GlobalThreadPool.submit(() -> {
new Thread(() -> { try {
try { Thread.sleep(toastDelay);
Thread.sleep(toastDelay); } catch (InterruptedException e) {
} catch (InterruptedException e) { Thread.currentThread().interrupt();
Thread.currentThread().interrupt(); }
} Timeline fadeOutTimeline = new Timeline();
Timeline fadeOutTimeline = new Timeline(); KeyFrame fadeOutKey1 = new KeyFrame(Duration.millis(fadeOutDelay), new KeyValue(toastStage.getScene().getRoot().opacityProperty(), 0));
KeyFrame fadeOutKey1 = new KeyFrame(Duration.millis(fadeOutDelay), new KeyValue(toastStage.getScene().getRoot().opacityProperty(), 0)); fadeOutTimeline.getKeyFrames().add(fadeOutKey1);
fadeOutTimeline.getKeyFrames().add(fadeOutKey1); fadeOutTimeline.setOnFinished(aeb -> toastStage.close());
fadeOutTimeline.setOnFinished((aeb) -> toastStage.close()); fadeOutTimeline.play();
fadeOutTimeline.play(); }));
}).start();
});
fadeInTimeline.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; 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.JsonAdapter;
import com.squareup.moshi.Moshi; import com.squareup.moshi.Moshi;
import ctbrec.GlobalThreadPool;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.ui.CamrecApplication; import ctbrec.ui.CamrecApplication;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
@ -14,12 +23,6 @@ import javafx.scene.control.Tab;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; 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 { public class NewsTab extends Tab implements TabSelectionListener {
private static final String ACCESS_TOKEN = "a2804d73a89951a22e0f8483a6fcec8943afd88b7ba17c459c095aa9e6f94fd0"; private static final String ACCESS_TOKEN = "a2804d73a89951a22e0f8483a6fcec8943afd88b7ba17c459c095aa9e6f94fd0";
@ -36,7 +39,7 @@ public class NewsTab extends Tab implements TabSelectionListener {
@Override @Override
public void selected() { public void selected() {
new Thread(this::loadToots).start(); GlobalThreadPool.submit(this::loadToots);
} }
private void loadToots() { private void loadToots() {

View File

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

View File

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

View File

@ -1,17 +1,16 @@
package ctbrec.ui.sites.bonga; 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.BongaCams;
import ctbrec.sites.bonga.BongaCamsHttpClient; import ctbrec.sites.bonga.BongaCamsHttpClient;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.sites.AbstractSiteUi; import ctbrec.ui.sites.AbstractSiteUi;
import ctbrec.ui.sites.ConfigUI; import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabProvider; 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 { public class BongaCamsSiteUi extends AbstractSiteUi {
@ -39,37 +38,20 @@ public class BongaCamsSiteUi extends AbstractSiteUi {
@Override @Override
public synchronized boolean login() throws IOException { public synchronized boolean login() throws IOException {
boolean automaticLogin = bongaCams.login(); boolean automaticLogin = bongaCams.login();
if(automaticLogin) { if (automaticLogin) {
return true; return true;
} else { } else {
BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>(); // login with external browser window
try { try {
new Thread(() -> { new BongaCamsElectronLoginDialog(bongaCams.getHttpClient().getCookieJar());
// login with external browser window } catch (Exception e1) {
try { LOG.error("Error logging in with external browser", e1);
new BongaCamsElectronLoginDialog(bongaCams.getHttpClient().getCookieJar()); Dialogs.showError("Login error", "Couldn't login to " + bongaCams.getName(), e1);
} 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);
} }
BongaCamsHttpClient httpClient = (BongaCamsHttpClient)bongaCams.getHttpClient(); BongaCamsHttpClient httpClient = (BongaCamsHttpClient) bongaCams.getHttpClient();
boolean loggedIn = httpClient.checkLoginSuccess(); boolean loggedIn = httpClient.checkLoginSuccess();
if(loggedIn) { if (loggedIn) {
LOG.info("Logged in. User ID is {}", httpClient.getUserId()); LOG.info("Logged in. User ID is {}", httpClient.getUserId());
} else { } else {
LOG.info("Login failed"); LOG.info("Login failed");

View File

@ -22,7 +22,7 @@ import okhttp3.HttpUrl;
public class Cam4ElectronLoginDialog { 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 DOMAIN = "cam4.com";
public static final String URL = Cam4.BASE_URI + "/login"; public static final String URL = Cam4.BASE_URI + "/login";
private CookieJar cookieJar; private CookieJar cookieJar;
@ -40,15 +40,16 @@ public class Cam4ElectronLoginDialog {
msg.put("config", config); msg.put("config", config);
browser.run(msg, msgHandler); browser.run(msg, msgHandler);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Couldn't wait for login dialog", e); throw new IOException("Couldn't wait for login dialog", e);
} finally { } finally {
browser.close(); browser.close();
} }
} }
private Consumer<String> msgHandler = (line) -> { private Consumer<String> msgHandler = line -> {
if(!line.startsWith("{")) { if(!line.startsWith("{")) {
System.err.println(line); LOG.error("Didn't received a JSON object {}", line);
} else { } else {
JSONObject json = new JSONObject(line); JSONObject json = new JSONObject(line);
if(json.has("url")) { if(json.has("url")) {
@ -75,11 +76,10 @@ public class Cam4ElectronLoginDialog {
} }
if(json.has("cookies")) { if(json.has("cookies")) {
JSONArray _cookies = json.getJSONArray("cookies"); JSONArray cookiesFromBrowser = json.getJSONArray("cookies");
try { try {
URL _url = new URL(url); for (int i = 0; i < cookiesFromBrowser.length(); i++) {
for (int i = 0; i < _cookies.length(); i++) { JSONObject cookie = cookiesFromBrowser.getJSONObject(i);
JSONObject cookie = _cookies.getJSONObject(i);
if(cookie.getString("domain").contains("cam4")) { if(cookie.getString("domain").contains("cam4")) {
String domain = cookie.getString("domain"); String domain = cookie.getString("domain");
if(domain.startsWith(".")) { if(domain.startsWith(".")) {
@ -91,12 +91,8 @@ public class Cam4ElectronLoginDialog {
cookieJar.saveFromResponse(HttpUrl.parse(Cam4.BASE_URI), Collections.singletonList(c)); cookieJar.saveFromResponse(HttpUrl.parse(Cam4.BASE_URI), Collections.singletonList(c));
} }
} }
if (Objects.equals(_url.getPath(), "/")) { if (Objects.equals(new URL(url).getPath(), "/")) {
try { closeBrowser();
browser.close();
} catch(IOException e) {
LOG.error("Couldn't send close request to browser", e);
}
} }
} catch (MalformedURLException e) { } catch (MalformedURLException e) {
LOG.error("Couldn't parse new url {}", url, e); LOG.error("Couldn't parse new url {}", url, e);
@ -112,7 +108,7 @@ public class Cam4ElectronLoginDialog {
.domain(domain) .domain(domain)
.name(cookie.getString("name")) .name(cookie.getString("name"))
.value(cookie.getString("value")) .value(cookie.getString("value"))
.expiresAt(Double.valueOf(cookie.optDouble("expirationDate")).longValue()); .expiresAt((long) cookie.optDouble("expirationDate"));
if(cookie.optBoolean("hostOnly")) { if(cookie.optBoolean("hostOnly")) {
b.hostOnlyDomain(domain); b.hostOnlyDomain(domain);
} }
@ -124,4 +120,12 @@ public class Cam4ElectronLoginDialog {
} }
return b.build(); 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; package ctbrec.ui.sites.cam4;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -13,10 +11,9 @@ import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.sites.AbstractSiteUi; import ctbrec.ui.sites.AbstractSiteUi;
import ctbrec.ui.sites.ConfigUI; import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabProvider; import ctbrec.ui.tabs.TabProvider;
import javafx.application.Platform;
public class Cam4SiteUi extends AbstractSiteUi { 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 Cam4TabProvider tabProvider;
private Cam4ConfigUI configUI; private Cam4ConfigUI configUI;
@ -44,33 +41,13 @@ public class Cam4SiteUi extends AbstractSiteUi {
if (automaticLogin) { if (automaticLogin) {
return true; return true;
} else { } else {
// login with external browser
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);
try { try {
queue.take(); new Cam4ElectronLoginDialog(cam4.getHttpClient().getCookieJar());
} catch (InterruptedException e) { } catch (Exception e1) {
LOG.error("Error while waiting for login dialog to close", e); LOG.error("Error logging in with external browser", e1);
throw new IOException(e); Dialogs.showError("Login error", "Couldn't login to " + cam4.getName(), e1);
} }
Cam4HttpClient httpClient = (Cam4HttpClient) cam4.getHttpClient(); Cam4HttpClient httpClient = (Cam4HttpClient) cam4.getHttpClient();
boolean loggedIn = httpClient.checkLoginSuccess(); boolean loggedIn = httpClient.checkLoginSuccess();
return loggedIn; return loggedIn;

View File

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

View File

@ -1,32 +1,5 @@
package ctbrec.ui.sites.camsoda; 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.io.IOException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@ -40,7 +13,42 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; 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 { 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 @Override
@ -173,17 +181,17 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
grid.add(createLabel(formatter.format(endTime), false), 1, 1); grid.add(createLabel(formatter.format(endTime), false), 1, 1);
Button record = new Button("Record Model"); Button record = new Button("Record Model");
record.setTooltip(new Tooltip(record.getText())); record.setTooltip(new Tooltip(record.getText()));
record.setOnAction((evt) -> record(model)); record.setOnAction(evt -> record(model));
grid.add(record, 1, 2); grid.add(record, 1, 2);
GridPane.setMargin(record, new Insets(10)); GridPane.setMargin(record, new Insets(10));
Button follow = new Button("Follow"); Button follow = new Button("Follow");
follow.setTooltip(new Tooltip(follow.getText())); follow.setTooltip(new Tooltip(follow.getText()));
follow.setOnAction((evt) -> follow(model)); follow.setOnAction(evt -> follow(model));
grid.add(follow, 1, 3); grid.add(follow, 1, 3);
GridPane.setMargin(follow, new Insets(10)); GridPane.setMargin(follow, new Insets(10));
Button openInBrowser = new Button("Open in Browser"); Button openInBrowser = new Button("Open in Browser");
openInBrowser.setTooltip(new Tooltip(openInBrowser.getText())); 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); grid.add(openInBrowser, 1, 4);
GridPane.setMargin(openInBrowser, new Insets(10)); GridPane.setMargin(openInBrowser, new Insets(10));
root.setCenter(grid); root.setCenter(grid);
@ -195,7 +203,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
private void follow(Model model) { private void follow(Model model) {
setCursor(Cursor.WAIT); setCursor(Cursor.WAIT);
CompletableFuture.runAsync(() -> { GlobalThreadPool.submit(() -> {
try { try {
SiteUiFactory.getUi(model.getSite()).login(); SiteUiFactory.getUi(model.getSite()).login();
model.follow(); model.follow();
@ -203,30 +211,26 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
LOG.error("Couldn't follow model {}", model, e); LOG.error("Couldn't follow model {}", model, e);
showErrorDialog("Oh no!", "Couldn't follow model", e.getMessage()); showErrorDialog("Oh no!", "Couldn't follow model", e.getMessage());
} finally { } finally {
Platform.runLater(() -> { Platform.runLater(() -> setCursor(Cursor.DEFAULT));
setCursor(Cursor.DEFAULT);
});
} }
}); });
} }
private void record(Model model) { private void record(Model model) {
setCursor(Cursor.WAIT); setCursor(Cursor.WAIT);
CompletableFuture.runAsync(() -> { GlobalThreadPool.submit(() -> {
try { try {
recorder.addModel(model); recorder.addModel(model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) { } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
showErrorDialog("Oh no!", "Couldn't add model to the recorder", "Recorder error: " + e.getMessage()); showErrorDialog("Oh no!", "Couldn't add model to the recorder", "Recorder error: " + e.getMessage());
} finally { } finally {
Platform.runLater(() -> { Platform.runLater(() -> setCursor(Cursor.DEFAULT));
setCursor(Cursor.DEFAULT);
});
} }
}); });
} }
private void loadImage(Model model, ImageView thumb) { private void loadImage(Model model, ImageView thumb) {
CompletableFuture.runAsync(() -> { GlobalThreadPool.submit(() -> {
try { try {
String url = camsoda.getBaseUrl() + "/api/v1/user/" + model.getName(); String url = camsoda.getBaseUrl() + "/api/v1/user/" + model.getName();
Request detailRequest = new Request.Builder().url(url).build(); Request detailRequest = new Request.Builder().url(url).build();

View File

@ -5,7 +5,6 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -21,7 +20,7 @@ import okhttp3.Response;
public class ChaturbateUpdateService extends PaginatedScheduledService { 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 String url;
private boolean loginRequired; private boolean loginRequired;
private Chaturbate chaturbate; private Chaturbate chaturbate;
@ -31,14 +30,11 @@ public class ChaturbateUpdateService extends PaginatedScheduledService {
this.loginRequired = loginRequired; this.loginRequired = loginRequired;
this.chaturbate = chaturbate; this.chaturbate = chaturbate;
ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() { ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
@Override Thread t = new Thread(r);
public Thread newThread(Runnable r) { t.setDaemon(true);
Thread t = new Thread(r); t.setName("ThumbOverviewTab UpdateService");
t.setDaemon(true); return t;
t.setName("ThumbOverviewTab UpdateService");
return t;
}
}); });
setExecutor(executor); setExecutor(executor);
} }
@ -51,12 +47,12 @@ public class ChaturbateUpdateService extends PaginatedScheduledService {
if(loginRequired && !chaturbate.credentialsAvailable()) { if(loginRequired && !chaturbate.credentialsAvailable()) {
return Collections.emptyList(); return Collections.emptyList();
} else { } else {
String url = ChaturbateUpdateService.this.url + "?page="+page+"&keywords=&_=" + System.currentTimeMillis(); String pageUrl = ChaturbateUpdateService.this.url + "?page="+page+"&keywords=&_=" + System.currentTimeMillis();
LOG.debug("Fetching page {}", url); LOG.debug("Fetching page {}", pageUrl);
if(loginRequired) { if(loginRequired) {
SiteUiFactory.getUi(chaturbate).login(); 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); Response response = chaturbate.getHttpClient().execute(request);
if (response.isSuccessful()) { if (response.isSuccessful()) {
List<Model> models = ChaturbateModelParser.parseModels(chaturbate, response.body().string()); 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.GlobalThreadPool;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.sites.fc2live.Fc2Live; import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.fc2live.Fc2Model; import ctbrec.sites.fc2live.Fc2Model;
@ -16,7 +17,7 @@ import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabProvider; import ctbrec.ui.tabs.TabProvider;
public class Fc2LiveSiteUi extends AbstractSiteUi { 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 Fc2Live fc2live;
private Fc2TabProvider tabProvider; private Fc2TabProvider tabProvider;
private Fc2LiveConfigUI configUi; private Fc2LiveConfigUI configUi;
@ -44,10 +45,10 @@ public class Fc2LiveSiteUi extends AbstractSiteUi {
@Override @Override
public boolean play(Model model) { public boolean play(Model model) {
new Thread(() -> { GlobalThreadPool.submit(() -> {
Fc2Model m; Fc2Model m;
if(model instanceof JavaFxModel) { if (model instanceof JavaFxModel) {
m = (Fc2Model) ((JavaFxModel)model).getDelegate(); m = (Fc2Model) ((JavaFxModel) model).getDelegate();
} else { } else {
m = (Fc2Model) model; m = (Fc2Model) model;
} }
@ -55,12 +56,20 @@ public class Fc2LiveSiteUi extends AbstractSiteUi {
m.openWebsocket(); m.openWebsocket();
LOG.debug("Starting player for {}", model); LOG.debug("Starting player for {}", model);
Player.play(model, false); Player.play(model, false);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
handleException(e);
} catch (IOException e) {
handleException(e);
} finally {
m.closeWebsocket(); m.closeWebsocket();
} catch (InterruptedException | IOException e) {
LOG.error("Error playing the stream", e);
Dialogs.showError("Player", "Error playing the stream", e);
} }
}).start(); });
return true; 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; package ctbrec.ui.sites.jasmin;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -17,7 +15,7 @@ import ctbrec.ui.tabs.TabProvider;
public class LiveJasminSiteUi extends AbstractSiteUi { 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 LiveJasmin liveJasmin;
private LiveJasminTabProvider tabProvider; private LiveJasminTabProvider tabProvider;
private LiveJasminConfigUi configUi; private LiveJasminConfigUi configUi;
@ -44,43 +42,27 @@ public class LiveJasminSiteUi extends AbstractSiteUi {
// renew login every 30 min // renew login every 30 min
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
boolean renew = false; boolean renew = false;
if((now - lastLoginTime) > TimeUnit.MINUTES.toMillis(30)) { if ((now - lastLoginTime) > TimeUnit.MINUTES.toMillis(30)) {
renew = true; renew = true;
} }
boolean automaticLogin = liveJasmin.login(); boolean automaticLogin = liveJasmin.login();
if(automaticLogin && !renew) { if (automaticLogin && !renew) {
return true; return true;
} else { } else {
lastLoginTime = System.currentTimeMillis(); 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 { try {
queue.take(); new LiveJasminElectronLoginDialog(liveJasmin.getHttpClient().getCookieJar());
} catch (InterruptedException e) { } catch (Exception e1) {
LOG.error("Error while waiting for login dialog to close", e); LOG.error("Error logging in with external browser", e1);
throw new IOException(e); 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(); boolean loggedIn = httpClient.checkLoginSuccess();
if(loggedIn) { if (loggedIn) {
LOG.info("Logged in"); LOG.info("Logged in");
} else { } else {
LOG.info("Login failed"); LOG.info("Login failed");

View File

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

View File

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

View File

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

View File

@ -1,8 +1,6 @@
package ctbrec.ui.sites.stripchat; package ctbrec.ui.sites.stripchat;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -13,7 +11,6 @@ import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.sites.AbstractSiteUi; import ctbrec.ui.sites.AbstractSiteUi;
import ctbrec.ui.sites.ConfigUI; import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabProvider; import ctbrec.ui.tabs.TabProvider;
import javafx.application.Platform;
public class StripchatSiteUi extends AbstractSiteUi { public class StripchatSiteUi extends AbstractSiteUi {
@ -45,31 +42,12 @@ public class StripchatSiteUi extends AbstractSiteUi {
if (automaticLogin) { if (automaticLogin) {
return true; return true;
} else { } else {
// login with external browser
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);
try { try {
queue.take(); new StripchatElectronLoginDialog(site.getHttpClient().getCookieJar());
} catch (InterruptedException e) { } catch (Exception e1) {
LOG.error("Error while waiting for login dialog to close", e); LOG.error("Error logging in with external browser", e1);
throw new IOException(e); Dialogs.showError("Login error", "Couldn't login to " + site.getName(), e1);
} }
StripchatHttpClient httpClient = (StripchatHttpClient) site.getHttpClient(); 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.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import ctbrec.ui.ShutdownListener;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.geometry.Side; import javafx.geometry.Side;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import javafx.scene.control.TabPane; import javafx.scene.control.TabPane;
public class RecordedTab extends Tab implements TabSelectionListener { public class RecordedTab extends Tab implements TabSelectionListener, ShutdownListener {
private TabPane tabPane; private TabPane tabPane;
private RecordedModelsTab recordedModelsTab; private RecordedModelsTab recordedModelsTab;
@ -54,7 +55,8 @@ public class RecordedTab extends Tab implements TabSelectionListener {
} }
} }
public void saveState() { @Override
public void onShutdown() {
recordedModelsTab.saveState(); recordedModelsTab.saveState();
recordLaterTab.saveState(); recordLaterTab.saveState();
} }

View File

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

View File

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

View File

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

View File

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

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 static ctbrec.Recording.State.*;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
@ -14,6 +15,9 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.event.EventBusHolder; import ctbrec.event.EventBusHolder;
import ctbrec.event.RecordingStateChangedEvent; import ctbrec.event.RecordingStateChangedEvent;
import ctbrec.io.IoUtils; import ctbrec.io.IoUtils;
@ -21,6 +25,8 @@ import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.VideoLengthDetector; import ctbrec.recorder.download.VideoLengthDetector;
public class Recording implements Serializable, Callable<Recording> { public class Recording implements Serializable, Callable<Recording> {
private static final transient Logger LOG = LoggerFactory.getLogger(Recording.class);
private String id; private String id;
private Model model; private Model model;
private transient Download download; private transient Download download;
@ -104,10 +110,6 @@ public class Recording implements Serializable, Callable<Recording> {
this.progress = progress; this.progress = progress;
} }
// public String getPath() {
// return path;
// }
public void setPath(String path) { public void setPath(String path) {
this.path = path; this.path = path;
} }
@ -246,22 +248,43 @@ public class Recording implements Serializable, Callable<Recording> {
} }
private long getSize() { private long getSize() {
File rec = getAbsoluteFile(); try {
if (rec.isDirectory()) { Set<File> files = getAllRecordingFiles();
return IoUtils.getDirectorySize(rec); long sum = 0;
} else { for (File file : files) {
if (!rec.exists()) { if (file.isDirectory()) {
if (rec.getName().endsWith(".m3u8")) { sum += IoUtils.getDirectorySize(file);
return IoUtils.getDirectorySize(rec.getParentFile());
} else { } 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() { public void refresh() {
sizeInByte = getSize(); sizeInByte = getSize();
} }

View File

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

View File

@ -1,15 +1,7 @@
package ctbrec.io; package ctbrec.io;
import com.squareup.moshi.JsonAdapter; import static java.nio.charset.StandardCharsets.*;
import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.Settings.ProxyType;
import okhttp3.*;
import okhttp3.OkHttpClient.Builder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.*;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
@ -21,11 +13,40 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.*; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit; 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 { public abstract class HttpClient {
private static final Logger LOG = LoggerFactory.getLogger(HttpClient.class); private static final Logger LOG = LoggerFactory.getLogger(HttpClient.class);
@ -275,10 +296,22 @@ public abstract class HttpClient {
} }
public static String bodyToJsonObject(Response response) { 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) { 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.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
@ -216,6 +215,7 @@ public class NextGenLocalRecorder implements Recorder {
break; break;
} }
} }
recording.refresh();
if (recording.getStatus() != State.DELETED) { if (recording.getStatus() != State.DELETED) {
setRecordingStatus(recording, State.FINISHED); setRecordingStatus(recording, State.FINISHED);
recordingManager.saveRecording(recording); recordingManager.saveRecording(recording);
@ -448,58 +448,40 @@ public class NextGenLocalRecorder implements Recorder {
recording = false; recording = false;
if (!immediately) { if (!immediately) {
LOG.debug("Stopping all recording processes"); stopRecordingProcesses();
recorderLock.lock(); awaitDownloadsFinish();
try { shutdownThreadPools();
// 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();
}
// wait for downloads to finish private void awaitDownloadsFinish() {
LOG.info("Waiting for downloads to finish"); LOG.info("Waiting for downloads to finish");
for (int i = 0; i < 60; i++) { for (int i = 0; i < 60; i++) {
if (!recordingProcesses.isEmpty()) { if (!recordingProcesses.isEmpty()) {
try { try {
Thread.sleep(1000); Thread.sleep(1000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
LOG.error("Error while waiting for downloads to finish", e); LOG.error("Error while waiting for downloads to finish", e);
}
} }
} }
}
}
// shutdown threadpools private void shutdownThreadPools() {
try { try {
LOG.info("Shutting down download pool"); LOG.info("Shutting down download pool");
downloadPool.shutdown(); downloadPool.shutdown();
client.shutdown(); client.shutdown();
downloadPool.awaitTermination(1, TimeUnit.MINUTES); downloadPool.awaitTermination(1, TimeUnit.MINUTES);
LOG.info("Shutting down post-processing pool"); LOG.info("Shutting down post-processing pool");
ppPool.shutdown(); ppPool.shutdown();
int minutesToWait = 10; int minutesToWait = 10;
LOG.info("Waiting {} minutes (max) for post-processing to finish", minutesToWait); LOG.info("Waiting {} minutes (max) for post-processing to finish", minutesToWait);
ppPool.awaitTermination(minutesToWait, TimeUnit.MINUTES); ppPool.awaitTermination(minutesToWait, TimeUnit.MINUTES);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
LOG.error("Error while waiting for pools to finish", e); LOG.error("Error while waiting for pools to finish", e);
}
} }
} }
@ -703,7 +685,6 @@ public class NextGenLocalRecorder implements Recorder {
config.save(); config.save();
} else { } else {
LOG.warn("Couldn't change priority for model {}. Not found in list", model.getName()); LOG.warn("Couldn't change priority for model {}. Not found in list", model.getName());
return;
} }
} catch (IOException e) { } catch (IOException e) {
LOG.error("Couldn't save config", e); LOG.error("Couldn't save config", e);

View File

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

View File

@ -1,10 +1,12 @@
package ctbrec.recorder.download.hls; package ctbrec.recorder.download.hls;
import static ctbrec.recorder.download.StreamSource.*; import static ctbrec.recorder.download.StreamSource.*;
import static java.util.concurrent.TimeUnit.*;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -41,6 +43,8 @@ public class HlsdlDownload extends AbstractDownload {
private transient Hlsdl hlsdl; private transient Hlsdl hlsdl;
protected transient Process hlsdlProcess; protected transient Process hlsdlProcess;
protected transient boolean running = true; protected transient boolean running = true;
protected transient Instant lastSizeChange = Instant.now();
protected transient long lastSize = 0;
@Override @Override
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException { 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)) { if (splittingStrategy.splitNecessary(this)) {
stop(); 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) { } catch (ProcessExitedUncleanException e) {
LOG.error("hlsdl exited unclean", e); LOG.error("hlsdl exited unclean", e);
} }

View File

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

View File

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

View File

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

View File

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

View File

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