Merge branch 'dev' into showup
# Conflicts: # common/src/main/java/ctbrec/sites/showup/ShowupHttpClient.java
This commit is contained in:
commit
5f4e17c2d2
59
CHANGELOG.md
59
CHANGELOG.md
|
@ -1,3 +1,62 @@
|
|||
3.10.10
|
||||
========================
|
||||
* Fixed MVLive recordings once again
|
||||
* Fix: "Check URLs" button stays inactive after the first run
|
||||
* Fix: recordings for some Cam4 models still didn't start
|
||||
* Some smaller tweaks here and there
|
||||
|
||||
3.10.9
|
||||
========================
|
||||
* Added more category tabs for CamSoda
|
||||
* Added button to the "Recording" tab to go over all model URLs and check, if
|
||||
the account still exists
|
||||
* Fix: some Cam4 models were not detected as online
|
||||
|
||||
3.10.8
|
||||
========================
|
||||
* Fixed Stripchat recordings. For some models the recording didn't start,
|
||||
even if they were online and publicly visible in the browser
|
||||
* Fixed Bongacams "New" tab. It didn't show new models.
|
||||
* Added setting to switch FFmpeg logging on/off (category Advanced/Devtools)
|
||||
|
||||
3.10.7
|
||||
========================
|
||||
* Fixed streaming of recordings from the server (the file path was duplicated
|
||||
if single file was used)
|
||||
* Fixed credentials related bugs for Streamate and Stripchat.
|
||||
They used the user name from Chaturbate for some requests. Whoopsie!
|
||||
* Renamed settings for Chaturbate's user name and password
|
||||
* Added setting to split recordings by size
|
||||
* Added setting to monitor the clipboard for model URLs and automatically add
|
||||
them to the recorder
|
||||
* Fixed moving of segment recordings on the server (post-processing)
|
||||
* Fixed minimal browser on macOS
|
||||
* Minimal browser config is now stored in ctbrec's config directory
|
||||
|
||||
3.10.6
|
||||
========================
|
||||
* Fixed Cam4 downloads
|
||||
|
||||
3.10.5
|
||||
========================
|
||||
* Fixed MV Live downloads
|
||||
* MFC web socket now uses the TLS URL
|
||||
* Fix: date placeholders with patterns with more than one occurrence are
|
||||
replaced with the value of the first one
|
||||
* Some smaller UI tweaks
|
||||
* adjusted component sizes for small resolutions
|
||||
* recording indicator can now be used to pause / resume the recording
|
||||
* adjusted scroll speed in the thumbnail overviews
|
||||
* added shortcuts for the thumbnail overviews (keys 1-9 and arrow keys)
|
||||
* added "stop" and "pause" to Recordings tab
|
||||
* added "follow" to Recordings tab
|
||||
|
||||
3.10.4
|
||||
========================
|
||||
* Fix: Bongacams login
|
||||
* Fix: Minimal browser would freeze on windows
|
||||
* Update minimal browser to Electron 10.1.5
|
||||
|
||||
3.10.3
|
||||
========================
|
||||
* Fix: Recordings couldn't be found in client server setup, if the client was
|
||||
|
|
|
@ -8,3 +8,5 @@
|
|||
/server-local.sh
|
||||
/browser/
|
||||
/ffmpeg/
|
||||
/client.iml
|
||||
/.idea/
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<parent>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<version>3.10.3</version>
|
||||
<version>3.10.10</version>
|
||||
<relativePath>../master</relativePath>
|
||||
</parent>
|
||||
|
||||
|
@ -81,10 +81,6 @@
|
|||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-media</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-servlet</artifactId>
|
||||
|
|
|
@ -8,12 +8,15 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -67,7 +70,6 @@ import ctbrec.ui.tabs.logging.LoggingTab;
|
|||
import javafx.application.Application;
|
||||
import javafx.application.HostServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
|
@ -92,24 +94,48 @@ public class CamrecApplication extends Application {
|
|||
private Recorder recorder;
|
||||
private OnlineMonitor onlineMonitor;
|
||||
static HostServices hostServices;
|
||||
private BorderPane rootPane = new BorderPane();
|
||||
private HBox statusBar = new HBox();
|
||||
private Label statusLabel = new Label();
|
||||
private TabPane tabPane = new TabPane();
|
||||
private List<Site> sites = new ArrayList<>();
|
||||
private final BorderPane rootPane = new BorderPane();
|
||||
private final HBox statusBar = new HBox();
|
||||
private final Label statusLabel = new Label();
|
||||
private final TabPane tabPane = new TabPane();
|
||||
private final List<Site> sites = new ArrayList<>();
|
||||
public static HttpClient httpClient;
|
||||
public static String title;
|
||||
private Stage primaryStage;
|
||||
private RecordedModelsTab modelsTab;
|
||||
private RecordingsTab recordingsTab;
|
||||
|
||||
private ScheduledExecutorService scheduler;
|
||||
private int activeRecordings = 0;
|
||||
private double bytesPerSecond = 0;
|
||||
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) throws Exception {
|
||||
this.primaryStage = primaryStage;
|
||||
scheduler = Executors.newScheduledThreadPool(1, r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setDaemon(true);
|
||||
t.setName("Scheduler");
|
||||
return t;
|
||||
});
|
||||
|
||||
logEnvironment();
|
||||
initSites();
|
||||
loadConfig();
|
||||
registerAlertSystem();
|
||||
registerActiveRecordingsCounter();
|
||||
registerBandwidthMeterListener();
|
||||
createHttpClient();
|
||||
hostServices = getHostServices();
|
||||
createRecorder();
|
||||
startOnlineMonitor();
|
||||
createGui(primaryStage);
|
||||
checkForUpdates();
|
||||
startHelpServer();
|
||||
registerClipboardListener();
|
||||
}
|
||||
|
||||
private void initSites() {
|
||||
sites.add(new BongaCams());
|
||||
sites.add(new Cam4());
|
||||
sites.add(new Camsoda());
|
||||
|
@ -122,17 +148,13 @@ public class CamrecApplication extends Application {
|
|||
sites.add(new Showup());
|
||||
sites.add(new Streamate());
|
||||
sites.add(new Stripchat());
|
||||
loadConfig();
|
||||
registerAlertSystem();
|
||||
registerActiveRecordingsCounter();
|
||||
registerBandwidthMeterListener();
|
||||
createHttpClient();
|
||||
hostServices = getHostServices();
|
||||
createRecorder();
|
||||
startOnlineMonitor();
|
||||
createGui(primaryStage);
|
||||
checkForUpdates();
|
||||
startHelpServer();
|
||||
}
|
||||
|
||||
private void registerClipboardListener() {
|
||||
if(config.getSettings().monitorClipboard) {
|
||||
ClipboardListener clipboardListener = new ClipboardListener(recorder, sites);
|
||||
scheduler.scheduleAtFixedRate(clipboardListener, 0, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private void startHelpServer() {
|
||||
|
@ -178,11 +200,11 @@ public class CamrecApplication extends Application {
|
|||
|
||||
Scene scene = new Scene(rootPane, windowWidth, windowHeight);
|
||||
primaryStage.setScene(scene);
|
||||
Dialogs.setScene(scene);
|
||||
rootPane.setCenter(tabPane);
|
||||
rootPane.setBottom(statusBar);
|
||||
for (Iterator<Site> iterator = sites.iterator(); iterator.hasNext();) {
|
||||
Site site = iterator.next();
|
||||
if(site.isEnabled()) {
|
||||
for (Site site : sites) {
|
||||
if (site.isEnabled()) {
|
||||
SiteTab siteTab = new SiteTab(site, scene);
|
||||
tabPane.getTabs().add(siteTab);
|
||||
}
|
||||
|
@ -190,7 +212,7 @@ public class CamrecApplication extends Application {
|
|||
|
||||
modelsTab = new RecordedModelsTab("Recording", recorder, sites);
|
||||
tabPane.getTabs().add(modelsTab);
|
||||
recordingsTab = new RecordingsTab("Recordings", recorder, config, sites);
|
||||
recordingsTab = new RecordingsTab("Recordings", recorder, config);
|
||||
tabPane.getTabs().add(recordingsTab);
|
||||
tabPane.getTabs().add(new SettingsTab(sites, recorder));
|
||||
tabPane.getTabs().add(new NewsTab());
|
||||
|
@ -206,16 +228,16 @@ public class CamrecApplication extends Application {
|
|||
}
|
||||
loadStyleSheet(primaryStage, "style.css");
|
||||
primaryStage.getScene().getStylesheets().add("/ctbrec/ui/settings/ColorSettingsPane.css");
|
||||
primaryStage.getScene().getStylesheets().add("/ctbrec/ui/ThumbCell.css");
|
||||
primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/SearchBox.css");
|
||||
primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/Popover.css");
|
||||
primaryStage.getScene().getStylesheets().add("/ctbrec/ui/settings/api/Preferences.css");
|
||||
primaryStage.getScene().getStylesheets().add("/ctbrec/ui/tabs/ThumbCell.css");
|
||||
primaryStage.getScene().widthProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowWidth = newVal.intValue());
|
||||
primaryStage.getScene().heightProperty()
|
||||
.addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowHeight = newVal.intValue());
|
||||
primaryStage.setMaximized(Config.getInstance().getSettings().windowMaximized);
|
||||
primaryStage.maximizedProperty()
|
||||
.addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowMaximized = newVal.booleanValue());
|
||||
.addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowMaximized = newVal);
|
||||
Player.scene = primaryStage.getScene();
|
||||
primaryStage.setX(Config.getInstance().getSettings().windowX);
|
||||
primaryStage.setY(Config.getInstance().getSettings().windowY);
|
||||
|
@ -225,7 +247,7 @@ public class CamrecApplication extends Application {
|
|||
primaryStage.setOnCloseRequest(createShutdownHandler());
|
||||
|
||||
// register changelistener to activate / deactivate tabs, when the user switches between them
|
||||
tabPane.getSelectionModel().selectedItemProperty().addListener((ChangeListener<Tab>) (ov, from, to) -> {
|
||||
tabPane.getSelectionModel().selectedItemProperty().addListener((ov, from, to) -> {
|
||||
if (from instanceof TabSelectionListener) {
|
||||
((TabSelectionListener) from).deselected();
|
||||
}
|
||||
|
@ -274,44 +296,42 @@ public class CamrecApplication extends Application {
|
|||
shutdownInfo.show();
|
||||
|
||||
final boolean immediately = shutdownNow;
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
modelsTab.saveState();
|
||||
recordingsTab.saveState();
|
||||
onlineMonitor.shutdown();
|
||||
recorder.shutdown(immediately);
|
||||
for (Site site : sites) {
|
||||
if(site.isEnabled()) {
|
||||
site.shutdown();
|
||||
}
|
||||
}
|
||||
try {
|
||||
Config.getInstance().save();
|
||||
LOG.info("Shutdown complete. Goodbye!");
|
||||
Platform.runLater(() -> {
|
||||
primaryStage.close();
|
||||
shutdownInfo.close();
|
||||
Platform.exit();
|
||||
// This is needed, because OkHttp?! seems to block the shutdown with its writer threads. They are not daemon threads :(
|
||||
System.exit(0);
|
||||
});
|
||||
} catch (IOException e1) {
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, primaryStage.getScene());
|
||||
alert.setTitle("Error saving settings");
|
||||
alert.setContentText("Couldn't save settings: " + e1.getLocalizedMessage());
|
||||
alert.showAndWait();
|
||||
System.exit(1);
|
||||
});
|
||||
}
|
||||
try {
|
||||
ExternalBrowser.getInstance().close();
|
||||
} catch (IOException e) {
|
||||
// noop
|
||||
new Thread(() -> {
|
||||
modelsTab.saveState();
|
||||
recordingsTab.saveState();
|
||||
onlineMonitor.shutdown();
|
||||
recorder.shutdown(immediately);
|
||||
for (Site site : sites) {
|
||||
if (site.isEnabled()) {
|
||||
site.shutdown();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
try {
|
||||
Config.getInstance().save();
|
||||
LOG.info("Shutdown complete. Goodbye!");
|
||||
Platform.runLater(() -> {
|
||||
primaryStage.close();
|
||||
shutdownInfo.close();
|
||||
Platform.exit();
|
||||
// This is needed, because OkHttp?! seems to block the shutdown with its writer threads. They are not daemon threads :(
|
||||
System.exit(0);
|
||||
});
|
||||
} catch (IOException e1) {
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, primaryStage.getScene());
|
||||
alert.setTitle("Error saving settings");
|
||||
alert.setContentText("Couldn't save settings: " + e1.getLocalizedMessage());
|
||||
alert.showAndWait();
|
||||
System.exit(1);
|
||||
});
|
||||
}
|
||||
try {
|
||||
ExternalBrowser.getInstance().close();
|
||||
} catch (IOException e12) {
|
||||
// noop
|
||||
}
|
||||
scheduler.shutdownNow();
|
||||
}).start();
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -355,8 +375,8 @@ public class CamrecApplication extends Application {
|
|||
if (activeRecordings == 0) {
|
||||
bytesPerSecond = 0;
|
||||
}
|
||||
String humanreadable = ByteUnitFormatter.format(bytesPerSecond);
|
||||
String status = String.format("Recording %s / %s models @ %s/s", activeRecordings, recorder.getModels().size(), humanreadable);
|
||||
String humanReadable = ByteUnitFormatter.format(bytesPerSecond);
|
||||
String status = String.format("Recording %s / %s models @ %s/s", activeRecordings, recorder.getModels().size(), humanReadable);
|
||||
Platform.runLater(() -> statusLabel.setText(status));
|
||||
}
|
||||
|
||||
|
@ -370,7 +390,7 @@ public class CamrecApplication extends Application {
|
|||
" -fx-focus-color: -fx-accent;\n" +
|
||||
" -fx-control-inner-background-alt: derive(-fx-base, 95%);\n" +
|
||||
"}";
|
||||
fos.write(content.getBytes("utf-8"));
|
||||
fos.write(content.getBytes(StandardCharsets.UTF_8));
|
||||
} catch(Exception e) {
|
||||
LOG.error("Couldn't write stylesheet for user defined color theme");
|
||||
}
|
||||
|
@ -400,7 +420,6 @@ public class CamrecApplication extends Application {
|
|||
|
||||
private void createRecorder() {
|
||||
if (config.getSettings().localRecording) {
|
||||
//recorder = new LocalRecorder(config);
|
||||
try {
|
||||
recorder = new NextGenLocalRecorder(config, sites);
|
||||
} catch (IOException e) {
|
||||
|
@ -431,7 +450,7 @@ public class CamrecApplication extends Application {
|
|||
private void createHttpClient() {
|
||||
httpClient = new HttpClient("camrec") {
|
||||
@Override
|
||||
public boolean login() throws IOException {
|
||||
public boolean login() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
@ -481,8 +500,7 @@ public class CamrecApplication extends Application {
|
|||
try (InputStream is = CamrecApplication.class.getClassLoader().getResourceAsStream("version")) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
|
||||
String versionString = reader.readLine();
|
||||
Version version = Version.of(versionString);
|
||||
return version;
|
||||
return Version.of(versionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.sites.Site;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.input.Clipboard;
|
||||
|
||||
public class ClipboardListener implements Runnable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClipboardListener.class);
|
||||
private Recorder recorder;
|
||||
private List<Site> sites;
|
||||
private Clipboard systemClipboard;
|
||||
private String lastUrl = null;
|
||||
|
||||
public ClipboardListener(Recorder recorder, List<Site> sites) {
|
||||
this.recorder = recorder;
|
||||
this.sites = sites;
|
||||
systemClipboard = Clipboard.getSystemClipboard();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
String url = null;
|
||||
if (systemClipboard.hasUrl()) {
|
||||
url = systemClipboard.getUrl();
|
||||
} else if (systemClipboard.hasString()) {
|
||||
url = systemClipboard.getString();
|
||||
}
|
||||
if (!Objects.equals(url, lastUrl)) {
|
||||
lastUrl = url;
|
||||
addModelIfUrlMatches(url);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error in clipboard polling loop", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addModelIfUrlMatches(String url) {
|
||||
for (Site site : sites) {
|
||||
Model m = site.createModelFromUrl(url);
|
||||
if (m != null) {
|
||||
try {
|
||||
recorder.startRecording(m);
|
||||
DesktopIntegration.notification("Add from clipboard", "Model added", "Model " + m.getDisplayName() + " added");
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
|
||||
DesktopIntegration.notification("Add from clipboard", "Error", "Couldn't add URL from clipboard: " + e.getLocalizedMessage());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.OS;
|
||||
import ctbrec.io.StreamRedirectThread;
|
||||
import ctbrec.io.StreamRedirector;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Label;
|
||||
|
@ -133,8 +133,8 @@ public class DesktopIntegration {
|
|||
msg.replace("-", "\\\\-").replace("\\s", "\\\\ "),
|
||||
"--icon=dialog-information"
|
||||
});
|
||||
new Thread(new StreamRedirectThread(p.getInputStream(), System.out)).start(); // NOSONAR
|
||||
new Thread(new StreamRedirectThread(p.getErrorStream(), System.err)).start(); // NOSONAR
|
||||
new Thread(new StreamRedirector(p.getInputStream(), System.out)).start(); // NOSONAR
|
||||
new Thread(new StreamRedirector(p.getErrorStream(), System.err)).start(); // NOSONAR
|
||||
} catch (IOException e1) {
|
||||
LOG.error("Notification failed", e1);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.*;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Consumer;
|
||||
|
@ -17,7 +21,7 @@ import org.slf4j.LoggerFactory;
|
|||
import ctbrec.Config;
|
||||
import ctbrec.OS;
|
||||
import ctbrec.Settings.ProxyType;
|
||||
import ctbrec.io.StreamRedirectThread;
|
||||
import ctbrec.io.StreamRedirector;
|
||||
|
||||
public class ExternalBrowser implements AutoCloseable {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ExternalBrowser.class);
|
||||
|
@ -47,16 +51,17 @@ public class ExternalBrowser implements AutoCloseable {
|
|||
|
||||
addProxyConfig(jsonConfig.getJSONObject("config"));
|
||||
|
||||
File configDir = new File(Config.getInstance().getConfigDir(), "ctbrec-minimal-browser");
|
||||
String[] cmdline = OS.getBrowserCommand(configDir.getCanonicalPath());
|
||||
p = new ProcessBuilder(cmdline).start();
|
||||
if (LOG.isTraceEnabled()) {
|
||||
p = new ProcessBuilder(OS.getBrowserCommand("--enable-logging")).start();
|
||||
new StreamRedirectThread(p.getInputStream(), System.err);
|
||||
new StreamRedirectThread(p.getErrorStream(), System.err);
|
||||
new Thread(new StreamRedirector(p.getInputStream(), System.out)).start();
|
||||
new Thread(new StreamRedirector(p.getErrorStream(), System.err)).start();
|
||||
} else {
|
||||
p = new ProcessBuilder(OS.getBrowserCommand()).start();
|
||||
new StreamRedirectThread(p.getInputStream(), OutputStream.nullOutputStream());
|
||||
new StreamRedirectThread(p.getErrorStream(), OutputStream.nullOutputStream());
|
||||
new Thread(new StreamRedirector(p.getInputStream(), OutputStream.nullOutputStream())).start();
|
||||
new Thread(new StreamRedirector(p.getErrorStream(), OutputStream.nullOutputStream())).start();
|
||||
}
|
||||
LOG.debug("Browser started");
|
||||
LOG.debug("Browser started: {}", Arrays.toString(cmdline));
|
||||
|
||||
connectToRemoteControlSocket();
|
||||
while (!browserReady) {
|
||||
|
@ -69,7 +74,7 @@ public class ExternalBrowser implements AutoCloseable {
|
|||
} else {
|
||||
LOG.debug("Connected to remote control server. Sending config");
|
||||
}
|
||||
out.write(jsonConfig.toString().getBytes("utf-8"));
|
||||
out.write(jsonConfig.toString().getBytes(UTF_8));
|
||||
out.write('\n');
|
||||
out.flush();
|
||||
|
||||
|
@ -95,6 +100,7 @@ public class ExternalBrowser implements AutoCloseable {
|
|||
out = socket.getOutputStream();
|
||||
reader = new Thread(this::readBrowserOutput);
|
||||
reader.start();
|
||||
LOG.debug("Connected to control socket");
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
if(i == 19) {
|
||||
|
@ -116,7 +122,7 @@ public class ExternalBrowser implements AutoCloseable {
|
|||
//LOG.debug("Executing JS {}", javaScript);
|
||||
JSONObject script = new JSONObject();
|
||||
script.put("execute", javaScript);
|
||||
out.write(script.toString().getBytes("utf-8"));
|
||||
out.write(script.toString().getBytes(UTF_8));
|
||||
out.write('\n');
|
||||
out.flush();
|
||||
if(javaScript.equals("quit")) {
|
||||
|
|
|
@ -307,4 +307,9 @@ public class JavaFxModel implements Model {
|
|||
public void setRecordUntilSubsequentAction(SubsequentAction action) {
|
||||
delegate.setRecordUntilSubsequentAction(action);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() throws IOException {
|
||||
return delegate.exists();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Polygon;
|
||||
|
||||
public class PauseIcon extends Polygon {
|
||||
|
||||
public PauseIcon(Color color, int size) {
|
||||
super(
|
||||
0, size,
|
||||
0, 0,
|
||||
(size * 2.0 / 5.0), 0,
|
||||
(size * 2.0 / 5.0), size,
|
||||
(size * 3.0 / 5.0), size,
|
||||
(size * 3.0 / 5.0), 0,
|
||||
size, 0,
|
||||
size, size
|
||||
);
|
||||
setFill(color);
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
|
||||
public class PauseIndicator extends HBox {
|
||||
|
||||
public PauseIndicator(Color c, int size) {
|
||||
spacingProperty().setValue(size*1/5);
|
||||
Rectangle left = new Rectangle(size*2/5, size);
|
||||
left.setFill(c);
|
||||
Rectangle right = new Rectangle(size*2/5, size);
|
||||
right.setFill(c);
|
||||
getChildren().add(left);
|
||||
getChildren().add(right);
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ import ctbrec.Config;
|
|||
import ctbrec.Model;
|
||||
import ctbrec.OS;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.io.StreamRedirectThread;
|
||||
import ctbrec.io.StreamRedirector;
|
||||
import ctbrec.io.UrlUtil;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
|
@ -165,12 +165,12 @@ public class Player {
|
|||
|
||||
// create threads, which read stdout and stderr of the player process. these are needed,
|
||||
// because otherwise the internal buffer for these streams fill up and block the process
|
||||
Thread std = new Thread(new StreamRedirectThread(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.setDaemon(true);
|
||||
std.start();
|
||||
Thread err = new Thread(new StreamRedirectThread(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.setDaemon(true);
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package ctbrec.ui.action;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Button;
|
||||
|
||||
public class CheckModelAccountAction {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CheckModelAccountAction.class);
|
||||
|
||||
private Button b;
|
||||
|
||||
private Recorder recorder;
|
||||
|
||||
public CheckModelAccountAction(Button b, Recorder recorder) {
|
||||
this.b = b;
|
||||
this.recorder = recorder;
|
||||
|
||||
}
|
||||
|
||||
public void execute() {
|
||||
String buttonText = b.getText();
|
||||
b.setDisable(true);
|
||||
Runnable checker = (() -> {
|
||||
List<Model> deletedAccounts = new ArrayList<>();
|
||||
try {
|
||||
List<Model> models = recorder.getModels();
|
||||
int total = models.size();
|
||||
for (int i = 0; i < total; i++) {
|
||||
final int counter = i+1;
|
||||
Platform.runLater(() -> b.setText(buttonText + ' ' + counter + '/' + total));
|
||||
Model modelToCheck = models.get(i);
|
||||
try {
|
||||
if (!modelToCheck.exists()) {
|
||||
deletedAccounts.add(modelToCheck);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Couldn't check, if model account still exists", e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Platform.runLater(() -> {
|
||||
b.setDisable(false);
|
||||
b.setText(buttonText);
|
||||
if (!deletedAccounts.isEmpty()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Model deletedModel : deletedAccounts) {
|
||||
String name = deletedModel.getDisplayName() + " ".repeat(30);
|
||||
name = name.substring(0, 30);
|
||||
sb.append(name).append(' ').append('(').append(deletedModel.getUrl()).append(')').append('\n');
|
||||
}
|
||||
boolean remove = Dialogs.showConfirmDialog("Deleted Accounts", sb.toString(),
|
||||
"The following accounts seem to have been deleted. Do you want to remove them?", b.getScene());
|
||||
if (remove) {
|
||||
new StopRecordingAction(b, deletedAccounts, recorder).execute();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
new Thread(checker).start();
|
||||
}
|
||||
}
|
|
@ -24,9 +24,15 @@ import javafx.stage.Stage;
|
|||
public class Dialogs {
|
||||
|
||||
private Dialogs() {}
|
||||
// TODO reduce calls to this method and use Dialogs.showError(Scene parent, String header, String text, Throwable t) instead
|
||||
|
||||
private static Scene scene;
|
||||
|
||||
public static void setScene(Scene scene) {
|
||||
Dialogs.scene = scene;
|
||||
}
|
||||
|
||||
public static void showError(String header, String text, Throwable t) {
|
||||
showError(null, header, text, t);
|
||||
showError(scene, header, text, t);
|
||||
}
|
||||
|
||||
public static void showError(Scene parent, String header, String text, Throwable t) {
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package ctbrec.ui.controls;
|
||||
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.skin.ScrollPaneSkin;
|
||||
import javafx.scene.input.ScrollEvent;
|
||||
|
||||
public class FasterVerticalScrollPaneSkin extends ScrollPaneSkin {
|
||||
|
||||
public FasterVerticalScrollPaneSkin(final ScrollPane scrollPane) {
|
||||
super(scrollPane);
|
||||
|
||||
getSkinnable().addEventFilter(ScrollEvent.SCROLL, event -> {
|
||||
double ratio = scrollPane.getViewportBounds().getHeight() / scrollPane.getContent().getBoundsInLocal().getHeight();
|
||||
double baseUnitIncrement = 0.15;
|
||||
double unitIncrement = baseUnitIncrement * ratio * 1.25;
|
||||
getVerticalScrollBar().setUnitIncrement(unitIncrement);
|
||||
|
||||
if (event.getDeltaX() < 0) {
|
||||
getHorizontalScrollBar().increment();
|
||||
} else if (event.getDeltaX() > 0) {
|
||||
getHorizontalScrollBar().decrement();
|
||||
}
|
||||
if (event.getDeltaY() < 0) {
|
||||
getVerticalScrollBar().increment();
|
||||
} else if (event.getDeltaY() > 0) {
|
||||
getVerticalScrollBar().decrement();
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package ctbrec.ui.controls;
|
||||
|
||||
import ctbrec.ui.PauseIcon;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
|
||||
public class PausedIndicator extends StackPane {
|
||||
|
||||
private PauseIcon pausedIcon;
|
||||
private Rectangle clickPanel;
|
||||
|
||||
public PausedIndicator(int size, Color color) {
|
||||
setMaxSize(size, size);
|
||||
|
||||
pausedIcon = new PauseIcon(color, size);
|
||||
pausedIcon.setVisible(false);
|
||||
clickPanel = new Rectangle(size, size);
|
||||
clickPanel.setCursor(Cursor.HAND);
|
||||
clickPanel.setFill(Paint.valueOf("#00000000"));
|
||||
getChildren().add(pausedIcon);
|
||||
getChildren().add(clickPanel);
|
||||
|
||||
pausedIcon.visibleProperty().bindBidirectional(visibleProperty());
|
||||
clickPanel.onMouseClickedProperty().bindBidirectional(onMouseClickedProperty());
|
||||
|
||||
Tooltip tooltip = new Tooltip("Resume Recording");
|
||||
Tooltip.install(clickPanel, tooltip);
|
||||
}
|
||||
}
|
||||
|
|
@ -221,6 +221,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
|
|||
this.model = null;
|
||||
} else {
|
||||
follow.setVisible(model.getSite().supportsFollow());
|
||||
follow.setDisable(!model.getSite().credentialsAvailable());
|
||||
title.setVisible(true);
|
||||
title.setText(model.getDisplayName());
|
||||
this.model = model;
|
||||
|
|
|
@ -1,15 +1,7 @@
|
|||
package ctbrec.ui.news;
|
||||
|
||||
import static ctbrec.io.HttpConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.ui.CamrecApplication;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
|
@ -22,10 +14,15 @@ import javafx.scene.control.Tab;
|
|||
import javafx.scene.layout.VBox;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
||||
import static ctbrec.io.HttpConstants.USER_AGENT;
|
||||
|
||||
public class NewsTab extends Tab implements TabSelectionListener {
|
||||
private static final String ACCESS_TOKEN = "a2804d73a89951a22e0f8483a6fcec8943afd88b7ba17c459c095aa9e6f94fd0";
|
||||
//private static final String URL = "https://mastodon.cloud/api/v1/timelines/home?limit=50";
|
||||
private static final String URL = "https://mastodon.cloud/api/v1/accounts/480960/statuses?limit=20&exclude_replies=true";
|
||||
|
||||
private VBox layout = new VBox();
|
||||
|
@ -64,7 +61,7 @@ public class NewsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Dialogs.showError("News", "Couldn't load news from mastodon", e);
|
||||
Dialogs.showError(getTabPane().getScene(), "News", "Couldn't load news from mastodon", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import javafx.beans.property.ListProperty;
|
|||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.CheckBox;
|
||||
|
@ -47,12 +48,17 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
|
|||
|
||||
private Config config;
|
||||
private Settings settings;
|
||||
private Preferences prefs;
|
||||
|
||||
public CtbrecPreferencesStorage(Config config) {
|
||||
this.config = config;
|
||||
this.settings = config.getSettings();
|
||||
}
|
||||
|
||||
public void setPreferences(Preferences prefs) {
|
||||
this.prefs = prefs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Preferences preferences) throws IOException {
|
||||
throw new RuntimeException("not implemented");
|
||||
|
@ -102,6 +108,9 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
|
|||
prop.addListener((obs, oldV, newV) -> saveValue(() -> {
|
||||
Field field = Settings.class.getField(setting.getKey());
|
||||
field.set(settings, newV);
|
||||
if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) {
|
||||
prefs.getRestartRequiredCallback().run();
|
||||
}
|
||||
config.save();
|
||||
}));
|
||||
HBox row = new HBox();
|
||||
|
@ -142,7 +151,7 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
|
|||
private int getRangeSliderValue(List<Integer> values, List<Integer> labels, int value) {
|
||||
for (int i = 0; i < labels.size(); i++) {
|
||||
int label = labels.get(i).intValue();
|
||||
if(label == value) {
|
||||
if (label == value) {
|
||||
return values.get(i);
|
||||
}
|
||||
}
|
||||
|
@ -157,6 +166,9 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
|
|||
String oldValue = (String) field.get(settings);
|
||||
if (!Objects.equals(path, oldValue)) {
|
||||
field.set(settings, path);
|
||||
if (setting.doesNeedRestart()) {
|
||||
prefs.getRestartRequiredCallback().run();
|
||||
}
|
||||
config.save();
|
||||
}
|
||||
}));
|
||||
|
@ -174,6 +186,9 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
|
|||
String oldValue = (String) field.get(settings);
|
||||
if (!Objects.equals(path, oldValue)) {
|
||||
field.set(settings, path);
|
||||
if (setting.doesNeedRestart()) {
|
||||
prefs.getRestartRequiredCallback().run();
|
||||
}
|
||||
config.save();
|
||||
}
|
||||
}));
|
||||
|
@ -187,6 +202,9 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
|
|||
ctrl.textProperty().addListener((obs, oldV, newV) -> saveValue(() -> {
|
||||
Field field = Settings.class.getField(setting.getKey());
|
||||
field.set(settings, newV);
|
||||
if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) {
|
||||
prefs.getRestartRequiredCallback().run();
|
||||
}
|
||||
config.save();
|
||||
}));
|
||||
StringProperty prop = (StringProperty) setting.getProperty();
|
||||
|
@ -204,6 +222,9 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
|
|||
if (!ctrl.getText().isEmpty()) {
|
||||
Field field = Settings.class.getField(setting.getKey());
|
||||
field.set(settings, Integer.parseInt(ctrl.getText()));
|
||||
if (setting.doesNeedRestart() && !Objects.equals(oldV, newV) && prefs != null) {
|
||||
prefs.getRestartRequiredCallback().run();
|
||||
}
|
||||
config.save();
|
||||
}
|
||||
}));
|
||||
|
@ -226,6 +247,9 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
|
|||
}
|
||||
Field field = Settings.class.getField(setting.getKey());
|
||||
field.set(settings, value);
|
||||
if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) {
|
||||
prefs.getRestartRequiredCallback().run();
|
||||
}
|
||||
config.save();
|
||||
}
|
||||
}));
|
||||
|
@ -239,6 +263,9 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
|
|||
ctrl.selectedProperty().addListener((obs, oldV, newV) -> saveValue(() -> {
|
||||
Field field = Settings.class.getField(setting.getKey());
|
||||
field.set(settings, newV);
|
||||
if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) {
|
||||
prefs.getRestartRequiredCallback().run();
|
||||
}
|
||||
config.save();
|
||||
}));
|
||||
BooleanProperty prop = (BooleanProperty) setting.getProperty();
|
||||
|
@ -265,8 +292,14 @@ public class CtbrecPreferencesStorage implements PreferencesStorage {
|
|||
} else {
|
||||
field.set(settings, newV);
|
||||
}
|
||||
if (setting.doesNeedRestart() && !Objects.equals(oldV, newV)) {
|
||||
prefs.getRestartRequiredCallback().run();
|
||||
}
|
||||
config.save();
|
||||
}));
|
||||
if (setting.getChangeListener() != null) {
|
||||
comboBox.valueProperty().addListener((ChangeListener<? super Object>) setting.getChangeListener());
|
||||
}
|
||||
return comboBox;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,18 @@
|
|||
package ctbrec.ui.settings;
|
||||
|
||||
import ctbrec.recorder.postprocessing.*;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import ctbrec.ui.settings.api.Preferences;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.recorder.postprocessing.CreateContactSheet;
|
||||
import ctbrec.recorder.postprocessing.DeleteTooShort;
|
||||
import ctbrec.recorder.postprocessing.Move;
|
||||
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||
import ctbrec.recorder.postprocessing.Remux;
|
||||
import ctbrec.recorder.postprocessing.Rename;
|
||||
import ctbrec.recorder.postprocessing.Script;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import ctbrec.ui.settings.api.Preferences;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
public class PostProcessingDialogFactory {
|
||||
|
||||
static Map<Class<?>, Class<?>> ppToDialogMap = new HashMap<>();
|
||||
|
@ -35,19 +28,19 @@ public class PostProcessingDialogFactory {
|
|||
private PostProcessingDialogFactory() {
|
||||
}
|
||||
|
||||
public static void openNewDialog(PostProcessor pp, Config config, Scene scene, ObservableList<PostProcessor> stepList) {
|
||||
openDialog(pp, config, scene, stepList, true);
|
||||
public static void openNewDialog(PostProcessor pp, Scene scene, ObservableList<PostProcessor> stepList) {
|
||||
openDialog(pp, scene, stepList, true);
|
||||
}
|
||||
|
||||
public static void openEditDialog(PostProcessor pp, Config config, Scene scene, ObservableList<PostProcessor> stepList) {
|
||||
openDialog(pp, config, scene, stepList, false);
|
||||
public static void openEditDialog(PostProcessor pp, Scene scene, ObservableList<PostProcessor> stepList) {
|
||||
openDialog(pp, scene, stepList, false);
|
||||
}
|
||||
|
||||
private static void openDialog(PostProcessor pp, Config config, Scene scene, ObservableList<PostProcessor> stepList, boolean newEntry) {
|
||||
private static void openDialog(PostProcessor pp, Scene scene, ObservableList<PostProcessor> stepList, boolean newEntry) {
|
||||
boolean ok;
|
||||
try {
|
||||
Optional<Preferences> preferences = createPreferences(pp);
|
||||
if(preferences.isPresent()) {
|
||||
if (preferences.isPresent()) {
|
||||
Region view = preferences.get().getView(false);
|
||||
view.setMinWidth(600);
|
||||
ok = Dialogs.showCustomInput(scene, "Configure " + pp.getName(), view);
|
||||
|
@ -62,12 +55,12 @@ public class PostProcessingDialogFactory {
|
|||
}
|
||||
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
|
||||
| InstantiationException | IOException e) {
|
||||
Dialogs.showError("New post-processing step", "Couldn't create dialog for " + pp.getName(), e);
|
||||
Dialogs.showError(scene, "New post-processing step", "Couldn't create dialog for " + pp.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<Preferences> createPreferences(PostProcessor pp) throws InstantiationException, IllegalAccessException, IllegalArgumentException,
|
||||
InvocationTargetException, NoSuchMethodException, SecurityException {
|
||||
private static Optional<Preferences> createPreferences(PostProcessor pp) throws InstantiationException, IllegalAccessException,
|
||||
InvocationTargetException, NoSuchMethodException {
|
||||
Class<?> paneFactoryClass = ppToDialogMap.get(pp.getClass());
|
||||
if (paneFactoryClass != null) {
|
||||
AbstractPostProcessingPaneFactory factory = (AbstractPostProcessingPaneFactory) paneFactoryClass.getDeclaredConstructor().newInstance();
|
||||
|
|
|
@ -1,21 +1,7 @@
|
|||
package ctbrec.ui.settings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Optional;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.recorder.postprocessing.Copy;
|
||||
import ctbrec.recorder.postprocessing.CreateContactSheet;
|
||||
import ctbrec.recorder.postprocessing.DeleteOriginal;
|
||||
import ctbrec.recorder.postprocessing.DeleteTooShort;
|
||||
import ctbrec.recorder.postprocessing.Move;
|
||||
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||
import ctbrec.recorder.postprocessing.RemoveKeepFile;
|
||||
import ctbrec.recorder.postprocessing.Remux;
|
||||
import ctbrec.recorder.postprocessing.Rename;
|
||||
import ctbrec.recorder.postprocessing.Script;
|
||||
import ctbrec.recorder.postprocessing.*;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
|
@ -30,21 +16,25 @@ import javafx.scene.layout.Priority;
|
|||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Optional;
|
||||
|
||||
public class PostProcessingStepPanel extends GridPane {
|
||||
|
||||
private Config config;
|
||||
private final Config config;
|
||||
|
||||
|
||||
private static final Class<?>[] POST_PROCESSOR_CLASSES = new Class<?>[] { // @formatter: off
|
||||
Copy.class,
|
||||
Rename.class,
|
||||
Move.class,
|
||||
Remux.class,
|
||||
Script.class,
|
||||
DeleteOriginal.class,
|
||||
DeleteTooShort.class,
|
||||
RemoveKeepFile.class,
|
||||
CreateContactSheet.class
|
||||
private static final Class<?>[] POST_PROCESSOR_CLASSES = new Class<?>[]{ // @formatter: off
|
||||
Copy.class,
|
||||
Rename.class,
|
||||
Move.class,
|
||||
Remux.class,
|
||||
Script.class,
|
||||
DeleteOriginal.class,
|
||||
DeleteTooShort.class,
|
||||
RemoveKeepFile.class,
|
||||
CreateContactSheet.class
|
||||
}; // @formatter: on
|
||||
|
||||
ListView<PostProcessor> stepListView;
|
||||
|
@ -98,33 +88,33 @@ public class PostProcessingStepPanel extends GridPane {
|
|||
}
|
||||
|
||||
private Button createUpButton() {
|
||||
Button up = createButton("\u25B4", "Move step up");
|
||||
up.setOnAction(evt -> {
|
||||
Button button = createButton("\u25B4", "Move step up");
|
||||
button.setOnAction(evt -> {
|
||||
int idx = stepListView.getSelectionModel().getSelectedIndex();
|
||||
PostProcessor selectedItem = stepListView.getSelectionModel().getSelectedItem();
|
||||
stepList.remove(idx);
|
||||
stepList.add(idx - 1, selectedItem);
|
||||
stepListView.getSelectionModel().select(idx - 1);
|
||||
});
|
||||
return up;
|
||||
return button;
|
||||
}
|
||||
|
||||
private Button createDownButton() {
|
||||
Button down = createButton("\u25BE", "Move step down");
|
||||
down.setOnAction(evt -> {
|
||||
Button button = createButton("\u25BE", "Move step down");
|
||||
button.setOnAction(evt -> {
|
||||
int idx = stepListView.getSelectionModel().getSelectedIndex();
|
||||
PostProcessor selectedItem = stepListView.getSelectionModel().getSelectedItem();
|
||||
stepList.remove(idx);
|
||||
stepList.add(idx + 1, selectedItem);
|
||||
stepListView.getSelectionModel().select(idx + 1);
|
||||
});
|
||||
return down;
|
||||
return button;
|
||||
}
|
||||
|
||||
private Button createAddButton() {
|
||||
Button add = createButton("+", "Add a new step");
|
||||
add.setDisable(false);
|
||||
add.setOnAction(evt -> {
|
||||
Button button = createButton("+", "Add a new step");
|
||||
button.setDisable(false);
|
||||
button.setOnAction(evt -> {
|
||||
PostProcessor[] options = createOptions();
|
||||
ChoiceDialog<PostProcessor> choice = new ChoiceDialog<>(options[0], options);
|
||||
choice.setTitle("New Post-Processing Step");
|
||||
|
@ -138,17 +128,17 @@ public class PostProcessingStepPanel extends GridPane {
|
|||
stage.getIcons().add(new Image(icon));
|
||||
|
||||
Optional<PostProcessor> result = choice.showAndWait();
|
||||
result.ifPresent(pp -> PostProcessingDialogFactory.openNewDialog(pp, config, getScene(), stepList));
|
||||
result.ifPresent(pp -> PostProcessingDialogFactory.openNewDialog(pp, getScene(), stepList));
|
||||
saveConfig();
|
||||
});
|
||||
return add;
|
||||
return button;
|
||||
}
|
||||
|
||||
private void saveConfig() {
|
||||
try {
|
||||
config.save();
|
||||
} catch (IOException e) {
|
||||
Dialogs.showError("Post-Processing", "Couldn't save post-processing step", e);
|
||||
Dialogs.showError(getScene(), "Post-Processing", "Couldn't save post-processing step", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,25 +160,25 @@ public class PostProcessingStepPanel extends GridPane {
|
|||
}
|
||||
|
||||
private Button createRemoveButton() {
|
||||
Button remove = createButton("-", "Remove selected step");
|
||||
remove.setOnAction(evt -> {
|
||||
Button button = createButton("-", "Remove selected step");
|
||||
button.setOnAction(evt -> {
|
||||
PostProcessor selectedItem = stepListView.getSelectionModel().getSelectedItem();
|
||||
if (selectedItem != null) {
|
||||
stepList.remove(selectedItem);
|
||||
}
|
||||
});
|
||||
return remove;
|
||||
return button;
|
||||
}
|
||||
|
||||
private Button createEditButton() {
|
||||
Button edit = createButton("\u270E", "Edit selected step");
|
||||
edit.setOnAction(evt -> {
|
||||
Button button = createButton("\u270E", "Edit selected step");
|
||||
button.setOnAction(evt -> {
|
||||
PostProcessor selectedItem = stepListView.getSelectionModel().getSelectedItem();
|
||||
PostProcessingDialogFactory.openEditDialog(selectedItem, config, getScene(), stepList);
|
||||
PostProcessingDialogFactory.openEditDialog(selectedItem, getScene(), stepList);
|
||||
stepListView.refresh();
|
||||
saveConfig();
|
||||
});
|
||||
return edit;
|
||||
return button;
|
||||
}
|
||||
|
||||
private Button createButton(String text, String tooltip) {
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
package ctbrec.ui.settings;
|
||||
import static ctbrec.Settings.ProxyType.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Settings.ProxyType;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.TitledPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
|
||||
public class ProxySettingsPane extends TitledPane implements EventHandler<ActionEvent> {
|
||||
|
||||
private ComboBox<ProxyType> proxyType;
|
||||
private TextField proxyHost = new TextField();
|
||||
private TextField proxyPort = new TextField();
|
||||
private TextField proxyUser = new TextField();
|
||||
private PasswordField proxyPassword = new PasswordField();
|
||||
private SettingsTab settingsTab;
|
||||
|
||||
public ProxySettingsPane(SettingsTab settingsTab) {
|
||||
this.settingsTab = settingsTab;
|
||||
createGui();
|
||||
loadConfig();
|
||||
}
|
||||
|
||||
private void createGui() {
|
||||
setText("Proxy");
|
||||
setCollapsible(false);
|
||||
GridPane layout = SettingsTab.createGridLayout();
|
||||
setContent(layout);
|
||||
|
||||
Label l = new Label("Type");
|
||||
layout.add(l, 0, 0);
|
||||
List<ProxyType> proxyTypes = new ArrayList<>();
|
||||
proxyTypes.add(DIRECT);
|
||||
proxyTypes.add(HTTP);
|
||||
proxyTypes.add(SOCKS4);
|
||||
proxyTypes.add(SOCKS5);
|
||||
proxyType = new ComboBox<>(FXCollections.observableList(proxyTypes));
|
||||
proxyType.setOnAction(this);
|
||||
layout.add(proxyType, 1, 0);
|
||||
|
||||
l = new Label("Host");
|
||||
layout.add(l, 0, 1);
|
||||
layout.add(proxyHost, 1, 1);
|
||||
proxyHost.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig());
|
||||
|
||||
l = new Label("Port");
|
||||
layout.add(l, 0, 2);
|
||||
layout.add(proxyPort, 1, 2);
|
||||
proxyPort.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig());
|
||||
|
||||
l = new Label("Username");
|
||||
layout.add(l, 0, 3);
|
||||
layout.add(proxyUser, 1, 3);
|
||||
proxyUser.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig());
|
||||
|
||||
|
||||
l = new Label("Password");
|
||||
layout.add(l, 0, 4);
|
||||
layout.add(proxyPassword, 1, 4);
|
||||
proxyPassword.textProperty().addListener((ob, o, n) -> settingsTab.saveConfig());
|
||||
}
|
||||
|
||||
private void loadConfig() {
|
||||
proxyType.valueProperty().set(Config.getInstance().getSettings().proxyType);
|
||||
proxyHost.setText(Config.getInstance().getSettings().proxyHost);
|
||||
proxyPort.setText(Config.getInstance().getSettings().proxyPort);
|
||||
proxyUser.setText(Config.getInstance().getSettings().proxyUser);
|
||||
proxyPassword.setText(Config.getInstance().getSettings().proxyPassword);
|
||||
setComponentDisableState();
|
||||
}
|
||||
|
||||
void saveConfig() {
|
||||
Config.getInstance().getSettings().proxyType = proxyType.getValue();
|
||||
Config.getInstance().getSettings().proxyHost = proxyHost.getText();
|
||||
Config.getInstance().getSettings().proxyPort = proxyPort.getText();
|
||||
Config.getInstance().getSettings().proxyUser = proxyUser.getText();
|
||||
Config.getInstance().getSettings().proxyPassword = proxyPassword.getText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(ActionEvent event) {
|
||||
setComponentDisableState();
|
||||
settingsTab.showRestartRequired();
|
||||
settingsTab.saveConfig();
|
||||
}
|
||||
|
||||
private void setComponentDisableState() {
|
||||
if(proxyType.getValue() == DIRECT) {
|
||||
proxyHost.setDisable(true);
|
||||
proxyPort.setDisable(true);
|
||||
proxyUser.setDisable(true);
|
||||
proxyPassword.setDisable(true);
|
||||
} else {
|
||||
proxyHost.setDisable(false);
|
||||
proxyPort.setDisable(false);
|
||||
proxyUser.setDisable(proxyType.getValue() == SOCKS4);
|
||||
proxyPassword.setDisable(proxyType.getValue() == SOCKS4);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package ctbrec.ui.settings;
|
|||
|
||||
import static ctbrec.Settings.DirectoryStructure.*;
|
||||
import static ctbrec.Settings.ProxyType.*;
|
||||
import static ctbrec.Settings.SplitStrategy.*;
|
||||
import static java.util.Optional.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -36,6 +37,9 @@ import ctbrec.ui.settings.api.SimpleRangeProperty;
|
|||
import ctbrec.ui.settings.api.ValueConverter;
|
||||
import ctbrec.ui.sites.ConfigUI;
|
||||
import ctbrec.ui.tabs.TabSelectionListener;
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
|
@ -45,18 +49,33 @@ import javafx.beans.property.SimpleStringProperty;
|
|||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TextInputDialog;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.BorderStroke;
|
||||
import javafx.scene.layout.BorderStrokeStyle;
|
||||
import javafx.scene.layout.BorderWidths;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class SettingsTab extends Tab implements TabSelectionListener {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SettingsTab.class);
|
||||
public static final int CHECKBOX_MARGIN = 6;
|
||||
private static final long MiB = 1024 * 1024L;
|
||||
private static final long GiB = 1024 * MiB;
|
||||
|
||||
private List<Site> sites;
|
||||
private Recorder recorder;
|
||||
|
@ -71,6 +90,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
private SimpleBooleanProperty determineResolution;
|
||||
private SimpleBooleanProperty chooseStreamQuality;
|
||||
private SimpleBooleanProperty livePreviews;
|
||||
private SimpleBooleanProperty monitorClipboard;
|
||||
private SimpleListProperty<String> startTab;
|
||||
private SimpleFileProperty mediaPlayer;
|
||||
private SimpleStringProperty mediaPlayerParams;
|
||||
|
@ -85,6 +105,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
private SimpleDirectoryProperty recordingsDir;
|
||||
private SimpleListProperty<DirectoryStructure> directoryStructure;
|
||||
private SimpleListProperty<SplitAfterOption> splitAfter;
|
||||
private SimpleListProperty<SplitBiggerThanOption> splitBiggerThan;
|
||||
private SimpleRangeProperty<Integer> resolutionRange;
|
||||
private List<Integer> labels = Arrays.asList(0, 240, 360, 480, 600, 720, 960, 1080, 1440, 2160, 4320, 8640);
|
||||
private List<Integer> values = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);
|
||||
|
@ -94,6 +115,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
private SimpleBooleanProperty onlineCheckSkipsPausedModels;
|
||||
private SimpleLongProperty leaveSpaceOnDevice;
|
||||
private SimpleStringProperty ffmpegParameters;
|
||||
private SimpleBooleanProperty logFFmpegOutput;
|
||||
private SimpleStringProperty fileExtension;
|
||||
private SimpleStringProperty server;
|
||||
private SimpleIntegerProperty port;
|
||||
|
@ -104,8 +126,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
private ExclusiveSelectionProperty recordLocal;
|
||||
private SimpleIntegerProperty postProcessingThreads;
|
||||
private IgnoreList ignoreList;
|
||||
private PostProcessingStepPanel postProcessingStepPanel;
|
||||
private Button variablesHelpButton;
|
||||
private Label restartNotification;
|
||||
|
||||
public SettingsTab(List<Site> sites, Recorder recorder) {
|
||||
this.sites = sites;
|
||||
|
@ -124,6 +145,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
determineResolution = new SimpleBooleanProperty(null, "determineResolution", settings.determineResolution);
|
||||
chooseStreamQuality = new SimpleBooleanProperty(null, "chooseStreamQuality", settings.chooseStreamQuality);
|
||||
livePreviews = new SimpleBooleanProperty(null, "livePreviews", settings.livePreviews);
|
||||
monitorClipboard = new SimpleBooleanProperty(null, "monitorClipboard", settings.monitorClipboard);
|
||||
startTab = new SimpleListProperty<>(null, "startTab", FXCollections.observableList(getTabNames()));
|
||||
mediaPlayer = new SimpleFileProperty(null, "mediaPlayer", settings.mediaPlayer);
|
||||
mediaPlayerParams = new SimpleStringProperty(null, "mediaPlayerParams", settings.mediaPlayerParams);
|
||||
|
@ -137,12 +159,14 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
proxyPassword = new SimpleStringProperty(null, "proxyPassword", settings.proxyPassword);
|
||||
recordingsDir = new SimpleDirectoryProperty(null, "recordingsDir", settings.recordingsDir);
|
||||
directoryStructure = new SimpleListProperty<>(null, "recordingsDirStructure", FXCollections.observableList(List.of(FLAT, ONE_PER_MODEL, ONE_PER_RECORDING)));
|
||||
splitAfter = new SimpleListProperty<>(null, "splitRecordings", FXCollections.observableList(getSplitOptions()));
|
||||
splitAfter = new SimpleListProperty<>(null, "splitRecordingsAfterSecs", FXCollections.observableList(getSplitAfterSecsOptions()));
|
||||
splitBiggerThan = new SimpleListProperty<>(null, "splitRecordingsBiggerThanBytes", FXCollections.observableList(getSplitBiggerThanOptions()));
|
||||
resolutionRange = new SimpleRangeProperty<>(rangeValues, "minimumResolution", "maximumResolution", settings.minimumResolution, settings.maximumResolution);
|
||||
concurrentRecordings = new SimpleIntegerProperty(null, "concurrentRecordings", settings.concurrentRecordings);
|
||||
onlineCheckIntervalInSecs = new SimpleIntegerProperty(null, "onlineCheckIntervalInSecs", settings.onlineCheckIntervalInSecs);
|
||||
leaveSpaceOnDevice = new SimpleLongProperty(null, "minimumSpaceLeftInBytes", (long) new GigabytesConverter().convertTo(settings.minimumSpaceLeftInBytes));
|
||||
ffmpegParameters = new SimpleStringProperty(null, "ffmpegMergedDownloadArgs", settings.ffmpegMergedDownloadArgs);
|
||||
logFFmpegOutput = new SimpleBooleanProperty(null, "logFFmpegOutput", settings.logFFmpegOutput);
|
||||
fileExtension = new SimpleStringProperty(null, "ffmpegFileSuffix", settings.ffmpegFileSuffix);
|
||||
server = new SimpleStringProperty(null, "httpServer", settings.httpServer);
|
||||
port = new SimpleIntegerProperty(null, "httpPort", settings.httpPort);
|
||||
|
@ -157,8 +181,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private void createGui() {
|
||||
postProcessingStepPanel = new PostProcessingStepPanel(config);
|
||||
variablesHelpButton = createHelpButton("Variables", "http://localhost:5689/docs/PostProcessing.md#variables");
|
||||
PostProcessingStepPanel postProcessingStepPanel = new PostProcessingStepPanel(config);
|
||||
Button variablesHelpButton = createHelpButton("Variables", "http://localhost:5689/docs/PostProcessing.md#variables");
|
||||
ignoreList = new IgnoreList(sites);
|
||||
List<Category> siteCategories = new ArrayList<>();
|
||||
for (Site site : sites) {
|
||||
|
@ -166,18 +190,20 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
.ifPresent(configPanel -> siteCategories.add(Category.of(site.getName(), configPanel)));
|
||||
}
|
||||
|
||||
Preferences prefs = Preferences.of(new CtbrecPreferencesStorage(config),
|
||||
CtbrecPreferencesStorage storage = new CtbrecPreferencesStorage(config);
|
||||
Preferences prefs = Preferences.of(storage,
|
||||
Category.of("General",
|
||||
Group.of("General",
|
||||
Setting.of("User-Agent", httpUserAgent),
|
||||
Setting.of("User-Agent mobile", httpUserAgentMobile),
|
||||
Setting.of("Update overview interval (seconds)", overviewUpdateIntervalInSecs, "Update the thumbnail overviews every x seconds"),
|
||||
Setting.of("Update overview interval (seconds)", overviewUpdateIntervalInSecs, "Update the thumbnail overviews every x seconds").needsRestart(),
|
||||
Setting.of("Update thumbnails", updateThumbnails, "The overviews will still be updated, but the thumbnails won't be changed. This is useful for less powerful systems."),
|
||||
Setting.of("Display stream resolution in overview", determineResolution),
|
||||
Setting.of("Manually select stream quality", chooseStreamQuality, "Opens a dialog to select the video resolution before recording"),
|
||||
Setting.of("Enable live previews (experimental)", livePreviews),
|
||||
Setting.of("Add models from clipboard", monitorClipboard, "Monitor clipboard for model URLs and automatically add them to the recorder").needsRestart(),
|
||||
Setting.of("Start Tab", startTab),
|
||||
Setting.of("Colors (Base / Accent)", new ColorSettingsPane(Config.getInstance()))
|
||||
Setting.of("Colors (Base / Accent)", new ColorSettingsPane(Config.getInstance())).needsRestart()
|
||||
),
|
||||
Group.of("Player",
|
||||
Setting.of("Player", mediaPlayer),
|
||||
|
@ -191,7 +217,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
Group.of("Settings",
|
||||
Setting.of("Recordings Directory", recordingsDir),
|
||||
Setting.of("Directory Structure", directoryStructure),
|
||||
Setting.of("Split recordings after (minutes)", splitAfter).converter(SplitAfterOption.converter()),
|
||||
Setting.of("Split recordings after", splitAfter).converter(SplitAfterOption.converter()).onChange(this::splitValuesChanged),
|
||||
Setting.of("Split recordings bigger than", splitBiggerThan).converter(SplitBiggerThanOption.converter()).onChange(this::splitValuesChanged),
|
||||
Setting.of("Restrict Resolution", resolutionRange, "Only record streams with resolution within the given range"),
|
||||
Setting.of("Concurrent Recordings (0 = unlimited)", concurrentRecordings),
|
||||
Setting.of("Leave space on device (GiB)", leaveSpaceOnDevice, "Stop recording, if the free space on the device gets below this threshold").converter(new GigabytesConverter()),
|
||||
|
@ -201,7 +228,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
Setting.of("Skip online check for paused models", onlineCheckSkipsPausedModels, "Skip online check for paused models")
|
||||
),
|
||||
Group.of("Location",
|
||||
Setting.of("Record Location", recordLocal),
|
||||
Setting.of("Record Location", recordLocal).needsRestart(),
|
||||
Setting.of("Server", server),
|
||||
Setting.of("Port", port),
|
||||
Setting.of("Path", path, "Leave empty, if you didn't change the servletContext in the server config"),
|
||||
|
@ -223,15 +250,22 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
Category.of("Sites", siteCategories.toArray(new Category[0])),
|
||||
Category.of("Proxy",
|
||||
Group.of("Proxy",
|
||||
Setting.of("Type", proxyType),
|
||||
Setting.of("Host", proxyHost),
|
||||
Setting.of("Port", proxyPort),
|
||||
Setting.of("Username", proxyUser),
|
||||
Setting.of("Password", proxyPassword)
|
||||
Setting.of("Type", proxyType).needsRestart(),
|
||||
Setting.of("Host", proxyHost).needsRestart(),
|
||||
Setting.of("Port", proxyPort).needsRestart(),
|
||||
Setting.of("Username", proxyUser).needsRestart(),
|
||||
Setting.of("Password", proxyPassword).needsRestart()
|
||||
)
|
||||
),
|
||||
Category.of("Advanced / Devtools",
|
||||
Group.of("Logging",
|
||||
Setting.of("Log FFmpeg output", logFFmpegOutput, "Log FFmpeg output to files in the system's temp directory")
|
||||
)
|
||||
)
|
||||
);
|
||||
Region preferencesView = prefs.getView();
|
||||
prefs.onRestartRequired(this::showRestartRequired);
|
||||
storage.setPreferences(prefs);
|
||||
preferencesView.setMinSize(800, 400);
|
||||
preferencesView.setPrefSize(1280, 960);
|
||||
ScrollPane scrollPane = new ScrollPane(preferencesView);
|
||||
|
@ -241,7 +275,20 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
GridPane.setFillHeight(scrollPane, true);
|
||||
GridPane.setHgrow(scrollPane, Priority.ALWAYS);
|
||||
GridPane.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
setContent(container);
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(container);
|
||||
restartNotification = new Label("Restart Required");
|
||||
restartNotification.setVisible(false);
|
||||
restartNotification.setOpacity(0);
|
||||
restartNotification.setStyle("-fx-font-size: 28; -fx-padding: .3em");
|
||||
restartNotification.setBorder(new Border(new BorderStroke(Color.web(settings.colorAccent), BorderStrokeStyle.SOLID, new CornerRadii(5), new BorderWidths(2))));
|
||||
restartNotification.setBackground(new Background(new BackgroundFill(Color.web(settings.colorBase), new CornerRadii(5), Insets.EMPTY)));
|
||||
stackPane.getChildren().add(restartNotification);
|
||||
StackPane.setAlignment(restartNotification, Pos.TOP_RIGHT);
|
||||
StackPane.setMargin(restartNotification, new Insets(10, 40, 0, 0));
|
||||
|
||||
setContent(stackPane);
|
||||
prefs.expandTree();
|
||||
|
||||
|
||||
|
@ -251,7 +298,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
prefs.getSetting("requireAuthentication").ifPresent(s -> bindEnabledProperty(s, recordLocal));
|
||||
prefs.getSetting("transportLayerSecurity").ifPresent(s -> bindEnabledProperty(s, recordLocal));
|
||||
prefs.getSetting("recordingsDir").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||
prefs.getSetting("splitRecordings").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||
prefs.getSetting("splitRecordingsAfterSecs").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||
prefs.getSetting("splitRecordingsBiggerThanBytes").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||
prefs.getSetting("minimumResolution").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||
prefs.getSetting("recordingsDirStructure").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||
prefs.getSetting("onlineCheckIntervalInSecs").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||
|
@ -266,6 +314,23 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
prefs.getSetting("downloadFilename").ifPresent(s -> bindEnabledProperty(s, recordLocal));
|
||||
postProcessingStepPanel.disableProperty().bind(recordLocal.not());
|
||||
variablesHelpButton.disableProperty().bind(recordLocal);
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void splitValuesChanged(ObservableValue<?> value, Object oldV, Object newV) {
|
||||
boolean splitAfterSet = settings.splitRecordingsAfterSecs > 0;
|
||||
boolean splitBiggerThanSet = settings.splitRecordingsBiggerThanBytes > 0;
|
||||
if (splitAfterSet && splitBiggerThanSet) {
|
||||
settings.splitStrategy = TIME_OR_SIZE;
|
||||
} else if (splitAfterSet) {
|
||||
settings.splitStrategy = TIME;
|
||||
} else if (splitBiggerThanSet) {
|
||||
settings.splitStrategy = SIZE;
|
||||
} else {
|
||||
settings.splitStrategy = DONT;
|
||||
}
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
private Button createHelpButton(String text, String url) {
|
||||
|
@ -288,7 +353,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<SplitAfterOption> getSplitOptions() {
|
||||
private List<SplitAfterOption> getSplitAfterSecsOptions() {
|
||||
List<SplitAfterOption> splitOptions = new ArrayList<>();
|
||||
splitOptions.add(new SplitAfterOption("disabled", 0));
|
||||
if (Config.isDevMode()) {
|
||||
|
@ -304,6 +369,29 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
return splitOptions;
|
||||
}
|
||||
|
||||
private List<SplitBiggerThanOption> getSplitBiggerThanOptions() {
|
||||
List<SplitBiggerThanOption> splitOptions = new ArrayList<>();
|
||||
splitOptions.add(new SplitBiggerThanOption("disabled", 0));
|
||||
if (Config.isDevMode()) {
|
||||
splitOptions.add(new SplitBiggerThanOption("10 MiB", 10 * MiB));
|
||||
splitOptions.add(new SplitBiggerThanOption("20 MiB", 20 * MiB));
|
||||
}
|
||||
splitOptions.add(new SplitBiggerThanOption("100 MiB", 100 * MiB));
|
||||
splitOptions.add(new SplitBiggerThanOption("250 MiB", 250 * MiB));
|
||||
splitOptions.add(new SplitBiggerThanOption("500 MiB", 500 * MiB));
|
||||
splitOptions.add(new SplitBiggerThanOption("1 GiB", 1 * GiB));
|
||||
splitOptions.add(new SplitBiggerThanOption("2 GiB", 2 * GiB));
|
||||
splitOptions.add(new SplitBiggerThanOption("3 GiB", 3 * GiB));
|
||||
splitOptions.add(new SplitBiggerThanOption("4 GiB", 4 * GiB));
|
||||
splitOptions.add(new SplitBiggerThanOption("5 GiB", 5 * GiB));
|
||||
splitOptions.add(new SplitBiggerThanOption("6 GiB", 6 * GiB));
|
||||
splitOptions.add(new SplitBiggerThanOption("7 GiB", 7 * GiB));
|
||||
splitOptions.add(new SplitBiggerThanOption("8 GiB", 8 * GiB));
|
||||
splitOptions.add(new SplitBiggerThanOption("9 GiB", 9 * GiB));
|
||||
splitOptions.add(new SplitBiggerThanOption("10 GiB", 10 * GiB));
|
||||
return splitOptions;
|
||||
}
|
||||
|
||||
private void requireAuthenticationChanged(ObservableValue<?> obs, Boolean oldV, Boolean newV) { // NOSONAR
|
||||
boolean requiresAuthentication = newV;
|
||||
Config.getInstance().getSettings().requireAuthentication = requiresAuthentication;
|
||||
|
@ -359,7 +447,26 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
void showRestartRequired() {
|
||||
// TODO restartLabel.setVisible(true);
|
||||
if (!restartNotification.isVisible()) {
|
||||
restartNotification.setVisible(true);
|
||||
Transition fadeIn = changeOpacity(restartNotification, 1);
|
||||
fadeIn.play();
|
||||
fadeIn.setOnFinished(e -> {
|
||||
Transition fadeOut = changeOpacity(restartNotification, 0);
|
||||
fadeOut.setOnFinished(e2 -> restartNotification.setVisible(false));
|
||||
PauseTransition pauseTransition = new PauseTransition(Duration.seconds(5));
|
||||
pauseTransition.setOnFinished(evt -> fadeOut.play());
|
||||
pauseTransition.play();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static final Duration ANIMATION_DURATION = new Duration(500);
|
||||
private Transition changeOpacity(Node node, double opacity) {
|
||||
FadeTransition transition = new FadeTransition(ANIMATION_DURATION, node);
|
||||
transition.setFromValue(node.getOpacity());
|
||||
transition.setToValue(opacity);
|
||||
return transition;
|
||||
}
|
||||
|
||||
public static class SplitAfterOption {
|
||||
|
@ -415,4 +522,60 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class SplitBiggerThanOption {
|
||||
private String label;
|
||||
private long value;
|
||||
|
||||
public SplitBiggerThanOption(String label, long value) {
|
||||
super();
|
||||
this.label = label;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public long getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + (int) (value ^ (value >>> 32));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
SplitBiggerThanOption other = (SplitBiggerThanOption) obj;
|
||||
if (value != other.value)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ValueConverter converter() {
|
||||
return new ValueConverter() {
|
||||
@Override
|
||||
public Long convertFrom(Object splitBiggerThanOption) {
|
||||
return ((SplitBiggerThanOption) splitBiggerThanOption).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SplitBiggerThanOption convertTo(Object value) {
|
||||
return new SplitBiggerThanOption(value.toString(), (Long) value);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@ public class GigabytesConverter implements ValueConverter {
|
|||
|
||||
@Override
|
||||
public Object convertFrom(Object b) {
|
||||
long spaceLeftInGiB = (long) b;
|
||||
return spaceLeftInGiB * ONE_GIB_IN_BYTES;
|
||||
long gibiBytes = (long) b;
|
||||
return gibiBytes * ONE_GIB_IN_BYTES;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -35,6 +35,8 @@ public class Preferences {
|
|||
|
||||
private PreferencesStorage preferencesStorage;
|
||||
|
||||
private Runnable restartRequiredCallback = () -> {};
|
||||
|
||||
private Preferences(PreferencesStorage preferencesStorage, Category...categories) {
|
||||
this.preferencesStorage = preferencesStorage;
|
||||
this.categories = categories;
|
||||
|
@ -248,4 +250,12 @@ public class Preferences {
|
|||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public void onRestartRequired(Runnable callback) {
|
||||
this.restartRequiredCallback = callback;
|
||||
}
|
||||
|
||||
public Runnable getRestartRequiredCallback() {
|
||||
return restartRequiredCallback;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import static java.util.Optional.*;
|
|||
|
||||
import ctbrec.StringUtil;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Tooltip;
|
||||
|
@ -17,6 +18,7 @@ public class Setting {
|
|||
private PreferencesStorage preferencesStorage;
|
||||
private boolean needsRestart = false;
|
||||
private ValueConverter converter;
|
||||
private ChangeListener<?> changeListener;
|
||||
|
||||
protected Setting(String name, Property<?> property) {
|
||||
this.name = name;
|
||||
|
@ -107,4 +109,13 @@ public class Setting {
|
|||
public ValueConverter getConverter() {
|
||||
return converter;
|
||||
}
|
||||
|
||||
public Setting onChange(ChangeListener<?> changeListener) {
|
||||
this.changeListener = changeListener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ChangeListener<?> getChangeListener() {
|
||||
return changeListener;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,11 +67,10 @@ public class BongaCamsElectronLoginDialog {
|
|||
browser.executeJavaScript("document.getElementById('log_in_password').value = '" + password + "';");
|
||||
}
|
||||
String[] simplify = new String[] {
|
||||
"$('div#header').css('display','none');",
|
||||
"$('div.footer').css('display','none');",
|
||||
"$('div.footer_copy').css('display','none')",
|
||||
"$('div[class~=\"banner_top_index\"]').css('display','none');",
|
||||
"$('td.menu_container').css('display','none');",
|
||||
"$('div[class~=\"page_header\"]').css('display','none');",
|
||||
"$('div[class~=\"header_bar\"]').css('display','none')",
|
||||
"$('footer').css('display','none');",
|
||||
"$('div[class~=\"footer_copy\"]').css('display','none')",
|
||||
"$('div[class~=\"fancybox-overlay\"]').css('display','none');"
|
||||
};
|
||||
for (String js : simplify) {
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
package ctbrec.ui.sites.bonga;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.sites.bonga.BongaCams;
|
||||
import ctbrec.sites.bonga.BongaCamsHttpClient;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import ctbrec.ui.sites.AbstractSiteUi;
|
||||
import ctbrec.ui.sites.ConfigUI;
|
||||
import ctbrec.ui.tabs.TabProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
public class BongaCamsSiteUi extends AbstractSiteUi {
|
||||
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsSiteUi.class);
|
||||
private BongaCamsTabProvider tabProvider;
|
||||
private BongaCamsConfigUI configUi;
|
||||
private BongaCams bongaCams;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BongaCamsSiteUi.class);
|
||||
private final BongaCamsTabProvider tabProvider;
|
||||
private final BongaCamsConfigUI configUi;
|
||||
private final BongaCams bongaCams;
|
||||
|
||||
public BongaCamsSiteUi(BongaCams bongaCams) {
|
||||
this.bongaCams = bongaCams;
|
||||
|
@ -57,11 +56,13 @@ public class BongaCamsSiteUi extends AbstractSiteUi {
|
|||
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);
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ public class BongaCamsTabProvider extends TabProvider {
|
|||
tabs.add(createTab("Transsexual", updateService));
|
||||
|
||||
// new
|
||||
url = BongaCams.baseUrl + "/tools/listing_v3.php?livetab=new-models&online_only=true&is_mobile=true&offset=";
|
||||
url = BongaCams.baseUrl + "/tools/listing_v3.php?livetab=new&online_only=true&is_mobile=true&offset=";
|
||||
updateService = new BongaCamsUpdateService(bongaCams, url);
|
||||
tabs.add(createTab("New", updateService));
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import static ctbrec.sites.camsoda.Camsoda.*;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import ctbrec.recorder.Recorder;
|
||||
|
@ -16,6 +17,7 @@ import javafx.scene.control.Tab;
|
|||
|
||||
public class CamsodaTabProvider extends TabProvider {
|
||||
|
||||
private static final String API_URL = BASE_URI + "/api/v1/browse/online";
|
||||
private Camsoda camsoda;
|
||||
private Recorder recorder;
|
||||
CamsodaFollowedTab followedTab;
|
||||
|
@ -29,8 +31,13 @@ public class CamsodaTabProvider extends TabProvider {
|
|||
@Override
|
||||
public List<Tab> getTabs(Scene scene) {
|
||||
List<Tab> tabs = new ArrayList<>();
|
||||
tabs.add(createTab("Online", BASE_URI + "/api/v1/browse/online", m -> true));
|
||||
//tabs.add(createTab("New", BASE_URI + "/api/v1/browse/online", CamsodaModel::isNew));
|
||||
tabs.add(createTab("All", API_URL, m -> true));
|
||||
tabs.add(createTab("New", API_URL, CamsodaModel::isNew));
|
||||
tabs.add(createTab("Female", API_URL, m -> Objects.equals("f", m.getGender())));
|
||||
tabs.add(createTab("Male", API_URL, m -> Objects.equals("m", m.getGender())));
|
||||
tabs.add(createTab("Couples", API_URL, m -> Objects.equals("c", m.getGender())));
|
||||
tabs.add(createTab("Trans", API_URL, m -> Objects.equals("t", m.getGender())));
|
||||
//tabs.add(createTab("#petite", BASE_URI + "/api/v1/browse/tag-petite", m -> true));
|
||||
followedTab.setRecorder(recorder);
|
||||
followedTab.setScene(scene);
|
||||
tabs.add(followedTab);
|
||||
|
|
|
@ -74,60 +74,58 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
|
|||
if (response.isSuccessful()) {
|
||||
String body = response.body().string();
|
||||
JSONObject json = new JSONObject(body);
|
||||
if (json.optBoolean("status")) {
|
||||
JSONArray template = json.getJSONArray("template");
|
||||
JSONArray results = json.getJSONArray("results");
|
||||
for (int i = 0; i < results.length(); i++) {
|
||||
Object result = results.getJSONObject(i).get("tpl");
|
||||
CamsodaModel model;
|
||||
try {
|
||||
if (result instanceof JSONObject) {
|
||||
JSONObject tpl = (JSONObject) result;
|
||||
String name = tpl.getString(Integer.toString(getTemplateIndex(template, "username")));
|
||||
model = (CamsodaModel) camsoda.createModel(name);
|
||||
model.setDescription(tpl.getString(Integer.toString(getTemplateIndex(template, "subject_html"))));
|
||||
model.setSortOrder(tpl.getFloat(Integer.toString(getTemplateIndex(template, "sort_value"))));
|
||||
String preview = "https:" + tpl.getString(Integer.toString(getTemplateIndex(template, "thumb")));
|
||||
model.setPreview(preview);
|
||||
String displayName = tpl.getString(Integer.toString(getTemplateIndex(template, "display_name")));
|
||||
model.setDisplayName(displayName.replaceAll("[^a-zA-Z0-9]", ""));
|
||||
if (model.getDisplayName().isBlank()) {
|
||||
model.setDisplayName(name);
|
||||
}
|
||||
model.setOnlineState(tpl.getString(Integer.toString(getTemplateIndex(template, "stream_name"))).isEmpty() ? OFFLINE : ONLINE);
|
||||
try {
|
||||
String statusIndex = Integer.toString(getTemplateIndex(template, "status"));
|
||||
if (tpl.has(statusIndex)) {
|
||||
model.setOnlineStateByStatus(tpl.getString(statusIndex));
|
||||
}
|
||||
} catch (NoSuchElementException e) {
|
||||
}
|
||||
models.add(model);
|
||||
} else if (result instanceof JSONArray) {
|
||||
JSONArray tpl = (JSONArray) result;
|
||||
String name = tpl.getString(getTemplateIndex(template, "username"));
|
||||
model = (CamsodaModel) camsoda.createModel(name);
|
||||
model.setSortOrder(tpl.getFloat(getTemplateIndex(template, "sort_value")));
|
||||
model.setDescription(tpl.getString(getTemplateIndex(template, "subject_html")));
|
||||
String preview = "https:" + tpl.getString(getTemplateIndex(template, "thumb"));
|
||||
model.setPreview(preview);
|
||||
model.setOnlineStateByStatus(tpl.getString(getTemplateIndex(template, "status")));
|
||||
String displayName = tpl.getString(getTemplateIndex(template, "display_name"));
|
||||
model.setDisplayName(displayName.replaceAll("[^a-zA-Z0-9]", ""));
|
||||
if (model.getDisplayName().isBlank()) {
|
||||
model.setDisplayName(name);
|
||||
}
|
||||
models.add(model);
|
||||
JSONArray template = json.getJSONArray("template");
|
||||
JSONArray results = json.getJSONArray("results");
|
||||
for (int i = 0; i < results.length(); i++) {
|
||||
JSONObject result = results.getJSONObject(i);
|
||||
Object templateObject = result.get("tpl");
|
||||
CamsodaModel model;
|
||||
try {
|
||||
if (templateObject instanceof JSONObject) {
|
||||
JSONObject tpl = (JSONObject) templateObject;
|
||||
String name = tpl.getString(Integer.toString(getTemplateIndex(template, "username")));
|
||||
model = (CamsodaModel) camsoda.createModel(name);
|
||||
model.setDescription(tpl.getString(Integer.toString(getTemplateIndex(template, "subject_html"))));
|
||||
model.setSortOrder(tpl.getFloat(Integer.toString(getTemplateIndex(template, "sort_value"))));
|
||||
model.setNew(result.optBoolean("new"));
|
||||
model.setGender(tpl.getString(Integer.toString(getTemplateIndex(template, "gender"))));
|
||||
String preview = "https:" + tpl.getString(Integer.toString(getTemplateIndex(template, "thumb")));
|
||||
model.setPreview(preview);
|
||||
String displayName = tpl.getString(Integer.toString(getTemplateIndex(template, "display_name")));
|
||||
model.setDisplayName(displayName.replaceAll("[^a-zA-Z0-9]", ""));
|
||||
if (model.getDisplayName().isBlank()) {
|
||||
model.setDisplayName(name);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Couldn't parse one of the models: {}", result, e);
|
||||
model.setOnlineState(tpl.getString(Integer.toString(getTemplateIndex(template, "stream_name"))).isEmpty() ? OFFLINE : ONLINE);
|
||||
try {
|
||||
String statusIndex = Integer.toString(getTemplateIndex(template, "status"));
|
||||
if (tpl.has(statusIndex)) {
|
||||
model.setOnlineStateByStatus(tpl.getString(statusIndex));
|
||||
}
|
||||
} catch (NoSuchElementException e) {
|
||||
}
|
||||
models.add(model);
|
||||
} else if (templateObject instanceof JSONArray) {
|
||||
JSONArray tpl = (JSONArray) templateObject;
|
||||
String name = tpl.getString(getTemplateIndex(template, "username"));
|
||||
model = (CamsodaModel) camsoda.createModel(name);
|
||||
model.setSortOrder(tpl.getFloat(getTemplateIndex(template, "sort_value")));
|
||||
model.setDescription(tpl.getString(getTemplateIndex(template, "subject_html")));
|
||||
String preview = "https:" + tpl.getString(getTemplateIndex(template, "thumb"));
|
||||
model.setPreview(preview);
|
||||
model.setOnlineStateByStatus(tpl.getString(getTemplateIndex(template, "status")));
|
||||
String displayName = tpl.getString(getTemplateIndex(template, "display_name"));
|
||||
model.setDisplayName(displayName.replaceAll("[^a-zA-Z0-9]", ""));
|
||||
if (model.getDisplayName().isBlank()) {
|
||||
model.setDisplayName(name);
|
||||
}
|
||||
models.add(model);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Couldn't parse one of the models: {}", result, e);
|
||||
}
|
||||
return models;
|
||||
} else {
|
||||
LOG.debug("Response was not successful: {}", json);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return models;
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
|
|
|
@ -45,10 +45,10 @@ public class ChaturbateConfigUi extends AbstractConfigUI {
|
|||
layout.add(enabled, 1, row++);
|
||||
|
||||
layout.add(new Label("Chaturbate User"), 0, row);
|
||||
TextField username = new TextField(Config.getInstance().getSettings().username);
|
||||
TextField username = new TextField(Config.getInstance().getSettings().chaturbateUsername);
|
||||
username.textProperty().addListener((ob, o, n) -> {
|
||||
if(!n.equals(Config.getInstance().getSettings().username)) {
|
||||
Config.getInstance().getSettings().username = n;
|
||||
if(!n.equals(Config.getInstance().getSettings().chaturbateUsername)) {
|
||||
Config.getInstance().getSettings().chaturbateUsername = n;
|
||||
chaturbate.getHttpClient().logout();
|
||||
save();
|
||||
}
|
||||
|
@ -60,10 +60,10 @@ public class ChaturbateConfigUi extends AbstractConfigUI {
|
|||
|
||||
layout.add(new Label("Chaturbate Password"), 0, row);
|
||||
PasswordField password = new PasswordField();
|
||||
password.setText(Config.getInstance().getSettings().password);
|
||||
password.setText(Config.getInstance().getSettings().chaturbatePassword);
|
||||
password.textProperty().addListener((ob, o, n) -> {
|
||||
if(!n.equals(Config.getInstance().getSettings().password)) {
|
||||
Config.getInstance().getSettings().password = n;
|
||||
if(!n.equals(Config.getInstance().getSettings().chaturbatePassword)) {
|
||||
Config.getInstance().getSettings().chaturbatePassword = n;
|
||||
chaturbate.getHttpClient().logout();
|
||||
save();
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.StringUtil;
|
||||
import ctbrec.sites.chaturbate.Chaturbate;
|
||||
import ctbrec.sites.chaturbate.ChaturbateModelParser;
|
||||
import ctbrec.ui.SiteUiFactory;
|
||||
|
@ -49,7 +48,7 @@ public class ChaturbateUpdateService extends PaginatedScheduledService {
|
|||
return new Task<List<Model>>() {
|
||||
@Override
|
||||
public List<Model> call() throws IOException {
|
||||
if(loginRequired && StringUtil.isBlank(ctbrec.Config.getInstance().getSettings().username)) {
|
||||
if(loginRequired && !chaturbate.credentialsAvailable()) {
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
String url = ChaturbateUpdateService.this.url + "?page="+page+"&keywords=&_=" + System.currentTimeMillis();
|
||||
|
|
|
@ -62,6 +62,7 @@ public class StreamateUpdateService extends PaginatedScheduledService {
|
|||
model.setId(p.getLong("id"));
|
||||
//model.setPreview(p.getString("thumbnail"));
|
||||
model.setPreview("https://cdn.nsimg.net/snap/320x240/" + model.getId() + ".jpg");
|
||||
model.setDescription(p.optString("headlineMessage"));
|
||||
boolean online = p.optBoolean("online");
|
||||
model.setOnline(online);
|
||||
model.setOnlineState(online ? ONLINE : OFFLINE);
|
||||
|
|
|
@ -15,7 +15,6 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.StringUtil;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.sites.stripchat.Stripchat;
|
||||
import ctbrec.sites.stripchat.StripchatModel;
|
||||
|
@ -46,7 +45,7 @@ public class StripchatUpdateService extends PaginatedScheduledService {
|
|||
@Override
|
||||
public List<Model> call() throws IOException {
|
||||
List<Model> models = new ArrayList<>();
|
||||
if(loginRequired && StringUtil.isBlank(ctbrec.Config.getInstance().getSettings().username)) {
|
||||
if(loginRequired && !stripchat.credentialsAvailable()) {
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
int offset = (getPage() - 1) * modelsPerPage;
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package ctbrec.ui.tabs;
|
||||
|
||||
import javafx.animation.Transition;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class FollowTabBlinkTransition extends Transition {
|
||||
|
||||
private final String normalStyle;
|
||||
private final Tab followedTab;
|
||||
private final Color normal;
|
||||
private final Color highlight;
|
||||
|
||||
FollowTabBlinkTransition(Tab followedTab) {
|
||||
this.followedTab = followedTab;
|
||||
normalStyle = followedTab.getStyle();
|
||||
normal = Color.web("#f4f4f4");
|
||||
highlight = Color.web("#2b8513");
|
||||
|
||||
setCycleDuration(Duration.millis(500));
|
||||
setCycleCount(6);
|
||||
setAutoReverse(true);
|
||||
setOnFinished(evt -> followedTab.setStyle(normalStyle));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void interpolate(double fraction) {
|
||||
double rh = highlight.getRed();
|
||||
double rn = normal.getRed();
|
||||
double diff = rh - rn;
|
||||
double r = (rn + diff * fraction) * 255;
|
||||
double gh = highlight.getGreen();
|
||||
double gn = normal.getGreen();
|
||||
diff = gh - gn;
|
||||
double g = (gn + diff * fraction) * 255;
|
||||
double bh = highlight.getBlue();
|
||||
double bn = normal.getBlue();
|
||||
diff = bh - bn;
|
||||
double b = (bn + diff * fraction) * 255;
|
||||
String style = "-fx-background-color: rgb(" + r + "," + g + "," + b + ")";
|
||||
followedTab.setStyle(style);
|
||||
}
|
||||
}
|
|
@ -38,6 +38,7 @@ import ctbrec.ui.DesktopIntegration;
|
|||
import ctbrec.ui.JavaFxModel;
|
||||
import ctbrec.ui.PreviewPopupHandler;
|
||||
import ctbrec.ui.StreamSourceSelectionDialog;
|
||||
import ctbrec.ui.action.CheckModelAccountAction;
|
||||
import ctbrec.ui.action.EditNotesAction;
|
||||
import ctbrec.ui.action.FollowAction;
|
||||
import ctbrec.ui.action.OpenRecordingsDir;
|
||||
|
@ -125,6 +126,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
Button pauseAll = new Button("Pause All");
|
||||
Button resumeAll = new Button("Resume All");
|
||||
ToggleButton toggleRecording = new ToggleButton("Pause Recording");
|
||||
Button checkModelAccountExistance = new Button("Check URLs");
|
||||
TextField filter;
|
||||
|
||||
public RecordedModelsTab(String title, Recorder recorder, List<Site> sites) {
|
||||
|
@ -269,7 +271,9 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
ObservableList<String> suggestions = FXCollections.observableArrayList();
|
||||
sites.forEach(site -> suggestions.add(site.getClass().getSimpleName()));
|
||||
model = new AutoFillTextField(new ObservableListSuggester(suggestions));
|
||||
model.setPrefWidth(600);
|
||||
model.minWidth(150);
|
||||
model.prefWidth(600);
|
||||
HBox.setHgrow(model, Priority.ALWAYS);
|
||||
model.setPromptText("e.g. MyFreeCams:ModelName or an URL like https://chaturbate.com/modelname/");
|
||||
model.onActionHandler(this::addModel);
|
||||
model.setTooltip(new Tooltip("To add a model enter SiteName:ModelName\n" +
|
||||
|
@ -277,7 +281,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
BorderPane.setMargin(addModelBox, new Insets(5));
|
||||
addModelButton.setOnAction(this::addModel);
|
||||
addModelButton.setPadding(new Insets(5));
|
||||
addModelBox.getChildren().addAll(modelLabel, model, addModelButton, pauseAll, resumeAll, toggleRecording);
|
||||
addModelBox.getChildren().addAll(modelLabel, model, addModelButton, pauseAll, resumeAll, toggleRecording, checkModelAccountExistance);
|
||||
HBox.setMargin(pauseAll, new Insets(0, 0, 0, 20));
|
||||
pauseAll.setOnAction(this::pauseAll);
|
||||
resumeAll.setOnAction(this::resumeAll);
|
||||
|
@ -286,13 +290,21 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
toggleRecording.setPadding(new Insets(5));
|
||||
toggleRecording.setOnAction(this::toggleRecording);
|
||||
HBox.setMargin(toggleRecording, new Insets(0, 0, 0, 20));
|
||||
checkModelAccountExistance.setPadding(new Insets(5));
|
||||
checkModelAccountExistance.setTooltip(new Tooltip("Go over all model URLs and check, if the account still exists"));
|
||||
HBox.setMargin(checkModelAccountExistance, new Insets(0, 0, 0, 20));
|
||||
checkModelAccountExistance.setOnAction(evt -> new CheckModelAccountAction(checkModelAccountExistance, recorder).execute());
|
||||
|
||||
HBox filterContainer = new HBox();
|
||||
filterContainer.setSpacing(0);
|
||||
filterContainer.setPadding(new Insets(0));
|
||||
filterContainer.setAlignment(Pos.CENTER_RIGHT);
|
||||
filterContainer.minWidth(100);
|
||||
filterContainer.prefWidth(150);
|
||||
HBox.setHgrow(filterContainer, Priority.ALWAYS);
|
||||
filter = new SearchBox(false);
|
||||
filter.minWidth(100);
|
||||
filter.prefWidth(150);
|
||||
filter.setPromptText("Filter");
|
||||
filter.textProperty().addListener( (observableValue, oldValue, newValue) -> {
|
||||
String q = filter.getText();
|
||||
|
@ -645,6 +657,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
switchStreamSource.setOnAction(e -> switchStreamSource(selectedModels.get(0)));
|
||||
MenuItem follow = new MenuItem("Follow");
|
||||
follow.setOnAction(e -> follow(selectedModels));
|
||||
follow.setDisable(!selectedModels.stream().allMatch(m -> m.getSite().supportsFollow() && m.getSite().credentialsAvailable()));
|
||||
MenuItem ignore = new MenuItem("Ignore");
|
||||
ignore.setOnAction(e -> ignore(selectedModels));
|
||||
MenuItem notes = new MenuItem("Notes");
|
||||
|
|
|
@ -14,7 +14,6 @@ import java.text.DecimalFormat;
|
|||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
@ -23,11 +22,13 @@ import java.util.concurrent.Executors;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.Recording.State;
|
||||
import ctbrec.StringUtil;
|
||||
|
@ -38,13 +39,16 @@ import ctbrec.recorder.ProgressListener;
|
|||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.recorder.RecordingPinnedException;
|
||||
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
|
||||
import ctbrec.sites.Site;
|
||||
import ctbrec.ui.AutosizeAlert;
|
||||
import ctbrec.ui.CamrecApplication;
|
||||
import ctbrec.ui.DesktopIntegration;
|
||||
import ctbrec.ui.FileDownload;
|
||||
import ctbrec.ui.JavaFxRecording;
|
||||
import ctbrec.ui.Player;
|
||||
import ctbrec.ui.action.FollowAction;
|
||||
import ctbrec.ui.action.PauseAction;
|
||||
import ctbrec.ui.action.PlayAction;
|
||||
import ctbrec.ui.action.StopRecordingAction;
|
||||
import ctbrec.ui.controls.DateTimeCellFactory;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import ctbrec.ui.controls.Toast;
|
||||
|
@ -91,10 +95,8 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
private static final Logger LOG = LoggerFactory.getLogger(RecordingsTab.class);
|
||||
|
||||
private ScheduledService<List<JavaFxRecording>> updateService;
|
||||
private Config config;
|
||||
private Recorder recorder;
|
||||
@SuppressWarnings("unused")
|
||||
private List<Site> sites;
|
||||
private final Config config;
|
||||
private final Recorder recorder;
|
||||
private long spaceTotal = -1;
|
||||
private long spaceFree = -1;
|
||||
|
||||
|
@ -107,11 +109,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
Label spaceLabel;
|
||||
Lock recordingsLock = new ReentrantLock();
|
||||
|
||||
public RecordingsTab(String title, Recorder recorder, Config config, List<Site> sites) {
|
||||
public RecordingsTab(String title, Recorder recorder, Config config) {
|
||||
super(title);
|
||||
this.recorder = recorder;
|
||||
this.config = config;
|
||||
this.sites = sites;
|
||||
createGui();
|
||||
setClosable(false);
|
||||
initializeUpdateService();
|
||||
|
@ -138,9 +139,9 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
date.setId("date");
|
||||
date.setCellValueFactory(cdf -> {
|
||||
Instant instant = cdf.getValue().getStartDate();
|
||||
return new SimpleObjectProperty<Instant>(instant);
|
||||
return new SimpleObjectProperty<>(instant);
|
||||
});
|
||||
date.setCellFactory(new DateTimeCellFactory<JavaFxRecording>());
|
||||
date.setCellFactory(new DateTimeCellFactory<>());
|
||||
date.setPrefWidth(200);
|
||||
TableColumn<JavaFxRecording, String> status = new TableColumn<>("Status");
|
||||
status.setId("status");
|
||||
|
@ -191,32 +192,29 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
public boolean isDownloadRunning() {
|
||||
return observableRecordings.stream()
|
||||
.map(Recording::getStatus)
|
||||
.anyMatch(s -> s == DOWNLOADING);
|
||||
return observableRecordings.stream().map(Recording::getStatus).anyMatch(s -> s == DOWNLOADING);
|
||||
}
|
||||
|
||||
private TableCell<JavaFxRecording, Number> createSizeCell() {
|
||||
TableCell<JavaFxRecording, Number> cell = new TableCell<JavaFxRecording, Number>() {
|
||||
return new TableCell<>() {
|
||||
@Override
|
||||
protected void updateItem(Number sizeInByte, boolean empty) {
|
||||
if(empty || sizeInByte == null) {
|
||||
if (empty || sizeInByte == null) {
|
||||
setText(null);
|
||||
setStyle(null);
|
||||
} else {
|
||||
setText(StringUtil.formatSize(sizeInByte));
|
||||
setStyle("-fx-alignment: CENTER-RIGHT;");
|
||||
if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||
if (Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||
int row = this.getTableRow().getIndex();
|
||||
JavaFxRecording rec = tableViewProperty().get().getItems().get(row);
|
||||
if(!rec.valueChanged() && rec.getStatus() == RECORDING) {
|
||||
if (!rec.valueChanged() && rec.getStatus() == RECORDING) {
|
||||
setStyle("-fx-alignment: CENTER-RIGHT; -fx-background-color: red");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return cell;
|
||||
}
|
||||
|
||||
private void onContextMenuRequested(ContextMenuEvent event) {
|
||||
|
@ -231,26 +229,29 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private void onMousePressed(MouseEvent event) {
|
||||
if(popup != null) {
|
||||
if (popup != null) {
|
||||
popup.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void onMouseClicked(MouseEvent event) {
|
||||
if(event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||
Recording recording = table.getSelectionModel().getSelectedItem();
|
||||
if(recording != null) {
|
||||
play(recording);
|
||||
if (recording != null) {
|
||||
State state = recording.getStatus();
|
||||
if(state == FINISHED || state == RECORDING && config.getSettings().localRecording) {
|
||||
play(recording);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onKeyPressed( KeyEvent event ) {
|
||||
private void onKeyPressed(KeyEvent event) {
|
||||
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
|
||||
if (recordings != null && !recordings.isEmpty()) {
|
||||
State status = recordings.get(0).getStatus();
|
||||
if (event.getCode() == KeyCode.DELETE) {
|
||||
if(recordings.size() > 1 || status == FINISHED || status == FAILED || status == WAITING) {
|
||||
if (recordings.size() > 1 || status == FINISHED || status == FAILED || status == WAITING) {
|
||||
delete(recordings);
|
||||
}
|
||||
} else if (event.getCode() == KeyCode.ENTER && status == FINISHED) {
|
||||
|
@ -273,14 +274,14 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR, getTabPane().getScene());
|
||||
autosizeAlert.setTitle("Whoopsie!");
|
||||
autosizeAlert.setHeaderText("Recordings not available");
|
||||
autosizeAlert.setContentText("An error occured while retrieving the list of recordings");
|
||||
autosizeAlert.setContentText("An error occurred while retrieving the list of recordings");
|
||||
autosizeAlert.showAndWait();
|
||||
});
|
||||
}
|
||||
|
||||
private void updateFreeSpaceDisplay() {
|
||||
if(spaceTotal != -1 && spaceFree != -1) {
|
||||
double free = ((double)spaceFree) / spaceTotal;
|
||||
if (spaceTotal != -1 && spaceFree != -1) {
|
||||
double free = ((double) spaceFree) / spaceTotal;
|
||||
spaceLeft.setProgress(free);
|
||||
double totalGiB = ((double) spaceTotal) / 1024 / 1024 / 1024;
|
||||
double freeGiB = ((double) spaceFree) / 1024 / 1024 / 1024;
|
||||
|
@ -299,13 +300,9 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
recordingsLock.lock();
|
||||
try {
|
||||
for (Iterator<JavaFxRecording> iterator = observableRecordings.iterator(); iterator.hasNext();) {
|
||||
JavaFxRecording old = iterator.next();
|
||||
if (!recordings.contains(old)) {
|
||||
// remove deleted recordings
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
// remove deleted recordings
|
||||
observableRecordings.removeIf(old -> !recordings.contains(old));
|
||||
|
||||
for (JavaFxRecording recording : recordings) {
|
||||
if (!observableRecordings.contains(recording)) {
|
||||
// add new recordings
|
||||
|
@ -324,10 +321,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private ScheduledService<List<JavaFxRecording>> createUpdateService() {
|
||||
ScheduledService<List<JavaFxRecording>> service = new ScheduledService<List<JavaFxRecording>>() {
|
||||
ScheduledService<List<JavaFxRecording>> service = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<List<JavaFxRecording>> createTask() {
|
||||
return new Task<List<JavaFxRecording>>() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
public List<JavaFxRecording> call() throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||
updateSpace();
|
||||
|
@ -388,7 +385,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
JavaFxRecording first = recordings.get(0);
|
||||
MenuItem openInPlayer = new MenuItem("Open in Player");
|
||||
openInPlayer.setOnAction(e -> play(first));
|
||||
if(first.getStatus() == FINISHED || Config.getInstance().getSettings().localRecording) {
|
||||
if (first.getStatus() == FINISHED || Config.getInstance().getSettings().localRecording) {
|
||||
contextMenu.getItems().add(openInPlayer);
|
||||
} else if (first.getStatus() == RECORDING && !Config.getInstance().getSettings().localRecording) {
|
||||
openInPlayer.setText("Open live stream");
|
||||
openInPlayer.setOnAction(e -> play(first.getModel()));
|
||||
contextMenu.getItems().add(openInPlayer);
|
||||
}
|
||||
|
||||
|
@ -397,30 +398,33 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
openContactSheet.setDisable(first.getContactSheet().isEmpty());
|
||||
contextMenu.getItems().add(openContactSheet);
|
||||
|
||||
// TODO find a way to reenable this
|
||||
// MenuItem stopRecording = new MenuItem("Stop recording");
|
||||
// stopRecording.setOnAction((e) -> {
|
||||
// Model m = site.createModel(recording.getModelName());
|
||||
// try {
|
||||
// recorder.stopRecording(m);
|
||||
// } catch (Exception e1) {
|
||||
// showErrorDialog("Stop recording", "Couldn't stop recording of model " + m.getName(), e1);
|
||||
// }
|
||||
// });
|
||||
// if(recording.getStatus() == STATUS.RECORDING) {
|
||||
// contextMenu.getItems().add(stopRecording);
|
||||
// }
|
||||
MenuItem stopRecording = new MenuItem("Stop Recording");
|
||||
stopRecording.setOnAction(e -> stopRecording(recordings.stream().map(JavaFxRecording::getModel).collect(Collectors.toList())));
|
||||
if (recordings.stream().anyMatch(r -> r.getStatus() == RECORDING)) {
|
||||
contextMenu.getItems().add(stopRecording);
|
||||
}
|
||||
|
||||
MenuItem pauseRecording = new MenuItem("Pause Recording");
|
||||
pauseRecording.setOnAction(e -> pauseRecording(recordings.stream().map(JavaFxRecording::getModel).collect(Collectors.toList())));
|
||||
if (recordings.stream().anyMatch(r -> r.getStatus() == RECORDING)) {
|
||||
contextMenu.getItems().add(pauseRecording);
|
||||
}
|
||||
|
||||
MenuItem deleteRecording = new MenuItem("Delete");
|
||||
deleteRecording.setOnAction(e -> delete(recordings));
|
||||
if(first.getStatus() == FINISHED || first.getStatus() == WAITING || first.getStatus() == FAILED || recordings.size() > 1) {
|
||||
if (first.getStatus() == FINISHED || first.getStatus() == WAITING || first.getStatus() == FAILED || recordings.size() > 1) {
|
||||
contextMenu.getItems().add(deleteRecording);
|
||||
deleteRecording.setDisable(recordings.stream().allMatch(Recording::isPinned));
|
||||
}
|
||||
|
||||
MenuItem followModels = new MenuItem("Follow Model");
|
||||
followModels.setOnAction(e -> follow(recordings.stream().map(JavaFxRecording::getModel).collect(Collectors.toList())));
|
||||
followModels.setDisable(!recordings.stream().map(JavaFxRecording::getModel).allMatch(m -> m.getSite().supportsFollow() && m.getSite().credentialsAvailable()));
|
||||
contextMenu.getItems().add(followModels);
|
||||
|
||||
MenuItem openDir = new MenuItem("Open directory");
|
||||
openDir.setOnAction(e -> onOpenDirectory(first));
|
||||
if(Config.getInstance().getSettings().localRecording) {
|
||||
if (Config.getInstance().getSettings().localRecording) {
|
||||
contextMenu.getItems().add(openDir);
|
||||
}
|
||||
|
||||
|
@ -449,7 +453,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
contextMenu.getItems().add(rerunPostProcessing);
|
||||
rerunPostProcessing.setDisable(!recordings.stream().allMatch(Recording::canBePostProcessed));
|
||||
|
||||
if(recordings.size() > 1) {
|
||||
if (recordings.size() > 1) {
|
||||
openInPlayer.setDisable(true);
|
||||
openDir.setDisable(true);
|
||||
}
|
||||
|
@ -457,6 +461,18 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
return contextMenu;
|
||||
}
|
||||
|
||||
private void follow(List<Model> selectedModels) {
|
||||
new FollowAction(getTabPane(), selectedModels).execute();
|
||||
}
|
||||
|
||||
private void stopRecording(List<Model> selectedModels) {
|
||||
new StopRecordingAction(getTabPane(), selectedModels, recorder).execute();
|
||||
}
|
||||
|
||||
private void pauseRecording(List<Model> selectedModels) {
|
||||
new PauseAction(getTabPane(), selectedModels, recorder).execute();
|
||||
}
|
||||
|
||||
private void openContactSheet(JavaFxRecording recording) {
|
||||
if (config.getSettings().localRecording) {
|
||||
recording.getContactSheet().ifPresent(f -> new Thread(() -> DesktopIntegration.open(f)).start());
|
||||
|
@ -466,7 +482,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
try {
|
||||
target = File.createTempFile("cs_", ".jpg");
|
||||
target.deleteOnExit();
|
||||
FileDownload download = new FileDownload(CamrecApplication.httpClient, (p) -> {
|
||||
FileDownload download = new FileDownload(CamrecApplication.httpClient, p -> {
|
||||
if (p == 100) {
|
||||
DesktopIntegration.open(target);
|
||||
}
|
||||
|
@ -487,7 +503,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
Node source = getTabPane();
|
||||
String notes = recording.getNote();
|
||||
Optional<String> newNote = Dialogs.showTextInput(source.getScene(), "Recording Notes", "", notes);
|
||||
if(newNote.isPresent()) {
|
||||
if (newNote.isPresent()) {
|
||||
table.setCursor(Cursor.WAIT);
|
||||
Thread backgroundThread = new Thread(() -> {
|
||||
List<Exception> exceptions = new ArrayList<>();
|
||||
|
@ -562,7 +578,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private void jumpToNextModel(KeyCode code) {
|
||||
if (!table.getItems().isEmpty() && (code.isLetterKey() || code.isDigitKey())) {
|
||||
try {
|
||||
ensureTableIsNotEmpty();
|
||||
ensureKeyCodeIsLetterOrDigit(code);
|
||||
|
||||
// determine where to start looking for the next model
|
||||
int startAt = 0;
|
||||
if (table.getSelectionModel().getSelectedIndex() >= 0) {
|
||||
|
@ -587,6 +606,20 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
i = 0;
|
||||
}
|
||||
} while (i != startAt);
|
||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||
// GUI was not in the state to process the user input
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureKeyCodeIsLetterOrDigit(KeyCode code) {
|
||||
if (!(code.isLetterKey() || code.isDigitKey())) {
|
||||
throw new IllegalArgumentException("keycode not allowed");
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureTableIsNotEmpty() {
|
||||
if (table.getItems().isEmpty()) {
|
||||
throw new IllegalStateException("table is empty");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -630,13 +663,12 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private String proposeTargetFilename(Recording recording) {
|
||||
if(recording.isSingleFile()) {
|
||||
if (recording.isSingleFile()) {
|
||||
return recording.getPostProcessedFile().getName();
|
||||
} else {
|
||||
String downloadFilename = config.getSettings().downloadFilename;
|
||||
String fileSuffix = config.getSettings().ffmpegFileSuffix;
|
||||
String filename = new DownloadPostprocessor().fillInPlaceHolders(downloadFilename, recording, config) + '.' + fileSuffix;
|
||||
return filename;
|
||||
return new DownloadPostprocessor().fillInPlaceHolders(downloadFilename, recording, config) + '.' + fileSuffix;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -665,7 +697,8 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
Platform.runLater(() -> {
|
||||
recording.setStatus(FINISHED);
|
||||
recording.setProgress(-1);
|
||||
RecordingStateChangedEvent evt = new RecordingStateChangedEvent(target, recording.getStatus(), recording.getModel(), recording.getStartDate());
|
||||
RecordingStateChangedEvent evt = new RecordingStateChangedEvent(target, recording.getStatus(), recording.getModel(),
|
||||
recording.getStartDate());
|
||||
EventBusHolder.BUS.post(evt);
|
||||
});
|
||||
}
|
||||
|
@ -697,7 +730,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
AutosizeAlert autosizeAlert = new AutosizeAlert(AlertType.ERROR, getTabPane().getScene());
|
||||
autosizeAlert.setTitle(title);
|
||||
autosizeAlert.setHeaderText(msg);
|
||||
StringBuilder contentText = new StringBuilder("On or more error(s) occured:");
|
||||
StringBuilder contentText = new StringBuilder("On or more error(s) occurred:");
|
||||
for (Exception exception : exceptions) {
|
||||
contentText.append("\n• ").append(exception.getLocalizedMessage());
|
||||
}
|
||||
|
@ -707,21 +740,22 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
private void play(Recording recording) {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean started = Player.play(recording);
|
||||
if(started && Config.getInstance().getSettings().showPlayerStarting) {
|
||||
Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500));
|
||||
}
|
||||
new Thread(() -> {
|
||||
boolean started = Player.play(recording);
|
||||
if (started && Config.getInstance().getSettings().showPlayerStarting) {
|
||||
Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500));
|
||||
}
|
||||
}.start();
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void play(Model model) {
|
||||
new PlayAction(table, model).execute();
|
||||
}
|
||||
|
||||
private void delete(List<JavaFxRecording> recordings) {
|
||||
table.setCursor(Cursor.WAIT);
|
||||
String msg;
|
||||
if(recordings.size() > 1) {
|
||||
if (recordings.size() > 1) {
|
||||
msg = "Delete " + recordings.size() + " recordings for good?";
|
||||
} else {
|
||||
Recording r = recordings.get(0);
|
||||
|
@ -745,8 +779,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
try {
|
||||
List<Recording> deleted = new ArrayList<>();
|
||||
List<Exception> exceptions = new ArrayList<>();
|
||||
for (Iterator<JavaFxRecording> iterator = recordings.iterator(); iterator.hasNext();) {
|
||||
JavaFxRecording r = iterator.next();
|
||||
for (JavaFxRecording r : recordings) {
|
||||
if (r.getStatus() != FINISHED && r.getStatus() != FAILED && r.getStatus() != WAITING) {
|
||||
continue;
|
||||
}
|
||||
|
@ -771,7 +804,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
public void saveState() {
|
||||
if(!table.getSortOrder().isEmpty()) {
|
||||
if (!table.getSortOrder().isEmpty()) {
|
||||
TableColumn<JavaFxRecording, ?> col = table.getSortOrder().get(0);
|
||||
Config.getInstance().getSettings().recordingsSortColumn = col.getText();
|
||||
Config.getInstance().getSettings().recordingsSortType = col.getSortType().toString();
|
||||
|
@ -795,9 +828,9 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
private void restoreSorting() {
|
||||
String sortCol = Config.getInstance().getSettings().recordingsSortColumn;
|
||||
if(StringUtil.isNotBlank(sortCol)) {
|
||||
if (StringUtil.isNotBlank(sortCol)) {
|
||||
for (TableColumn<JavaFxRecording, ?> col : table.getColumns()) {
|
||||
if(Objects.equals(sortCol, col.getText())) {
|
||||
if (Objects.equals(sortCol, col.getText())) {
|
||||
col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordingsSortType));
|
||||
table.getSortOrder().clear();
|
||||
table.getSortOrder().add(col);
|
||||
|
@ -809,10 +842,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
private void restoreColumnOrder() {
|
||||
String[] columnIds = Config.getInstance().getSettings().recordingsColumnIds;
|
||||
ObservableList<TableColumn<JavaFxRecording,?>> columns = table.getColumns();
|
||||
ObservableList<TableColumn<JavaFxRecording, ?>> columns = table.getColumns();
|
||||
for (int i = 0; i < columnIds.length; i++) {
|
||||
for (int j = 0; j < table.getColumns().size(); j++) {
|
||||
if(Objects.equals(columnIds[i], columns.get(j).getId())) {
|
||||
if (Objects.equals(columnIds[i], columns.get(j).getId())) {
|
||||
TableColumn<JavaFxRecording, ?> col = columns.get(j);
|
||||
columns.remove(j); // NOSONAR
|
||||
columns.add(i, col);
|
||||
|
@ -823,7 +856,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
private void restoreColumnWidths() {
|
||||
double[] columnWidths = Config.getInstance().getSettings().recordingsColumnWidths;
|
||||
if(columnWidths != null && columnWidths.length == table.getColumns().size()) {
|
||||
if (columnWidths != null && columnWidths.length == table.getColumns().size()) {
|
||||
for (int i = 0; i < columnWidths.length; i++) {
|
||||
table.getColumns().get(i).setPrefWidth(columnWidths[i]);
|
||||
}
|
||||
|
|
|
@ -1,25 +1,8 @@
|
|||
package ctbrec.ui.tabs;
|
||||
|
||||
import static ctbrec.Model.State.*;
|
||||
import static ctbrec.io.HttpConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.Model.State;
|
||||
|
@ -27,10 +10,11 @@ import ctbrec.io.HttpException;
|
|||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.ui.AutosizeAlert;
|
||||
import ctbrec.ui.CamrecApplication;
|
||||
import ctbrec.ui.PauseIndicator;
|
||||
import ctbrec.ui.SiteUiFactory;
|
||||
import ctbrec.ui.StreamSourceSelectionDialog;
|
||||
import ctbrec.ui.action.PlayAction;
|
||||
import ctbrec.ui.controls.Dialogs;
|
||||
import ctbrec.ui.controls.PausedIndicator;
|
||||
import ctbrec.ui.controls.StreamPreview;
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.animation.FillTransition;
|
||||
|
@ -39,7 +23,6 @@ import javafx.animation.Transition;
|
|||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
|
@ -48,6 +31,7 @@ import javafx.scene.Cursor;
|
|||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
@ -63,6 +47,18 @@ import javafx.scene.text.TextAlignment;
|
|||
import javafx.util.Duration;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static ctbrec.Model.State.OFFLINE;
|
||||
import static ctbrec.Model.State.ONLINE;
|
||||
import static ctbrec.io.HttpConstants.*;
|
||||
|
||||
public class ThumbCell extends StackPane {
|
||||
|
||||
|
@ -85,7 +81,7 @@ public class ThumbCell extends StackPane {
|
|||
private Text resolutionTag;
|
||||
private Recorder recorder;
|
||||
private Circle recordingIndicator;
|
||||
private PauseIndicator pausedIndicator;
|
||||
private PausedIndicator pausedIndicator;
|
||||
private int index = 0;
|
||||
ContextMenu popup;
|
||||
private static final Color colorNormal = Color.BLACK;
|
||||
|
@ -177,12 +173,16 @@ public class ThumbCell extends StackPane {
|
|||
|
||||
recordingIndicator = new Circle(8);
|
||||
recordingIndicator.setFill(colorRecording);
|
||||
recordingIndicator.setCursor(Cursor.HAND);
|
||||
recordingIndicator.setOnMouseClicked(e -> pauseResumeAction(true));
|
||||
Tooltip tooltip = new Tooltip("Pause Recording");
|
||||
Tooltip.install(recordingIndicator, tooltip);
|
||||
StackPane.setMargin(recordingIndicator, new Insets(3));
|
||||
StackPane.setAlignment(recordingIndicator, Pos.TOP_LEFT);
|
||||
getChildren().add(recordingIndicator);
|
||||
|
||||
pausedIndicator = new PauseIndicator(colorRecording, 16);
|
||||
pausedIndicator.setVisible(false);
|
||||
pausedIndicator = new PausedIndicator(16, colorRecording);
|
||||
pausedIndicator.setOnMouseClicked(e -> pauseResumeAction(false));
|
||||
StackPane.setMargin(pausedIndicator, new Insets(3));
|
||||
StackPane.setAlignment(pausedIndicator, Pos.TOP_LEFT);
|
||||
getChildren().add(pausedIndicator);
|
||||
|
@ -236,7 +236,7 @@ public class ThumbCell extends StackPane {
|
|||
play.setStyle("-fx-background-color: black;");
|
||||
previewTrigger.getChildren().add(play);
|
||||
|
||||
Circle clip = new Circle(s / 2);
|
||||
Circle clip = new Circle(s / 2.0);
|
||||
clip.setTranslateX(clip.getRadius());
|
||||
clip.setTranslateY(clip.getRadius());
|
||||
previewTrigger.setClip(clip);
|
||||
|
@ -287,8 +287,7 @@ public class ThumbCell extends StackPane {
|
|||
resolution = resolutionCache.get(model);
|
||||
resolutionBackgroundColor = resolutionOnlineColor;
|
||||
final int w = resolution[1];
|
||||
String width = w != Integer.MAX_VALUE ? Integer.toString(w) : "HD";
|
||||
tagText = width;
|
||||
tagText = w != Integer.MAX_VALUE ? Integer.toString(w) : "HD";
|
||||
if (w == 0) {
|
||||
State state = model.getOnlineState(false);
|
||||
tagText = state.name();
|
||||
|
@ -364,7 +363,7 @@ public class ThumbCell extends StackPane {
|
|||
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
|
||||
});
|
||||
} else {
|
||||
img.progressProperty().addListener((ChangeListener<Number>) (observable, oldValue, newValue) -> {
|
||||
img.progressProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue.doubleValue() == 1.0) {
|
||||
iv.setImage(img);
|
||||
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
|
||||
|
@ -403,13 +402,13 @@ public class ThumbCell extends StackPane {
|
|||
|
||||
private void setRecording(boolean recording) {
|
||||
this.recording = recording;
|
||||
Color c;
|
||||
if (recording) {
|
||||
Color c = mouseHovering ? colorHighlight : colorRecording;
|
||||
nameBackground.setFill(c);
|
||||
c = mouseHovering ? colorHighlight : colorRecording;
|
||||
} else {
|
||||
Color c = mouseHovering ? colorHighlight : colorNormal;
|
||||
nameBackground.setFill(c);
|
||||
c = mouseHovering ? colorHighlight : colorNormal;
|
||||
}
|
||||
nameBackground.setFill(c);
|
||||
|
||||
updateRecordingIndicator();
|
||||
}
|
||||
|
@ -430,7 +429,7 @@ public class ThumbCell extends StackPane {
|
|||
boolean selectSource = Config.getInstance().getSettings().chooseStreamQuality;
|
||||
if (selectSource && start) {
|
||||
Function<Model, Void> onSuccess = modl -> {
|
||||
startStopActionAsync(modl, start);
|
||||
startStopActionAsync(modl, true);
|
||||
return null;
|
||||
};
|
||||
Function<Throwable, Void> onFail = throwable -> {
|
||||
|
@ -484,13 +483,7 @@ public class ThumbCell extends StackPane {
|
|||
}
|
||||
} catch (Exception e1) {
|
||||
LOG.error(COULDNT_START_STOP_RECORDING, e1);
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getScene());
|
||||
alert.setTitle(ERROR);
|
||||
alert.setHeaderText(COULDNT_START_STOP_RECORDING);
|
||||
alert.setContentText("I/O error while starting/stopping the recording: " + e1.getLocalizedMessage());
|
||||
alert.showAndWait();
|
||||
});
|
||||
Dialogs.showError(getScene(), COULDNT_START_STOP_RECORDING, "I/O error while starting/stopping the recording: ", e1);
|
||||
} finally {
|
||||
setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
|
@ -507,13 +500,7 @@ public class ThumbCell extends StackPane {
|
|||
if (followed) {
|
||||
return true;
|
||||
} else {
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getScene());
|
||||
alert.setTitle(ERROR);
|
||||
alert.setHeaderText("Couldn't follow model");
|
||||
alert.setContentText("");
|
||||
alert.showAndWait();
|
||||
});
|
||||
Dialogs.showError(getScene(), "Couldn't follow model", "", null);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
|
@ -523,25 +510,14 @@ public class ThumbCell extends StackPane {
|
|||
Platform.runLater(() -> thumbCellList.remove(ThumbCell.this));
|
||||
return true;
|
||||
} else {
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getScene());
|
||||
alert.setTitle(ERROR);
|
||||
alert.setHeaderText("Couldn't unfollow model");
|
||||
alert.setContentText("");
|
||||
alert.showAndWait();
|
||||
});
|
||||
Dialogs.showError(getScene(), "Couldn't unfollow model", "", null);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (Exception e1) {
|
||||
LOG.error("Couldn't follow/unfollow model {}", model.getName(), e1);
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getScene());
|
||||
alert.setTitle(ERROR);
|
||||
alert.setHeaderText("Couldn't follow/unfollow model");
|
||||
alert.setContentText("I/O error while following/unfollowing model " + model.getName() + ": " + e1.getLocalizedMessage());
|
||||
alert.showAndWait();
|
||||
});
|
||||
String msg = "I/O error while following/unfollowing model " + model.getName() + ": ";
|
||||
Dialogs.showError(getScene(), "Couldn't follow/unfollow model", msg, e1);
|
||||
return false;
|
||||
} finally {
|
||||
setCursor(Cursor.DEFAULT);
|
||||
|
@ -616,8 +592,8 @@ public class ThumbCell extends StackPane {
|
|||
public void setThumbWidth(int width) {
|
||||
int height = (int) (width * imgAspectRatio);
|
||||
setSize(width, height);
|
||||
iv.prefHeight(height);
|
||||
iv.prefWidth(width);
|
||||
iv.prefHeight(width);
|
||||
iv.prefWidth(height);
|
||||
}
|
||||
|
||||
private void setSize(int w, int h) {
|
||||
|
@ -638,8 +614,8 @@ public class ThumbCell extends StackPane {
|
|||
topic.prefHeight(getHeight() - 25);
|
||||
topic.maxHeight(getHeight() - 25);
|
||||
int margin = 4;
|
||||
topic.maxWidth(w - margin * 2);
|
||||
topic.setWrappingWidth(w - margin * 2);
|
||||
topic.maxWidth(w - margin * 2.0);
|
||||
topic.setWrappingWidth(w - margin * 2.0);
|
||||
|
||||
streamPreview.resizeTo(w, h);
|
||||
|
||||
|
|
|
@ -1,31 +1,5 @@
|
|||
package ctbrec.ui.tabs;
|
||||
|
||||
import static ctbrec.ui.controls.Dialogs.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.event.EventBusHolder;
|
||||
|
@ -33,21 +7,13 @@ import ctbrec.recorder.Recorder;
|
|||
import ctbrec.sites.Site;
|
||||
import ctbrec.sites.mfc.MyFreeCamsClient;
|
||||
import ctbrec.sites.mfc.MyFreeCamsModel;
|
||||
import ctbrec.ui.AutosizeAlert;
|
||||
import ctbrec.ui.DesktopIntegration;
|
||||
import ctbrec.ui.SiteUiFactory;
|
||||
import ctbrec.ui.TipDialog;
|
||||
import ctbrec.ui.TokenLabel;
|
||||
import ctbrec.ui.*;
|
||||
import ctbrec.ui.action.OpenRecordingsDir;
|
||||
import ctbrec.ui.controls.FasterVerticalScrollPaneSkin;
|
||||
import ctbrec.ui.controls.SearchBox;
|
||||
import ctbrec.ui.controls.SearchPopover;
|
||||
import ctbrec.ui.controls.SearchPopoverTreeList;
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.ParallelTransition;
|
||||
import javafx.animation.ScaleTransition;
|
||||
import javafx.animation.Transition;
|
||||
import javafx.animation.TranslateTransition;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
@ -57,38 +23,30 @@ import javafx.collections.ObservableList;
|
|||
import javafx.concurrent.Task;
|
||||
import javafx.concurrent.Worker.State;
|
||||
import javafx.concurrent.WorkerStateEvent;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.transform.Transform;
|
||||
import javafx.util.Duration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static ctbrec.ui.controls.Dialogs.showError;
|
||||
|
||||
public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ThumbOverviewTab.class);
|
||||
|
@ -106,7 +64,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
private String filter;
|
||||
ReentrantLock gridLock = new ReentrantLock();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
boolean loginRequired;
|
||||
TextField pageInput = new TextField(Integer.toString(1));
|
||||
Button pageFirst = new Button("1");
|
||||
Button pagePrev = new Button("◀");
|
||||
|
@ -117,9 +74,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
StackPane root = new StackPane();
|
||||
Task<List<Model>> searchTask;
|
||||
SearchPopover popover;
|
||||
SearchPopoverTreeList popoverTreelist = new SearchPopoverTreeList();
|
||||
SearchPopoverTreeList popoverTreeList = new SearchPopoverTreeList();
|
||||
double imageAspectRatio = 3.0 / 4.0;
|
||||
private SimpleBooleanProperty preserveAspectRatio = new SimpleBooleanProperty(true);
|
||||
private final SimpleBooleanProperty preserveAspectRatio = new SimpleBooleanProperty(true);
|
||||
|
||||
private ComboBox<Integer> thumbWidth;
|
||||
|
||||
|
@ -172,7 +129,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
popover.maxHeightProperty().bind(popover.minHeightProperty());
|
||||
popover.prefHeightProperty().bind(popover.minHeightProperty());
|
||||
popover.setMinHeight(450);
|
||||
popover.pushPage(popoverTreelist);
|
||||
popover.pushPage(popoverTreeList);
|
||||
StackPane.setAlignment(popover, Pos.TOP_RIGHT);
|
||||
StackPane.setMargin(popover, new Insets(35, 50, 0, 0));
|
||||
|
||||
|
@ -196,6 +153,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
scrollPane.setContent(grid);
|
||||
scrollPane.setFitToHeight(true);
|
||||
scrollPane.setFitToWidth(true);
|
||||
FasterVerticalScrollPaneSkin scrollPaneSkin = new FasterVerticalScrollPaneSkin(scrollPane);
|
||||
scrollPane.setSkin(scrollPaneSkin);
|
||||
BorderPane.setMargin(scrollPane, new Insets(5));
|
||||
|
||||
pagination = new HBox(5);
|
||||
|
@ -205,29 +164,13 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
pagination.getChildren().add(pageInput);
|
||||
BorderPane.setMargin(pagination, new Insets(5));
|
||||
pageInput.setPrefWidth(50);
|
||||
pageInput.setOnAction(e -> handlePageNumberInput());
|
||||
pageInput.setOnAction(this::handlePageNumberInput);
|
||||
pageFirst.setTooltip(new Tooltip("First Page"));
|
||||
pageFirst.setOnAction(e -> {
|
||||
pageInput.setText(Integer.toString(1));
|
||||
updateService.setPage(1);
|
||||
restartUpdateService();
|
||||
});
|
||||
pageFirst.setOnAction(e -> changePageTo(1));
|
||||
pagePrev.setTooltip(new Tooltip("Previous Page"));
|
||||
pagePrev.setOnAction(e -> {
|
||||
int page = updateService.getPage();
|
||||
page = Math.max(1, --page);
|
||||
pageInput.setText(Integer.toString(page));
|
||||
updateService.setPage(page);
|
||||
restartUpdateService();
|
||||
});
|
||||
pagePrev.setOnAction(e -> previousPage());
|
||||
pageNext.setTooltip(new Tooltip("Next Page"));
|
||||
pageNext.setOnAction(e -> {
|
||||
int page = updateService.getPage();
|
||||
page++;
|
||||
pageInput.setText(Integer.toString(page));
|
||||
updateService.setPage(page);
|
||||
restartUpdateService();
|
||||
});
|
||||
pageNext.setOnAction(e -> nextPage());
|
||||
|
||||
HBox thumbSizeSelector = new HBox(5);
|
||||
Label l = new Label("Thumb Size");
|
||||
|
@ -242,8 +185,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
thumbWidth = new ComboBox<>(FXCollections.observableList(thumbWidths));
|
||||
thumbWidth.getSelectionModel().select(Integer.valueOf(Config.getInstance().getSettings().thumbWidth));
|
||||
thumbWidth.setOnAction(e -> {
|
||||
int width = thumbWidth.getSelectionModel().getSelectedItem();
|
||||
Config.getInstance().getSettings().thumbWidth = width;
|
||||
Config.getInstance().getSettings().thumbWidth = thumbWidth.getSelectionModel().getSelectedItem();
|
||||
updateThumbSize();
|
||||
});
|
||||
thumbSizeSelector.getChildren().add(thumbWidth);
|
||||
|
@ -263,6 +205,35 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
root.getChildren().add(borderPane);
|
||||
root.getChildren().add(popover);
|
||||
setContent(root);
|
||||
|
||||
scrollPane.setOnKeyReleased(event -> {
|
||||
if (event.getCode() == KeyCode.RIGHT) {
|
||||
nextPage();
|
||||
} else if (event.getCode() == KeyCode.LEFT) {
|
||||
previousPage();
|
||||
} else if (event.getCode().getCode() >= KeyCode.DIGIT1.getCode() && event.getCode().getCode() <= KeyCode.DIGIT9.getCode()) {
|
||||
changePageTo(event.getCode().getCode() - 48);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void nextPage() {
|
||||
int page = updateService.getPage();
|
||||
page++;
|
||||
changePageTo(page);
|
||||
}
|
||||
|
||||
|
||||
private void previousPage() {
|
||||
int page = updateService.getPage();
|
||||
page = Math.max(1, --page);
|
||||
changePageTo(page);
|
||||
}
|
||||
|
||||
private void changePageTo(int page) {
|
||||
pageInput.setText(Integer.toString(page));
|
||||
updateService.setPage(page);
|
||||
restartUpdateService();
|
||||
}
|
||||
|
||||
private ChangeListener<? super String> search() {
|
||||
|
@ -273,7 +244,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
if(newValue.length() < 2) {
|
||||
return;
|
||||
}
|
||||
searchTask = new ThumbOverviewTabSearchTask(site, popover, popoverTreelist, newValue);
|
||||
searchTask = new ThumbOverviewTabSearchTask(site, popover, popoverTreeList, newValue);
|
||||
new Thread(searchTask).start();
|
||||
};
|
||||
}
|
||||
|
@ -292,12 +263,11 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
}
|
||||
|
||||
private void handlePageNumberInput() {
|
||||
private void handlePageNumberInput(ActionEvent event) {
|
||||
try {
|
||||
int page = Integer.parseInt(pageInput.getText());
|
||||
page = Math.max(1, page);
|
||||
updateService.setPage(page);
|
||||
restartUpdateService();
|
||||
changePageTo(page);
|
||||
} catch(NumberFormatException e) {
|
||||
// noop
|
||||
} finally {
|
||||
|
@ -426,11 +396,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
if(popup != null) {
|
||||
popup.hide();
|
||||
popup = null;
|
||||
return;
|
||||
}
|
||||
});
|
||||
newCell.selectionProperty().addListener((obs, oldValue, newValue) -> {
|
||||
if(newValue.booleanValue()) {
|
||||
if (Boolean.TRUE.equals(newValue)) {
|
||||
selectedThumbCells.add(newCell);
|
||||
} else {
|
||||
selectedThumbCells.remove(newCell);
|
||||
|
@ -555,10 +524,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
event.put("amount", tokens.doubleValue());
|
||||
EventBusHolder.BUS.post(event);
|
||||
} catch (IOException ex) {
|
||||
LOG.error("An error occured while sending tip", ex);
|
||||
showError("Couldn't send tip", "An error occured while sending tip:", ex);
|
||||
LOG.error("An error occurred while sending tip", ex);
|
||||
showError(getTabPane().getScene(), "Couldn't send tip", "An error occurred while sending tip:", ex);
|
||||
} catch (Exception ex) {
|
||||
showError("Couldn't send tip", "You entered an invalid amount of tokens", ex);
|
||||
showError(getTabPane().getScene(), "Couldn't send tip", "You entered an invalid amount of tokens", ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -577,7 +546,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
protected void follow(List<ThumbCell> selection, boolean follow) {
|
||||
for (ThumbCell thumbCell : selection) {
|
||||
thumbCell.follow(follow).thenAccept(success -> {
|
||||
if(follow && success.booleanValue()) {
|
||||
if (follow && Boolean.TRUE.equals(success)) {
|
||||
showAddToFollowedAnimation(thumbCell);
|
||||
}
|
||||
});
|
||||
|
@ -625,35 +594,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
ParallelTransition pt = new ParallelTransition(translate, scale);
|
||||
pt.play();
|
||||
pt.setOnFinished(evt -> root.getChildren().remove(iv));
|
||||
|
||||
String normalStyle = followedTab.getStyle();
|
||||
Color normal = Color.web("#f4f4f4");
|
||||
Color highlight = Color.web("#2b8513");
|
||||
Transition blink = new Transition() {
|
||||
{
|
||||
setCycleDuration(Duration.millis(500));
|
||||
}
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
double rh = highlight.getRed();
|
||||
double rn = normal.getRed();
|
||||
double diff = rh - rn;
|
||||
double r = (rn + diff * frac) * 255;
|
||||
double gh = highlight.getGreen();
|
||||
double gn = normal.getGreen();
|
||||
diff = gh - gn;
|
||||
double g = (gn + diff * frac) * 255;
|
||||
double bh = highlight.getBlue();
|
||||
double bn = normal.getBlue();
|
||||
diff = bh - bn;
|
||||
double b = (bn + diff * frac) * 255;
|
||||
String style = "-fx-background-color: rgb(" + r + "," + g + "," + b + ")";
|
||||
followedTab.setStyle(style);
|
||||
}
|
||||
};
|
||||
blink.setCycleCount(6);
|
||||
blink.setAutoReverse(true);
|
||||
blink.setOnFinished(evt -> followedTab.setStyle(normalStyle));
|
||||
FollowTabBlinkTransition blink = new FollowTabBlinkTransition(followedTab);
|
||||
blink.play();
|
||||
});
|
||||
}
|
||||
|
@ -692,13 +633,13 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
}
|
||||
|
||||
private EventHandler<MouseEvent> mouseClickListener = e -> {
|
||||
private final EventHandler<MouseEvent> mouseClickListener = e -> {
|
||||
ThumbCell cell = (ThumbCell) e.getSource();
|
||||
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
|
||||
cell.setSelected(false);
|
||||
cell.startPlayer();
|
||||
} else if (e.getButton() == MouseButton.PRIMARY && e.isControlDown()) {
|
||||
if(popup == null) {
|
||||
if (popup == null) {
|
||||
cell.setSelected(!cell.isSelected());
|
||||
}
|
||||
} else if (e.getButton() == MouseButton.PRIMARY) {
|
||||
|
@ -728,12 +669,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
|
||||
void filter() {
|
||||
Collections.sort(filteredThumbCells, (o1, o2) -> {
|
||||
ThumbCell c1 = o1;
|
||||
ThumbCell c2 = o2;
|
||||
|
||||
if(c1.getIndex() < c2.getIndex()) return -1;
|
||||
if(c1.getIndex() > c2.getIndex()) return 1;
|
||||
filteredThumbCells.sort((c1, c2) -> {
|
||||
if (c1.getIndex() < c2.getIndex()) return -1;
|
||||
if (c1.getIndex() > c2.getIndex()) return 1;
|
||||
return c1.getModel().getName().compareTo(c2.getModel().getName());
|
||||
});
|
||||
|
||||
|
@ -839,7 +777,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
return !tokensMissing;
|
||||
}
|
||||
|
||||
private String createSearchText(Model m) throws ExecutionException {
|
||||
private String createSearchText(Model m) {
|
||||
StringBuilder searchTextBuilder = new StringBuilder(m.getName());
|
||||
searchTextBuilder.append(' ');
|
||||
searchTextBuilder.append(m.getDisplayName());
|
||||
|
@ -856,7 +794,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
|
||||
public void setRecorder(Recorder recorder) {
|
||||
this.recorder = recorder;
|
||||
popoverTreelist.setRecorder(recorder);
|
||||
popoverTreeList.setRecorder(recorder);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
package ctbrec.ui.tabs;
|
||||
|
||||
import static ctbrec.ui.controls.Dialogs.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.sites.Site;
|
||||
import ctbrec.ui.SiteUiFactory;
|
||||
|
@ -15,20 +7,27 @@ import ctbrec.ui.controls.SearchPopover;
|
|||
import ctbrec.ui.controls.SearchPopoverTreeList;
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static ctbrec.ui.controls.Dialogs.showError;
|
||||
|
||||
public class ThumbOverviewTabSearchTask extends Task<List<Model>> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ThumbOverviewTabSearchTask.class);
|
||||
|
||||
private Site site;
|
||||
private SearchPopover popover;
|
||||
private SearchPopoverTreeList popoverTreelist;
|
||||
private String query;
|
||||
private final Site site;
|
||||
private final SearchPopover popover;
|
||||
private final SearchPopoverTreeList popoverTreeList;
|
||||
private final String query;
|
||||
|
||||
public ThumbOverviewTabSearchTask(Site site, SearchPopover popover, SearchPopoverTreeList popoverTreelist, String query) {
|
||||
public ThumbOverviewTabSearchTask(Site site, SearchPopover popover, SearchPopoverTreeList popoverTreeList, String query) {
|
||||
this.site = site;
|
||||
this.popover = popover;
|
||||
this.popoverTreelist = popoverTreelist;
|
||||
this.popoverTreeList = popoverTreeList;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
|
@ -39,10 +38,10 @@ public class ThumbOverviewTabSearchTask extends Task<List<Model>> {
|
|||
try {
|
||||
loggedin = SiteUiFactory.getUi(site).login();
|
||||
} catch (IOException e) {
|
||||
loggedin = false;
|
||||
// nothing to do
|
||||
}
|
||||
if(!loggedin) {
|
||||
showError("Login failed", "Search won't work correctly without login", null);
|
||||
showError(popover.getScene(), "Login failed", "Search won't work correctly without login", null);
|
||||
}
|
||||
}
|
||||
return site.search(query);
|
||||
|
@ -61,9 +60,9 @@ public class ThumbOverviewTabSearchTask extends Task<List<Model>> {
|
|||
if(models.isEmpty()) {
|
||||
popover.hide();
|
||||
} else {
|
||||
popoverTreelist.getItems().clear();
|
||||
popoverTreeList.getItems().clear();
|
||||
for (Model model : getValue()) {
|
||||
popoverTreelist.getItems().add(model);
|
||||
popoverTreeList.getItems().add(model);
|
||||
}
|
||||
popover.show();
|
||||
}
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
package ctbrec.ui.tabs;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.ui.CamrecApplication;
|
||||
import ctbrec.ui.CamrecApplication.Release;
|
||||
|
@ -18,12 +15,14 @@ import javafx.scene.layout.Priority;
|
|||
import javafx.scene.layout.VBox;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class UpdateTab extends Tab {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UpdateTab.class);
|
||||
|
||||
private TextArea changelog;
|
||||
private final TextArea changelog;
|
||||
|
||||
public UpdateTab(Release latest) {
|
||||
setText("Update Available");
|
||||
|
@ -52,7 +51,7 @@ public class UpdateTab extends Tab {
|
|||
}
|
||||
} catch (Exception e1) {
|
||||
LOG.error("Couldn't download the changelog", e1);
|
||||
Dialogs.showError("Communication error", "Couldn't download the changelog", e1);
|
||||
Dialogs.showError(getTabPane().getScene(), "Communication error", "Couldn't download the changelog", e1);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
|
|
@ -90,9 +90,13 @@ public class LoggingTab extends Tab {
|
|||
|
||||
TableColumn<LoggingEvent, String> location = createTableColumn("Location", 250, idx++);
|
||||
location.setCellValueFactory(cdf -> {
|
||||
StackTraceElement loc = cdf.getValue().getCallerData()[0];
|
||||
String l = loc.getFileName() + ":" + loc.getLineNumber();
|
||||
return new SimpleStringProperty(l);
|
||||
if(cdf.getValue().getCallerData().length > 0) {
|
||||
StackTraceElement loc = cdf.getValue().getCallerData()[0];
|
||||
String l = loc.getFileName() + ":" + loc.getLineNumber();
|
||||
return new SimpleStringProperty(l);
|
||||
} else {
|
||||
return new SimpleStringProperty("");
|
||||
}
|
||||
});
|
||||
table.getColumns().add(location);
|
||||
|
||||
|
|
|
@ -53,16 +53,21 @@ the port ctbrec tries to connect to, if it is run in remote mode.
|
|||
|
||||
- **onlineCheckSkipsPausedModels** - [`true`,`false`] Skip the online check for paused models. If you have many models in the recording list, this can reduce the delay when a recording starts after a model came online.
|
||||
|
||||
- **postProcessing** - Absolute path to a script, which is executed once a recording is finished. See [Post-Processing](/docs/PostProcessing.md).
|
||||
- **postProcessing** - **Deprecated. See [Post-Processing](/docs/PostProcessing.md)** Absolute path to a script, which is executed once a recording is finished.
|
||||
|
||||
- **recordingsDir** - Where ctbrec saves the recordings.
|
||||
|
||||
- **recordingsDirStructure** (server only) - [`FLAT`, `ONE_PER_MODEL`, `ONE_PER_RECORDING`] How recordings are stored in the file system. `FLAT` - all recordings in one directory; `ONE_PER_MODEL` - one directory per model; `ONE_PER_RECORDING` - each recordings ends up in its own directory. Change this only, if you have `recordSingleFile` set to `true`
|
||||
|
||||
- **recordSingleFile** (server only) - [`true`,`false`] - How recordings are stored in the file system. `true` means, each recording is saved in one large file. `false` means, ctbrec just downloads the stream segments.
|
||||
- **recordSingleFile** (server only) - [`true`,`false`] - How recordings are stored in the file system. `true` means, each recording is saved in one large file. `false` means, ctbrec just downloads the stream segments.
|
||||
|
||||
- **splitRecordings** - [0 - 2147483647] in seconds. Split recordings after this amount of seconds. The recordings are split up into several individual recordings,
|
||||
which have the defined length (roughly). 0 means no splitting. The server does not support splitRecordings.
|
||||
- **splitStrategy** - [`DONT`, `TIME`, `SIZE`, `TIME_OR_SIZE`] Defines if and how to split recordings. Also see `splitRecordingsAfterSecs` and `splitRecordingsBiggerThanBytes`
|
||||
|
||||
- **splitRecordingsAfterSecs** - [0 - 2147483647] in seconds. Split recordings after this amount of seconds. The recordings are split up into several individual recordings,
|
||||
which have the defined length (roughly). Has to be activated with `splitStrategy`.
|
||||
|
||||
- **splitRecordingsBiggerThanBytes** - [0 - 9223372036854775807] in bytes. Split recordings, if the size on disk exceeds this value. The recordings are split up into several individual recordings,
|
||||
which have the defined size (roughly). Has to be activated with `splitStrategy`.
|
||||
|
||||
- **webinterface** (server only) - [`true`,`false`] Enables the webinterface for the server. You can access it with http://host:port/static/index.html Don't activate this on
|
||||
a machine, which can be accessed from the internet, because this is totally unprotected at the moment.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<configuration scan="true" scanPeriod="30 seconds">
|
||||
<configuration scan="true" scanPeriod="10 seconds">
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoders are assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
/bin/
|
||||
/target/
|
||||
/common.iml
|
||||
/.idea/
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<parent>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<version>3.10.3</version>
|
||||
<version>3.10.10</version>
|
||||
<relativePath>../master</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package ctbrec;
|
||||
|
||||
import static ctbrec.io.HttpConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
|
@ -17,6 +19,8 @@ import ctbrec.recorder.download.HttpHeaderFactoryImpl;
|
|||
import ctbrec.recorder.download.hls.HlsDownload;
|
||||
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
|
||||
import ctbrec.sites.Site;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public abstract class AbstractModel implements Model {
|
||||
|
||||
|
@ -270,4 +274,18 @@ public abstract class AbstractModel implements Model {
|
|||
fac.setSegmentHeaders(new HashMap<>());
|
||||
return fac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() throws IOException {
|
||||
Request req = new Request.Builder() // @formatter:off
|
||||
.url(getUrl())
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
.build(); // @formatter:on
|
||||
try (Response response = getSite().getHttpClient().execute(req)) {
|
||||
if (!response.isSuccessful() && response.code() == 404) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,12 +17,14 @@ import java.util.List;
|
|||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
|
||||
import ctbrec.Settings.SplitStrategy;
|
||||
import ctbrec.io.FileJsonAdapter;
|
||||
import ctbrec.io.ModelJsonAdapter;
|
||||
import ctbrec.io.PostProcessorJsonAdapter;
|
||||
|
@ -102,6 +104,7 @@ public class Config {
|
|||
migrateOldSettings();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void migrateOldSettings() {
|
||||
// 3.8.0 from maxResolution only to resolution range
|
||||
if(settings.minimumResolution == settings.maximumResolution && settings.minimumResolution == 0) {
|
||||
|
@ -128,6 +131,36 @@ public class Config {
|
|||
settings.postProcessors.add(removeKeepFile);
|
||||
settings.removeRecordingAfterPostProcessing = false;
|
||||
}
|
||||
|
||||
// 3.10.7
|
||||
if (StringUtil.isNotBlank(settings.username)) {
|
||||
settings.chaturbateUsername = settings.username;
|
||||
settings.username = null;
|
||||
}
|
||||
if (StringUtil.isNotBlank(settings.password)) {
|
||||
settings.chaturbatePassword = settings.password;
|
||||
settings.password = null;
|
||||
}
|
||||
if (settings.splitRecordings > 0) {
|
||||
settings.splitStrategy = SplitStrategy.TIME;
|
||||
settings.splitRecordingsAfterSecs = settings.splitRecordings;
|
||||
settings.splitRecordings = 0;
|
||||
}
|
||||
// migrate old config from ctbrec-minimal browser
|
||||
File oldLocation = new File(OS.getConfigDir().getParentFile(), "ctbrec-minimal-browser");
|
||||
if (oldLocation.exists()) {
|
||||
File newLocation = new File(getConfigDir(), oldLocation.getName());
|
||||
try {
|
||||
if (!newLocation.exists()) {
|
||||
LOG.debug("Moving minimal browser config {} --> {}", oldLocation, newLocation);
|
||||
FileUtils.moveDirectory(oldLocation, newLocation);
|
||||
} else {
|
||||
LOG.debug("minimal browser settings have been migrated before");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Couldn't migrate minimal browser config location", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void makeBackup(File source) {
|
||||
|
|
|
@ -136,4 +136,11 @@ public interface Model extends Comparable<Model>, Serializable {
|
|||
public SubsequentAction getRecordUntilSubsequentAction();
|
||||
public void setRecordUntilSubsequentAction(SubsequentAction action);
|
||||
|
||||
/**
|
||||
* Check, if this model account exists
|
||||
* @return true, if it exists, false otherwise
|
||||
* @throws IOException
|
||||
*/
|
||||
public boolean exists() throws IOException;
|
||||
|
||||
}
|
|
@ -86,10 +86,16 @@ public class OS {
|
|||
System.arraycopy(args, 0, cmd, 1, args.length);
|
||||
break;
|
||||
case MAC:
|
||||
cmd = new String[args.length + 2];
|
||||
cmd[0] = "open";
|
||||
cmd[1] = new File(browserDir, "ctbrec-minimal-browser.app").getAbsolutePath();
|
||||
System.arraycopy(args, 0, cmd, 2, args.length);
|
||||
cmd = new String[args.length + 5];
|
||||
int index = 0;
|
||||
cmd[index++] = "open";
|
||||
cmd[index++] = "-W";
|
||||
cmd[index++] = "-a";
|
||||
cmd[index++] = new File(browserDir, "ctbrec-minimal-browser.app").getAbsolutePath();
|
||||
if (args.length > 0) {
|
||||
cmd[index] = "--args";
|
||||
System.arraycopy(args, 0, cmd, 5, args.length);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperatingSystemException(System.getProperty(OS_NAME));
|
||||
|
|
|
@ -3,35 +3,23 @@ package ctbrec;
|
|||
import static ctbrec.Recording.State.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.nio.file.FileVisitOption;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.event.EventBusHolder;
|
||||
import ctbrec.event.RecordingStateChangedEvent;
|
||||
import ctbrec.io.IoUtils;
|
||||
import ctbrec.recorder.download.Download;
|
||||
import ctbrec.recorder.download.VideoLengthDetector;
|
||||
|
||||
public class Recording implements Serializable {
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(Recording.class);
|
||||
|
||||
private String id;
|
||||
private Model model;
|
||||
private transient Download download;
|
||||
|
@ -211,7 +199,7 @@ public class Recording implements Serializable {
|
|||
public Duration getLength() {
|
||||
File ppFile = getPostProcessedFile();
|
||||
if (ppFile.isDirectory()) {
|
||||
File playlist = new File(ppFile, "playlist.m3u8a");
|
||||
File playlist = new File(ppFile, "playlist.m3u8");
|
||||
return VideoLengthDetector.getLength(playlist);
|
||||
} else {
|
||||
return VideoLengthDetector.getLength(ppFile);
|
||||
|
@ -253,11 +241,11 @@ public class Recording implements Serializable {
|
|||
private long getSize() {
|
||||
File rec = getAbsoluteFile();
|
||||
if (rec.isDirectory()) {
|
||||
return getDirectorySize(rec);
|
||||
return IoUtils.getDirectorySize(rec);
|
||||
} else {
|
||||
if (!rec.exists()) {
|
||||
if (rec.getName().endsWith(".m3u8")) {
|
||||
return getDirectorySize(rec.getParentFile());
|
||||
return IoUtils.getDirectorySize(rec.getParentFile());
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
|
@ -267,29 +255,6 @@ public class Recording implements Serializable {
|
|||
}
|
||||
}
|
||||
|
||||
private long getDirectorySize(File dir) {
|
||||
final long[] size = { 0 };
|
||||
int maxDepth = 1; // Don't expect subdirs, so don't even try
|
||||
try {
|
||||
Files.walkFileTree(dir.toPath(), EnumSet.noneOf(FileVisitOption.class), maxDepth, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
||||
size[0] += attrs.size();
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) {
|
||||
// Ignore file access issues
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
LOG.error("Couldn't determine size of recording {}", this, e);
|
||||
}
|
||||
return size[0];
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
sizeInByte = getSize();
|
||||
}
|
||||
|
|
|
@ -34,6 +34,13 @@ public class Settings {
|
|||
SOCKS5
|
||||
}
|
||||
|
||||
public enum SplitStrategy {
|
||||
DONT,
|
||||
TIME,
|
||||
SIZE,
|
||||
TIME_OR_SIZE
|
||||
}
|
||||
|
||||
public String bongacamsBaseUrl = "https://bongacams.com";
|
||||
public String bongaPassword = "";
|
||||
public String bongaUsername = "";
|
||||
|
@ -41,6 +48,8 @@ public class Settings {
|
|||
public String cam4Username = "";
|
||||
public String camsodaPassword = "";
|
||||
public String camsodaUsername = "";
|
||||
public String chaturbatePassword = "";
|
||||
public String chaturbateUsername = "";
|
||||
public String chaturbateBaseUrl = "https://chaturbate.com";
|
||||
public boolean chooseStreamQuality = false;
|
||||
public String colorAccent = "#FFFFFF";
|
||||
|
@ -61,8 +70,8 @@ public class Settings {
|
|||
public int httpSecurePort = 8443;
|
||||
public String httpServer = "localhost";
|
||||
public int httpTimeout = 10000;
|
||||
public String httpUserAgent = "Mozilla/5.0 Gecko/20100101 Firefox/73.0";
|
||||
public String httpUserAgentMobile = "Mozilla/5.0 (Android 9.0; Mobile; rv:73.0) Gecko/63.0 Firefox/73.0";
|
||||
public String httpUserAgent = "Mozilla/5.0 (X11; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0";
|
||||
public String httpUserAgentMobile = "Mozilla/5.0 (Android 9.0; Mobile; rv:82.0) Gecko/82.0 Firefox/82.0";
|
||||
public byte[] key = null;
|
||||
public String lastDownloadDir = "";
|
||||
public String livejasminBaseUrl = "https://www.livejasmin.com";
|
||||
|
@ -71,6 +80,7 @@ public class Settings {
|
|||
public String livejasminUsername = "";
|
||||
public boolean livePreviews = false;
|
||||
public boolean localRecording = true;
|
||||
public boolean logFFmpegOutput = false;
|
||||
public int minimumResolution = 0;
|
||||
public int maximumResolution = 8640;
|
||||
public int maximumResolutionPlayer = 0;
|
||||
|
@ -90,10 +100,13 @@ public class Settings {
|
|||
public Map<String, String> modelNotes = new HashMap<>();
|
||||
public List<Model> models = new ArrayList<>();
|
||||
public List<Model> modelsIgnored = new ArrayList<>();
|
||||
public boolean monitorClipboard = false;
|
||||
public int onlineCheckIntervalInSecs = 60;
|
||||
public boolean onlineCheckSkipsPausedModels = false;
|
||||
public int overviewUpdateIntervalInSecs = 10;
|
||||
public String password = ""; // chaturbate password TODO maybe rename this onetime
|
||||
@Deprecated
|
||||
public String password = "";
|
||||
@Deprecated
|
||||
public String postProcessing = "";
|
||||
public int postProcessingThreads = 2;
|
||||
public List<PostProcessor> postProcessors = new ArrayList<>();
|
||||
|
@ -120,7 +133,11 @@ public class Settings {
|
|||
public String showupUsername = "";
|
||||
public String showupPassword = "";
|
||||
public boolean singlePlayer = true;
|
||||
@Deprecated
|
||||
public int splitRecordings = 0;
|
||||
public SplitStrategy splitStrategy = SplitStrategy.DONT;
|
||||
public int splitRecordingsAfterSecs = 0;
|
||||
public long splitRecordingsBiggerThanBytes = 0;
|
||||
public String startTab = "Settings";
|
||||
public String streamatePassword = "";
|
||||
public String streamateUsername = "";
|
||||
|
@ -130,7 +147,8 @@ public class Settings {
|
|||
public boolean transportLayerSecurity = true;
|
||||
public int thumbWidth = 180;
|
||||
public boolean updateThumbnails = true;
|
||||
public String username = ""; // chaturbate username TODO maybe rename this onetime
|
||||
@Deprecated
|
||||
public String username = "";
|
||||
public int windowHeight = 800;
|
||||
public boolean windowMaximized = false;
|
||||
public int windowWidth = 1340;
|
||||
|
|
|
@ -7,7 +7,7 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
import ctbrec.OS;
|
||||
import ctbrec.event.EventHandlerConfiguration.ActionConfiguration;
|
||||
import ctbrec.io.StreamRedirectThread;
|
||||
import ctbrec.io.StreamRedirector;
|
||||
|
||||
public class ExecuteProgram extends Action {
|
||||
|
||||
|
@ -38,11 +38,11 @@ public class ExecuteProgram extends Action {
|
|||
|
||||
// create threads, which read stdout and stderr of the player process. these are needed,
|
||||
// because otherwise the internal buffer for these streams fill up and block the process
|
||||
Thread std = new Thread(new StreamRedirectThread(process.getInputStream(), System.out));
|
||||
Thread std = new Thread(new StreamRedirector(process.getInputStream(), System.out));
|
||||
std.setName("Player stdout pipe");
|
||||
std.setDaemon(true);
|
||||
std.start();
|
||||
Thread err = new Thread(new StreamRedirectThread(process.getErrorStream(), System.err));
|
||||
Thread err = new Thread(new StreamRedirector(process.getErrorStream(), System.err));
|
||||
err.setName("Player stderr pipe");
|
||||
err.setDaemon(true);
|
||||
err.start();
|
||||
|
|
|
@ -2,7 +2,13 @@ package ctbrec.io;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitOption;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -50,4 +56,27 @@ public class IoUtils {
|
|||
throw new IOException("Couldn't delete all files in " + directory);
|
||||
}
|
||||
}
|
||||
|
||||
public static long getDirectorySize(File dir) {
|
||||
final long[] size = { 0 };
|
||||
int maxDepth = 1; // Don't expect subdirs, so don't even try
|
||||
try {
|
||||
Files.walkFileTree(dir.toPath(), EnumSet.noneOf(FileVisitOption.class), maxDepth, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
||||
size[0] += attrs.size();
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) {
|
||||
// Ignore file access issues
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
LOG.error("Couldn't determine size of directory {}", dir, e);
|
||||
}
|
||||
return size[0];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,13 @@ import java.io.OutputStream;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class StreamRedirectThread implements Runnable {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(StreamRedirectThread.class);
|
||||
public class StreamRedirector implements Runnable {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(StreamRedirector.class);
|
||||
|
||||
private InputStream in;
|
||||
private OutputStream out;
|
||||
|
||||
public StreamRedirectThread(InputStream in, OutputStream out) {
|
||||
public StreamRedirector(InputStream in, OutputStream out) {
|
||||
super();
|
||||
this.in = in;
|
||||
this.out = out;
|
|
@ -0,0 +1,135 @@
|
|||
package ctbrec.recorder;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.io.DevNull;
|
||||
import ctbrec.io.StreamRedirector;
|
||||
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||
|
||||
public class FFmpeg {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FFmpeg.class);
|
||||
|
||||
private Process process;
|
||||
private boolean logOutput = false;
|
||||
private Consumer<Process> startCallback;
|
||||
private Consumer<Integer> exitCallback;
|
||||
private File ffmpegLog = null;
|
||||
private OutputStream ffmpegLogStream;
|
||||
private Thread stdout;
|
||||
private Thread stderr;
|
||||
|
||||
private FFmpeg() {}
|
||||
|
||||
public void exec(String[] cmdline, String[] env, File executionDir) throws IOException, InterruptedException {
|
||||
LOG.debug("FFmpeg command line: {}", Arrays.toString(cmdline));
|
||||
process = Runtime.getRuntime().exec(cmdline, env, executionDir);
|
||||
afterStart();
|
||||
int exitCode = process.waitFor();
|
||||
afterExit(exitCode);
|
||||
}
|
||||
|
||||
private void afterStart() throws IOException {
|
||||
notifyStartCallback(process);
|
||||
setupLogging();
|
||||
}
|
||||
|
||||
private void afterExit(int exitCode) throws InterruptedException, IOException {
|
||||
LOG.debug("FFmpeg exit code was {}", exitCode);
|
||||
notifyExitCallback(exitCode);
|
||||
stdout.join();
|
||||
stderr.join();
|
||||
ffmpegLogStream.flush();
|
||||
ffmpegLogStream.close();
|
||||
if (exitCode != 1) {
|
||||
if (ffmpegLog != null && ffmpegLog.exists()) {
|
||||
Files.delete(ffmpegLog.toPath());
|
||||
}
|
||||
} else {
|
||||
throw new ProcessExitedUncleanException("FFmpeg exit code was " + exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupLogging() throws IOException {
|
||||
if (logOutput) {
|
||||
if (ffmpegLog == null) {
|
||||
ffmpegLog = File.createTempFile("ffmpeg_", ".log");
|
||||
}
|
||||
LOG.debug("Logging FFmpeg output to {}", ffmpegLog);
|
||||
ffmpegLog.deleteOnExit();
|
||||
ffmpegLogStream = new FileOutputStream(ffmpegLog);
|
||||
} else {
|
||||
ffmpegLogStream = new DevNull();
|
||||
}
|
||||
stdout = new Thread(new StreamRedirector(process.getInputStream(), ffmpegLogStream));
|
||||
stderr = new Thread(new StreamRedirector(process.getErrorStream(), ffmpegLogStream));
|
||||
stdout.start();
|
||||
stderr.start();
|
||||
}
|
||||
|
||||
private void notifyStartCallback(Process process) {
|
||||
try {
|
||||
startCallback.accept(process);
|
||||
} catch(Exception e) {
|
||||
LOG.error("Exception in onStart callback", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyExitCallback(int exitCode) {
|
||||
try {
|
||||
exitCallback.accept(exitCode);
|
||||
} catch(Exception e) {
|
||||
LOG.error("Exception in onExit callback", e);
|
||||
}
|
||||
}
|
||||
|
||||
public int waitFor() throws InterruptedException {
|
||||
return process.waitFor();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private boolean logOutput = false;
|
||||
private File logFile;
|
||||
private Consumer<Process> startCallback;
|
||||
private Consumer<Integer> exitCallback;
|
||||
|
||||
public Builder logOutput(boolean logOutput) {
|
||||
this.logOutput = logOutput;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder logFile(File logFile) {
|
||||
this.logFile = logFile;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder onStarted(Consumer<Process> callback) {
|
||||
this.startCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder onExit(Consumer<Integer> callback) {
|
||||
this.exitCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpeg build() {
|
||||
FFmpeg instance = new FFmpeg();
|
||||
instance.logOutput = logOutput;
|
||||
instance.startCallback = startCallback != null ? startCallback : p -> {};
|
||||
instance.exitCallback = exitCallback != null ? exitCallback : exitCode -> {};
|
||||
instance.ffmpegLog = logFile;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -159,6 +159,7 @@ public class NextGenLocalRecorder implements Recorder {
|
|||
ppPool.submit(() -> {
|
||||
try {
|
||||
setRecordingStatus(recording, State.POST_PROCESSING);
|
||||
recording.refresh();
|
||||
recordingManager.saveRecording(recording);
|
||||
recording.postprocess();
|
||||
List<PostProcessor> postProcessors = config.getSettings().postProcessors;
|
||||
|
|
|
@ -38,4 +38,6 @@ public interface Download extends Serializable {
|
|||
* @return true, if the recording is only a single file
|
||||
*/
|
||||
public boolean isSingleFile();
|
||||
|
||||
public long getSizeInByte();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package ctbrec.recorder.download;
|
||||
|
||||
import ctbrec.Settings;
|
||||
|
||||
public interface SplittingStrategy {
|
||||
|
||||
void init(Settings settings);
|
||||
boolean splitNecessary(Download download);
|
||||
}
|
|
@ -13,7 +13,7 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
import ctbrec.OS;
|
||||
import ctbrec.io.DevNull;
|
||||
import ctbrec.io.StreamRedirectThread;
|
||||
import ctbrec.io.StreamRedirector;
|
||||
|
||||
public class VideoLengthDetector {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VideoLengthDetector.class);
|
||||
|
@ -39,8 +39,8 @@ public class VideoLengthDetector {
|
|||
Process ffmpeg = Runtime.getRuntime().exec(cmdline, new String[0], videoFile.getParentFile());
|
||||
int exitCode = 1;
|
||||
ByteArrayOutputStream stdErrBuffer = new ByteArrayOutputStream();
|
||||
Thread stdout = new Thread(new StreamRedirectThread(ffmpeg.getInputStream(), new DevNull()));
|
||||
Thread stderr = new Thread(new StreamRedirectThread(ffmpeg.getErrorStream(), stdErrBuffer));
|
||||
Thread stdout = new Thread(new StreamRedirector(ffmpeg.getInputStream(), new DevNull()));
|
||||
Thread stderr = new Thread(new StreamRedirector(ffmpeg.getErrorStream(), stdErrBuffer));
|
||||
stdout.start();
|
||||
stderr.start();
|
||||
exitCode = ffmpeg.waitFor();
|
||||
|
|
|
@ -37,6 +37,7 @@ import ctbrec.Recording;
|
|||
import ctbrec.io.BandwidthMeter;
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.io.IoUtils;
|
||||
import ctbrec.recorder.download.AbstractDownload;
|
||||
import ctbrec.recorder.download.dash.SegmentTimelineType.S;
|
||||
import ctbrec.recorder.download.hls.PostProcessingException;
|
||||
|
@ -416,4 +417,9 @@ public class DashDownload extends AbstractDownload {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSizeInByte() {
|
||||
return IoUtils.getDirectorySize(downloadDir.toFile());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.OS;
|
||||
import ctbrec.io.StreamRedirectThread;
|
||||
import ctbrec.io.StreamRedirector;
|
||||
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||
|
||||
public class FfmpegMuxer {
|
||||
|
@ -99,8 +99,8 @@ public class FfmpegMuxer {
|
|||
// @formatter:on
|
||||
LOG.debug("Command line: {}", Arrays.toString(cmdline));
|
||||
Process ffmpeg = Runtime.getRuntime().exec(cmdline);
|
||||
Thread stdout = new Thread(new StreamRedirectThread(ffmpeg.getInputStream(), muxLogStream));
|
||||
Thread stderr = new Thread(new StreamRedirectThread(ffmpeg.getErrorStream(), muxLogStream));
|
||||
Thread stdout = new Thread(new StreamRedirector(ffmpeg.getInputStream(), muxLogStream));
|
||||
Thread stderr = new Thread(new StreamRedirector(ffmpeg.getErrorStream(), muxLogStream));
|
||||
stdout.start();
|
||||
stderr.start();
|
||||
int exitCode = ffmpeg.waitFor();
|
||||
|
|
|
@ -44,6 +44,7 @@ import com.iheartradio.m3u8.data.TrackData;
|
|||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.Recording.State;
|
||||
import ctbrec.Settings;
|
||||
import ctbrec.UnknownModel;
|
||||
import ctbrec.io.BandwidthMeter;
|
||||
import ctbrec.io.HttpClient;
|
||||
|
@ -51,6 +52,7 @@ import ctbrec.io.HttpException;
|
|||
import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException;
|
||||
import ctbrec.recorder.download.AbstractDownload;
|
||||
import ctbrec.recorder.download.HttpHeaderFactory;
|
||||
import ctbrec.recorder.download.SplittingStrategy;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import ctbrec.sites.Site;
|
||||
import okhttp3.Request;
|
||||
|
@ -67,6 +69,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
|
|||
protected Model model = new UnknownModel();
|
||||
protected transient LinkedBlockingQueue<Runnable> downloadQueue = new LinkedBlockingQueue<>(50);
|
||||
protected transient ExecutorService downloadThreadPool = new ThreadPoolExecutor(0, 5, 20, TimeUnit.SECONDS, downloadQueue, createThreadFactory());
|
||||
protected transient SplittingStrategy splittingStrategy;
|
||||
protected State state = State.UNKNOWN;
|
||||
private int playlistEmptyCount = 0;
|
||||
|
||||
|
@ -235,4 +238,27 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
|
|||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
protected SplittingStrategy initSplittingStrategy(Settings settings) {
|
||||
SplittingStrategy strategy;
|
||||
switch (settings.splitStrategy) {
|
||||
case TIME:
|
||||
strategy = new TimeSplittingStrategy();
|
||||
break;
|
||||
case SIZE:
|
||||
strategy = new SizeSplittingStrategy();
|
||||
break;
|
||||
case TIME_OR_SIZE:
|
||||
SplittingStrategy timeSplittingStrategy = new TimeSplittingStrategy();
|
||||
SplittingStrategy sizeSplittingStrategy = new SizeSplittingStrategy();
|
||||
strategy = new CombinedSplittingStrategy(timeSplittingStrategy, sizeSplittingStrategy);
|
||||
break;
|
||||
case DONT:
|
||||
default:
|
||||
strategy = new NoopSplittingStrategy();
|
||||
break;
|
||||
}
|
||||
strategy.init(settings);
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package ctbrec.recorder.download.hls;
|
||||
|
||||
import ctbrec.Settings;
|
||||
import ctbrec.recorder.download.Download;
|
||||
import ctbrec.recorder.download.SplittingStrategy;
|
||||
|
||||
public class CombinedSplittingStrategy implements SplittingStrategy {
|
||||
|
||||
private SplittingStrategy[] splittingStrategies;
|
||||
|
||||
public CombinedSplittingStrategy(SplittingStrategy... splittingStrategies) {
|
||||
this.splittingStrategies = splittingStrategies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Settings settings) {
|
||||
for (SplittingStrategy splittingStrategy : splittingStrategies) {
|
||||
splittingStrategy.init(settings);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean splitNecessary(Download download) {
|
||||
for (SplittingStrategy splittingStrategy : splittingStrategies) {
|
||||
if (splittingStrategy.splitNecessary(download)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ import ctbrec.Model;
|
|||
import ctbrec.OS;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.io.StreamRedirectThread;
|
||||
import ctbrec.io.StreamRedirector;
|
||||
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||
|
||||
/**
|
||||
|
@ -73,8 +73,8 @@ public class FFmpegDownload extends AbstractHlsDownload {
|
|||
File ffmpegLog = File.createTempFile(targetFile.getName(), ".log");
|
||||
ffmpegLog.deleteOnExit();
|
||||
try (FileOutputStream mergeLogStream = new FileOutputStream(ffmpegLog)) {
|
||||
Thread stdout = new Thread(new StreamRedirectThread(ffmpeg.getInputStream(), mergeLogStream));
|
||||
Thread stderr = new Thread(new StreamRedirectThread(ffmpeg.getErrorStream(), mergeLogStream));
|
||||
Thread stdout = new Thread(new StreamRedirector(ffmpeg.getInputStream(), mergeLogStream));
|
||||
Thread stderr = new Thread(new StreamRedirector(ffmpeg.getErrorStream(), mergeLogStream));
|
||||
stdout.start();
|
||||
stderr.start();
|
||||
exitCode = ffmpeg.waitFor();
|
||||
|
@ -145,4 +145,9 @@ public class FFmpegDownload extends AbstractHlsDownload {
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSizeInByte() {
|
||||
return getTarget().length();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,7 +13,6 @@ import java.nio.file.Files;
|
|||
import java.nio.file.Path;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
|
@ -40,6 +39,7 @@ import ctbrec.Recording.State;
|
|||
import ctbrec.io.BandwidthMeter;
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.io.IoUtils;
|
||||
import ctbrec.recorder.PlaylistGenerator;
|
||||
import ctbrec.recorder.download.HttpHeaderFactory;
|
||||
import okhttp3.Request;
|
||||
|
@ -48,6 +48,8 @@ import okhttp3.Response;
|
|||
|
||||
public class HlsDownload extends AbstractHlsDownload {
|
||||
|
||||
private static final int TEN_SECONDS = 10_000;
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HlsDownload.class);
|
||||
|
||||
protected transient Path downloadDir;
|
||||
|
@ -55,8 +57,8 @@ public class HlsDownload extends AbstractHlsDownload {
|
|||
private int segmentCounter = 1;
|
||||
private NumberFormat nf = new DecimalFormat("000000");
|
||||
private transient AtomicBoolean downloadFinished = new AtomicBoolean(false);
|
||||
private ZonedDateTime splitRecStartTime;
|
||||
protected transient Config config;
|
||||
private transient int waitFactor = 1;
|
||||
|
||||
public HlsDownload(HttpClient client) {
|
||||
super(client);
|
||||
|
@ -71,6 +73,7 @@ public class HlsDownload extends AbstractHlsDownload {
|
|||
String formattedStartTime = formatter.format(ZonedDateTime.ofInstant(this.startTime, ZoneId.systemDefault()));
|
||||
Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getSanitizedNamed());
|
||||
downloadDir = FileSystems.getDefault().getPath(modelDir.toString(), formattedStartTime);
|
||||
splittingStrategy = initSplittingStrategy(config.getSettings());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -78,7 +81,6 @@ public class HlsDownload extends AbstractHlsDownload {
|
|||
try {
|
||||
running = true;
|
||||
Thread.currentThread().setName("Download " + model.getName());
|
||||
splitRecStartTime = ZonedDateTime.now();
|
||||
String segments = getSegmentPlaylistUrl(model);
|
||||
if (segments != null) {
|
||||
if (!downloadDir.toFile().exists()) {
|
||||
|
@ -86,45 +88,13 @@ public class HlsDownload extends AbstractHlsDownload {
|
|||
}
|
||||
int lastSegmentNumber = 0;
|
||||
int nextSegmentNumber = 0;
|
||||
int waitFactor = 1;
|
||||
while (running) {
|
||||
SegmentPlaylist playlist = getNextSegments(segments);
|
||||
emptyPlaylistCheck(playlist);
|
||||
if (nextSegmentNumber > 0 && playlist.seq > nextSegmentNumber) {
|
||||
waitFactor *= 2;
|
||||
LOG.warn("Missed segments {} < {} in download for {} - setting wait factor to 1/{}", nextSegmentNumber, playlist.seq, model,
|
||||
waitFactor);
|
||||
}
|
||||
int skip = nextSegmentNumber - playlist.seq;
|
||||
for (String segment : playlist.segments) {
|
||||
if (skip > 0) {
|
||||
skip--;
|
||||
} else {
|
||||
URL segmentUrl = new URL(segment);
|
||||
String prefix = nf.format(segmentCounter++);
|
||||
SegmentDownload segmentDownload = new SegmentDownload(playlist, segmentUrl, downloadDir, client, prefix);
|
||||
enqueueDownload(segmentDownload, prefix, segmentUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// split recordings
|
||||
boolean split = splitRecording();
|
||||
if (split) {
|
||||
break;
|
||||
}
|
||||
|
||||
long waitForMillis = 0;
|
||||
if (lastSegmentNumber == playlist.seq) {
|
||||
// playlist didn't change -> wait for at least half the target duration
|
||||
waitForMillis = (long) playlist.targetDuration * 1000 / waitFactor;
|
||||
LOG.trace("Playlist didn't change... waiting for {}ms", waitForMillis);
|
||||
} else {
|
||||
// playlist did change -> wait for at least last segment duration
|
||||
waitForMillis = 1;
|
||||
LOG.trace("Playlist changed... waiting for {}ms", waitForMillis);
|
||||
}
|
||||
|
||||
waitSomeTime(waitForMillis);
|
||||
logMissedSegments(playlist, nextSegmentNumber);
|
||||
enqueueNewSegments(playlist, nextSegmentNumber);
|
||||
splitRecordingIfNecessary();
|
||||
waitSomeTime(playlist, lastSegmentNumber, waitFactor);
|
||||
|
||||
// this if check makes sure, that we don't decrease nextSegment. for some reason
|
||||
// streamate playlists sometimes jump back. e.g. max sequence = 79 -> 80 -> 79
|
||||
|
@ -137,52 +107,103 @@ public class HlsDownload extends AbstractHlsDownload {
|
|||
throw new IOException("Couldn't determine segments uri");
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
throw new IOException("Couldn't parse HLS playlist:\n" + e.getInput(), e);
|
||||
throw new IOException("Couldn't parse HLS playlist for model " + model + "\n" + e.getInput(), e);
|
||||
} catch (PlaylistException e) {
|
||||
throw new IOException("Couldn't parse HLS playlist", e);
|
||||
throw new IOException("Couldn't parse HLS playlist for model " + model, e);
|
||||
} catch (EOFException e) {
|
||||
// end of playlist reached
|
||||
LOG.debug("Reached end of playlist for model {}", model);
|
||||
} catch (HttpException e) {
|
||||
if (e.getResponseCode() == 404) {
|
||||
ctbrec.Model.State modelState;
|
||||
try {
|
||||
modelState = model.getOnlineState(false);
|
||||
} catch (ExecutionException e1) {
|
||||
modelState = ctbrec.Model.State.UNKNOWN;
|
||||
}
|
||||
LOG.info("Playlist not found (404). Model {} probably went offline. Model state: {}", model, modelState);
|
||||
waitSomeTime(10_000);
|
||||
} else if (e.getResponseCode() == 403) {
|
||||
ctbrec.Model.State modelState;
|
||||
try {
|
||||
modelState = model.getOnlineState(false);
|
||||
} catch (ExecutionException e1) {
|
||||
modelState = ctbrec.Model.State.UNKNOWN;
|
||||
}
|
||||
LOG.info("Playlist access forbidden (403). Model {} probably went private or offline. Model state: {}", model, modelState);
|
||||
waitSomeTime(10_000);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
handleHttpException(e);
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Couldn't download segment", e);
|
||||
} finally {
|
||||
downloadThreadPool.shutdown();
|
||||
try {
|
||||
LOG.debug("Waiting for last segments for {}", model);
|
||||
downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
downloadFinished.set(true);
|
||||
synchronized (downloadFinished) {
|
||||
downloadFinished.notifyAll();
|
||||
}
|
||||
LOG.debug("Download for {} terminated", model);
|
||||
finalizeDownload();
|
||||
}
|
||||
}
|
||||
|
||||
private void finalizeDownload() {
|
||||
downloadThreadPool.shutdown();
|
||||
try {
|
||||
LOG.debug("Waiting for last segments for {}", model);
|
||||
downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
downloadFinished.set(true);
|
||||
synchronized (downloadFinished) {
|
||||
downloadFinished.notifyAll();
|
||||
}
|
||||
LOG.debug("Download for {} terminated", model);
|
||||
}
|
||||
|
||||
private void handleHttpException(HttpException e) throws IOException {
|
||||
if (e.getResponseCode() == 404) {
|
||||
ctbrec.Model.State modelState;
|
||||
try {
|
||||
modelState = model.getOnlineState(false);
|
||||
} catch (ExecutionException e1) {
|
||||
modelState = ctbrec.Model.State.UNKNOWN;
|
||||
}
|
||||
LOG.info("Playlist not found (404). Model {} probably went offline. Model state: {}", model, modelState);
|
||||
waitSomeTime(TEN_SECONDS);
|
||||
} else if (e.getResponseCode() == 403) {
|
||||
ctbrec.Model.State modelState;
|
||||
try {
|
||||
modelState = model.getOnlineState(false);
|
||||
} catch (ExecutionException e1) {
|
||||
modelState = ctbrec.Model.State.UNKNOWN;
|
||||
}
|
||||
LOG.info("Playlist access forbidden (403). Model {} probably went private or offline. Model state: {}", model, modelState);
|
||||
waitSomeTime(TEN_SECONDS);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void splitRecordingIfNecessary() {
|
||||
if (splittingStrategy.splitNecessary(this)) {
|
||||
internalStop();
|
||||
}
|
||||
}
|
||||
|
||||
private void enqueueNewSegments(SegmentPlaylist playlist, int nextSegmentNumber) throws IOException, ExecutionException, InterruptedException {
|
||||
int skip = nextSegmentNumber - playlist.seq;
|
||||
for (String segment : playlist.segments) {
|
||||
if (skip > 0) {
|
||||
skip--;
|
||||
} else {
|
||||
URL segmentUrl = new URL(segment);
|
||||
String prefix = nf.format(segmentCounter++);
|
||||
SegmentDownload segmentDownload = new SegmentDownload(playlist, segmentUrl, downloadDir, client, prefix);
|
||||
enqueueDownload(segmentDownload, prefix, segmentUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void logMissedSegments(SegmentPlaylist playlist, int nextSegmentNumber) {
|
||||
if (nextSegmentNumber > 0 && playlist.seq > nextSegmentNumber) {
|
||||
waitFactor *= 2;
|
||||
LOG.warn("Missed segments {} < {} in download for {} - setting wait factor to 1/{}", nextSegmentNumber, playlist.seq, model,
|
||||
waitFactor);
|
||||
}
|
||||
}
|
||||
|
||||
private void waitSomeTime(SegmentPlaylist playlist, int lastSegmentNumber, int waitFactor) {
|
||||
long waitForMillis = 0;
|
||||
if (lastSegmentNumber == playlist.seq) {
|
||||
// playlist didn't change -> wait for at least half the target duration
|
||||
waitForMillis = (long) playlist.targetDuration * 1000 / waitFactor;
|
||||
LOG.trace("Playlist didn't change... waiting for {}ms", waitForMillis);
|
||||
} else {
|
||||
// playlist did change -> wait for at least last segment duration
|
||||
waitForMillis = 1;
|
||||
LOG.trace("Playlist changed... waiting for {}ms", waitForMillis);
|
||||
}
|
||||
|
||||
waitSomeTime(waitForMillis);
|
||||
}
|
||||
|
||||
private void enqueueDownload(SegmentDownload segmentDownload, String prefix, URL segmentUrl) throws IOException, ExecutionException, InterruptedException {
|
||||
try {
|
||||
downloadThreadPool.submit(segmentDownload);
|
||||
|
@ -228,18 +249,6 @@ public class HlsDownload extends AbstractHlsDownload {
|
|||
|
||||
}
|
||||
|
||||
private boolean splitRecording() {
|
||||
if (config.getSettings().splitRecordings > 0) {
|
||||
Duration recordingDuration = Duration.between(splitRecStartTime, ZonedDateTime.now());
|
||||
long seconds = recordingDuration.getSeconds();
|
||||
if (seconds >= config.getSettings().splitRecordings) {
|
||||
internalStop();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (running) {
|
||||
|
@ -334,4 +343,9 @@ public class HlsDownload extends AbstractHlsDownload {
|
|||
public boolean isSingleFile() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSizeInByte() {
|
||||
return IoUtils.getDirectorySize(getTarget());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,13 @@ import static java.util.Optional.*;
|
|||
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
@ -40,7 +36,7 @@ import ctbrec.Recording;
|
|||
import ctbrec.io.BandwidthMeter;
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.io.StreamRedirectThread;
|
||||
import ctbrec.recorder.FFmpeg;
|
||||
import ctbrec.recorder.ProgressListener;
|
||||
import ctbrec.recorder.download.HttpHeaderFactory;
|
||||
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||
|
@ -53,14 +49,15 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
private static final long serialVersionUID = 1L;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MergedFfmpegHlsDownload.class);
|
||||
private static final boolean IGNORE_CACHE = true;
|
||||
private ZonedDateTime splitRecStartTime;
|
||||
private File targetFile;
|
||||
private transient Config config;
|
||||
private transient Process ffmpeg;
|
||||
private transient Process ffmpegProcess;
|
||||
private transient OutputStream ffmpegStdIn;
|
||||
protected transient Thread ffmpegThread;
|
||||
private transient Object ffmpegStartMonitor = new Object();
|
||||
private Queue<Future<byte[]>> downloads = new LinkedList<>();
|
||||
private transient Queue<Future<byte[]>> downloads = new LinkedList<>();
|
||||
private transient int lastSegment = 0;
|
||||
private transient int nextSegment = 0;
|
||||
|
||||
public MergedFfmpegHlsDownload(HttpClient client) {
|
||||
super(client);
|
||||
|
@ -73,6 +70,7 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
this.model = model;
|
||||
String fileSuffix = config.getSettings().ffmpegFileSuffix;
|
||||
targetFile = config.getFileForRecording(model, fileSuffix, startTime);
|
||||
splittingStrategy = initSplittingStrategy(config.getSettings());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -86,7 +84,6 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
running = true;
|
||||
Thread.currentThread().setName("Download " + model.getName());
|
||||
super.startTime = Instant.now();
|
||||
splitRecStartTime = ZonedDateTime.now();
|
||||
|
||||
String segments = getSegmentPlaylistUrl(model);
|
||||
|
||||
|
@ -94,17 +91,17 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
startFfmpegProcess(targetFile);
|
||||
synchronized (ffmpegStartMonitor) {
|
||||
int tries = 0;
|
||||
while (ffmpeg == null && tries++ < 15) {
|
||||
while (ffmpegProcess == null && tries++ < 15) {
|
||||
LOG.debug("Waiting for FFmpeg to spawn to record {}", model.getName());
|
||||
ffmpegStartMonitor.wait(1000);
|
||||
}
|
||||
}
|
||||
|
||||
if (ffmpeg == null) {
|
||||
if (ffmpegProcess == null) {
|
||||
throw new ProcessExitedUncleanException("Couldn't spawn FFmpeg");
|
||||
} else {
|
||||
LOG.debug("Starting to download segments");
|
||||
downloadSegments(segments, true);
|
||||
startDownloadLoop(segments, true);
|
||||
ffmpegThread.join();
|
||||
LOG.debug("FFmpeg thread terminated");
|
||||
}
|
||||
|
@ -134,47 +131,20 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
private void startFfmpegProcess(File target) {
|
||||
ffmpegThread = new Thread(() -> {
|
||||
try {
|
||||
String[] args = config.getSettings().ffmpegMergedDownloadArgs.split(" ");
|
||||
String[] argsPlusFile = new String[args.length + 3];
|
||||
int i = 0;
|
||||
argsPlusFile[i++] = "-i";
|
||||
argsPlusFile[i++] = "-";
|
||||
System.arraycopy(args, 0, argsPlusFile, i, args.length);
|
||||
argsPlusFile[argsPlusFile.length - 1] = target.getAbsolutePath();
|
||||
String[] cmdline = OS.getFFmpegCommand(argsPlusFile);
|
||||
|
||||
LOG.debug("Command line: {}", Arrays.toString(cmdline));
|
||||
ffmpeg = Runtime.getRuntime().exec(cmdline, new String[0], target.getParentFile());
|
||||
synchronized (ffmpegStartMonitor) {
|
||||
ffmpegStartMonitor.notifyAll();
|
||||
}
|
||||
ffmpegStdIn = ffmpeg.getOutputStream();
|
||||
int exitCode = 1;
|
||||
File ffmpegLog = File.createTempFile(target.getName(), ".log");
|
||||
ffmpegLog.deleteOnExit();
|
||||
try (FileOutputStream mergeLogStream = new FileOutputStream(ffmpegLog)) {
|
||||
Thread stdout = new Thread(new StreamRedirectThread(ffmpeg.getInputStream(), mergeLogStream));
|
||||
Thread stderr = new Thread(new StreamRedirectThread(ffmpeg.getErrorStream(), mergeLogStream));
|
||||
stdout.start();
|
||||
stderr.start();
|
||||
exitCode = ffmpeg.waitFor();
|
||||
LOG.debug("FFmpeg exited with code {}", exitCode);
|
||||
stdout.join();
|
||||
stderr.join();
|
||||
mergeLogStream.flush();
|
||||
}
|
||||
if (exitCode != 1) {
|
||||
if (ffmpegLog.exists()) {
|
||||
Files.delete(ffmpegLog.toPath());
|
||||
}
|
||||
} else {
|
||||
if (running) {
|
||||
LOG.info("FFmpeg exit code was {}. Logfile: {}", exitCode, ffmpegLog.getAbsolutePath());
|
||||
throw new ProcessExitedUncleanException("FFmpeg exit code was " + exitCode);
|
||||
}
|
||||
}
|
||||
String[] cmdline = prepareCommandLine(target);
|
||||
FFmpeg ffmpeg = new FFmpeg.Builder()
|
||||
.logOutput(config.getSettings().logFFmpegOutput)
|
||||
.onStarted(p -> {
|
||||
ffmpegProcess = p;
|
||||
ffmpegStdIn = ffmpegProcess.getOutputStream();
|
||||
synchronized (ffmpegStartMonitor) {
|
||||
ffmpegStartMonitor.notifyAll();
|
||||
}
|
||||
})
|
||||
.build();
|
||||
ffmpeg.exec(cmdline, new String[0], target.getParentFile());
|
||||
} catch (IOException | ProcessExitedUncleanException e) {
|
||||
LOG.error("Error in FFMpeg thread", e);
|
||||
LOG.error("Error in FFmpeg thread", e);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
if (running) {
|
||||
|
@ -187,52 +157,23 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
ffmpegThread.start();
|
||||
}
|
||||
|
||||
protected void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
|
||||
int lastSegment = 0;
|
||||
int nextSegment = 0;
|
||||
private String[] prepareCommandLine(File target) {
|
||||
String[] args = config.getSettings().ffmpegMergedDownloadArgs.split(" ");
|
||||
String[] argsPlusFile = new String[args.length + 3];
|
||||
int i = 0;
|
||||
argsPlusFile[i++] = "-i";
|
||||
argsPlusFile[i++] = "-";
|
||||
System.arraycopy(args, 0, argsPlusFile, i, args.length);
|
||||
argsPlusFile[argsPlusFile.length - 1] = target.getAbsolutePath();
|
||||
return OS.getFFmpegCommand(argsPlusFile);
|
||||
}
|
||||
|
||||
protected void startDownloadLoop(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
|
||||
while (running) {
|
||||
try {
|
||||
SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri);
|
||||
emptyPlaylistCheck(lsp);
|
||||
|
||||
// download new segments
|
||||
long downloadStart = System.currentTimeMillis();
|
||||
if (livestreamDownload) {
|
||||
downloadNewSegments(lsp, nextSegment);
|
||||
} else {
|
||||
downloadRecording(lsp);
|
||||
}
|
||||
long downloadTookMillis = System.currentTimeMillis() - downloadStart;
|
||||
|
||||
// download segments, which might have been skipped
|
||||
if (nextSegment > 0 && lsp.seq > nextSegment) {
|
||||
LOG.warn("Missed segments {} < {} in download for {}. Download took {}ms. Playlist is {}sec", nextSegment, lsp.seq, lsp.url,
|
||||
downloadTookMillis, lsp.totalDuration);
|
||||
}
|
||||
|
||||
if (livestreamDownload) {
|
||||
// split up the recording, if configured
|
||||
boolean split = splitRecording();
|
||||
if (split) {
|
||||
break;
|
||||
}
|
||||
|
||||
// wait some time until requesting the segment playlist again to not hammer the server
|
||||
waitForNewSegments(lsp, lastSegment, downloadTookMillis);
|
||||
|
||||
lastSegment = lsp.seq;
|
||||
nextSegment = lastSegment + lsp.segments.size();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
downloadSegments(segmentPlaylistUri, livestreamDownload);
|
||||
} catch (HttpException e) {
|
||||
if (e.getResponseCode() == 404) {
|
||||
LOG.debug("Playlist not found (404). Model {} probably went offline", model);
|
||||
} else if (e.getResponseCode() == 403) {
|
||||
LOG.debug("Playlist access forbidden (403). Model {} probably went private or offline", model);
|
||||
} else {
|
||||
LOG.info("Unexpected error while downloading {}", model, e);
|
||||
}
|
||||
logHttpException(e);
|
||||
running = false;
|
||||
} catch (MalformedURLException e) {
|
||||
LOG.info("Malformed URL {} - {}", model, segmentPlaylistUri, e);
|
||||
|
@ -245,6 +186,54 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
ffmpegThread.interrupt();
|
||||
}
|
||||
|
||||
private void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException, ExecutionException {
|
||||
SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri);
|
||||
emptyPlaylistCheck(lsp);
|
||||
|
||||
// download new segments
|
||||
long downloadStart = System.currentTimeMillis();
|
||||
if (livestreamDownload) {
|
||||
downloadNewSegments(lsp, nextSegment);
|
||||
} else {
|
||||
downloadRecording(lsp);
|
||||
}
|
||||
long downloadTookMillis = System.currentTimeMillis() - downloadStart;
|
||||
|
||||
// download segments, which might have been skipped
|
||||
if (nextSegment > 0 && lsp.seq > nextSegment) {
|
||||
LOG.warn("Missed segments {} < {} in download for {}. Download took {}ms. Playlist is {}sec", nextSegment, lsp.seq, lsp.url,
|
||||
downloadTookMillis, lsp.totalDuration);
|
||||
}
|
||||
|
||||
if (livestreamDownload) {
|
||||
splitRecordingIfNecessary();
|
||||
|
||||
// wait some time until requesting the segment playlist again to not hammer the server
|
||||
waitForNewSegments(lsp, lastSegment, downloadTookMillis);
|
||||
|
||||
lastSegment = lsp.seq;
|
||||
nextSegment = lastSegment + lsp.segments.size();
|
||||
} else {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void logHttpException(HttpException e) {
|
||||
if (e.getResponseCode() == 404) {
|
||||
LOG.debug("Playlist not found (404). Model {} probably went offline", model);
|
||||
} else if (e.getResponseCode() == 403) {
|
||||
LOG.debug("Playlist access forbidden (403). Model {} probably went private or offline", model);
|
||||
} else {
|
||||
LOG.info("Unexpected error while downloading {}", model, e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void splitRecordingIfNecessary() {
|
||||
if (splittingStrategy.splitNecessary(this)) {
|
||||
internalStop();
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadRecording(SegmentPlaylist lsp) throws IOException {
|
||||
for (String segment : lsp.segments) {
|
||||
URL segmentUrl = new URL(segment);
|
||||
|
@ -292,33 +281,40 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
} catch (CancellationException e) {
|
||||
LOG.info("Segment download cancelled");
|
||||
} catch (ExecutionException e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof MissingSegmentException) {
|
||||
if (model != null && !isModelOnline()) {
|
||||
LOG.debug("Error while downloading segment, because model {} is offline. Stopping now", model.getName());
|
||||
running = false;
|
||||
} else {
|
||||
LOG.debug("Segment not available, but model {} still online. Going on", ofNullable(model).map(Model::getName).orElse("n/a"));
|
||||
}
|
||||
} else if (cause instanceof HttpException) {
|
||||
HttpException he = (HttpException) cause;
|
||||
if (model != null && !isModelOnline()) {
|
||||
LOG.debug("Error {} while downloading segment, because model {} is offline. Stopping now", he.getResponseCode(), model.getName());
|
||||
running = false;
|
||||
} else {
|
||||
if (he.getResponseCode() == 404) {
|
||||
LOG.info("Playlist for {} not found [HTTP 404]. Stopping now", ofNullable(model).map(Model::getName).orElse("n/a"));
|
||||
running = false;
|
||||
} else if (he.getResponseCode() == 403) {
|
||||
LOG.info("Playlist for {} not accessible [HTTP 403]. Stopping now", ofNullable(model).map(Model::getName).orElse("n/a"));
|
||||
running = false;
|
||||
} else {
|
||||
throw he;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
handleExecutionExceptione(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleExecutionExceptione(ExecutionException e) throws HttpException, ExecutionException {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof MissingSegmentException) {
|
||||
if (model != null && !isModelOnline()) {
|
||||
LOG.debug("Error while downloading segment, because model {} is offline. Stopping now", model.getName());
|
||||
running = false;
|
||||
} else {
|
||||
LOG.debug("Segment not available, but model {} still online. Going on", ofNullable(model).map(Model::getName).orElse("n/a"));
|
||||
}
|
||||
} else if (cause instanceof HttpException) {
|
||||
handleHttpException((HttpException)cause);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleHttpException(HttpException he) throws HttpException {
|
||||
if (model != null && !isModelOnline()) {
|
||||
LOG.debug("Error {} while downloading segment, because model {} is offline. Stopping now", he.getResponseCode(), model.getName());
|
||||
running = false;
|
||||
} else {
|
||||
if (he.getResponseCode() == 404) {
|
||||
LOG.info("Playlist for {} not found [HTTP 404]. Stopping now", ofNullable(model).map(Model::getName).orElse("n/a"));
|
||||
running = false;
|
||||
} else if (he.getResponseCode() == 403) {
|
||||
LOG.info("Playlist for {} not accessible [HTTP 403]. Stopping now", ofNullable(model).map(Model::getName).orElse("n/a"));
|
||||
running = false;
|
||||
} else {
|
||||
throw he;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -337,18 +333,6 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
writeSegment(segmentData, 0, segmentData.length);
|
||||
}
|
||||
|
||||
protected boolean splitRecording() {
|
||||
if (config.getSettings().splitRecordings > 0) {
|
||||
Duration recordingDuration = Duration.between(splitRecStartTime, ZonedDateTime.now());
|
||||
long seconds = recordingDuration.getSeconds();
|
||||
if (seconds >= config.getSettings().splitRecordings) {
|
||||
internalStop();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void waitForNewSegments(SegmentPlaylist lsp, int lastSegment, long downloadTookMillis) {
|
||||
try {
|
||||
long wait = 0;
|
||||
|
@ -409,15 +393,15 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
}
|
||||
}
|
||||
|
||||
if (ffmpeg != null) {
|
||||
if (ffmpegProcess != null) {
|
||||
try {
|
||||
boolean waitFor = ffmpeg.waitFor(45, TimeUnit.SECONDS);
|
||||
if (!waitFor && ffmpeg.isAlive()) {
|
||||
ffmpeg.destroy();
|
||||
if (ffmpeg.isAlive()) {
|
||||
boolean waitFor = ffmpegProcess.waitFor(45, TimeUnit.SECONDS);
|
||||
if (!waitFor && ffmpegProcess.isAlive()) {
|
||||
ffmpegProcess.destroy();
|
||||
if (ffmpegProcess.isAlive()) {
|
||||
LOG.info("FFmpeg didn't terminate. Destroying the process with force!");
|
||||
ffmpeg.destroyForcibly();
|
||||
ffmpeg = null;
|
||||
ffmpegProcess.destroyForcibly();
|
||||
ffmpegProcess = null;
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
|
@ -493,6 +477,7 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
|
||||
@Override
|
||||
public void postprocess(Recording recording) {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
public void downloadFinishedRecording(String segmentPlaylistUri, File target, ProgressListener progressListener, long sizeInBytes) throws Exception {
|
||||
|
@ -549,4 +534,9 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
|||
public boolean isSingleFile() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSizeInByte() {
|
||||
return getTarget().length();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package ctbrec.recorder.download.hls;
|
||||
|
||||
import ctbrec.Settings;
|
||||
import ctbrec.recorder.download.Download;
|
||||
import ctbrec.recorder.download.SplittingStrategy;
|
||||
|
||||
public class NoopSplittingStrategy implements SplittingStrategy {
|
||||
|
||||
@Override
|
||||
public void init(Settings settings) {
|
||||
// settings not needed
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean splitNecessary(Download download) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package ctbrec.recorder.download.hls;
|
||||
|
||||
import ctbrec.Settings;
|
||||
import ctbrec.recorder.download.Download;
|
||||
import ctbrec.recorder.download.SplittingStrategy;
|
||||
|
||||
public class SizeSplittingStrategy implements SplittingStrategy {
|
||||
|
||||
private Settings settings;
|
||||
|
||||
@Override
|
||||
public void init(Settings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean splitNecessary(Download download) {
|
||||
long sizeInByte = download.getSizeInByte();
|
||||
return sizeInByte >= settings.splitRecordingsBiggerThanBytes;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package ctbrec.recorder.download.hls;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import ctbrec.Settings;
|
||||
import ctbrec.recorder.download.Download;
|
||||
import ctbrec.recorder.download.SplittingStrategy;
|
||||
|
||||
public class TimeSplittingStrategy implements SplittingStrategy {
|
||||
|
||||
private Settings settings;
|
||||
|
||||
@Override
|
||||
public void init(Settings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean splitNecessary(Download download) {
|
||||
ZonedDateTime startTime = download.getStartTime().atZone(ZoneId.systemDefault());
|
||||
Duration recordingDuration = Duration.between(startTime, ZonedDateTime.now());
|
||||
long seconds = recordingDuration.getSeconds();
|
||||
return seconds >= settings.splitRecordingsAfterSecs;
|
||||
}
|
||||
|
||||
}
|
|
@ -67,15 +67,18 @@ public abstract class AbstractPlaceholderAwarePostProcessor extends AbstractPost
|
|||
|
||||
private String replaceDateTime(Recording rec, String filename, String placeHolder, ZoneId zone) {
|
||||
String pattern = "yyyy-MM-dd_HH-mm-ss";
|
||||
Matcher m = Pattern.compile("\\$\\{" + placeHolder + "(?:\\((.*?)\\))?\\}").matcher(filename);
|
||||
if (m.find()) {
|
||||
Pattern regex = Pattern.compile("\\$\\{" + placeHolder + "(?:\\((.*?)\\))?\\}");
|
||||
Matcher m = regex.matcher(filename);
|
||||
while (m.find()) {
|
||||
String p = m.group(1);
|
||||
if (p != null) {
|
||||
pattern = p;
|
||||
}
|
||||
String formattedDate = getDateTime(rec, pattern, zone);
|
||||
filename = m.replaceFirst(formattedDate);
|
||||
m = regex.matcher(filename);
|
||||
}
|
||||
String formattedDate = getDateTime(rec, pattern, zone);
|
||||
return m.replaceAll(formattedDate);
|
||||
return filename;
|
||||
}
|
||||
|
||||
private String getDateTime(Recording rec, String pattern, ZoneId zone) {
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
package ctbrec.recorder.postprocessing;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
@ -14,7 +12,7 @@ import org.slf4j.LoggerFactory;
|
|||
import ctbrec.Config;
|
||||
import ctbrec.OS;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.io.StreamRedirectThread;
|
||||
import ctbrec.recorder.FFmpeg;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
|
||||
public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
|
||||
|
@ -80,28 +78,15 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
|
|||
};
|
||||
String[] cmdline = OS.getFFmpegCommand(args);
|
||||
LOG.info("Executing {} in working directory {}", Arrays.toString(cmdline), executionDir);
|
||||
Process ffmpeg = Runtime.getRuntime().exec(cmdline, OS.getEnvironment(), executionDir);
|
||||
int exitCode = 1;
|
||||
File ffmpegLog = File.createTempFile("create_contact_sheet_" + rec.getId() + '_', ".log");
|
||||
ffmpegLog.deleteOnExit();
|
||||
try (FileOutputStream mergeLogStream = new FileOutputStream(ffmpegLog)) {
|
||||
Thread stdout = new Thread(new StreamRedirectThread(ffmpeg.getInputStream(), mergeLogStream));
|
||||
Thread stderr = new Thread(new StreamRedirectThread(ffmpeg.getErrorStream(), mergeLogStream));
|
||||
stdout.start();
|
||||
stderr.start();
|
||||
exitCode = ffmpeg.waitFor();
|
||||
LOG.debug("FFmpeg exited with code {}", exitCode);
|
||||
stdout.join();
|
||||
stderr.join();
|
||||
mergeLogStream.flush();
|
||||
}
|
||||
File ffmpegLog = new File(System.getProperty("java.io.tmpdir"), "create_contact_sheet_" + rec.getId() + ".log");
|
||||
FFmpeg ffmpeg = new FFmpeg.Builder()
|
||||
.logOutput(config.getSettings().logFFmpegOutput)
|
||||
.logFile(ffmpegLog)
|
||||
.build();
|
||||
ffmpeg.exec(cmdline, OS.getEnvironment(), executionDir);
|
||||
int exitCode = ffmpeg.waitFor();
|
||||
rec.getAssociatedFiles().add(output.getCanonicalPath());
|
||||
if (exitCode != 1) {
|
||||
if (ffmpegLog.exists()) {
|
||||
Files.delete(ffmpegLog.toPath());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return exitCode != 1;
|
||||
}
|
||||
|
||||
private File getInputFile(Recording rec) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import java.io.IOException;
|
|||
import java.nio.file.Files;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -15,7 +16,7 @@ import ctbrec.recorder.RecordingManager;
|
|||
|
||||
public class Move extends AbstractPlaceholderAwarePostProcessor {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Rename.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Move.class);
|
||||
public static final String PATH_TEMPLATE = "path.template";
|
||||
public static final String DEFAULT = "${modelSanitizedName}" + File.separatorChar + "${localDateTime}";
|
||||
|
||||
|
@ -36,7 +37,11 @@ public class Move extends AbstractPlaceholderAwarePostProcessor {
|
|||
}
|
||||
LOG.info("Moving {} to {}", src.getName(), target.getParentFile().getCanonicalPath());
|
||||
Files.createDirectories(target.getParentFile().toPath());
|
||||
Files.move(rec.getPostProcessedFile().toPath(), target.toPath());
|
||||
if (rec.getPostProcessedFile().isDirectory()) {
|
||||
FileUtils.moveDirectory(rec.getPostProcessedFile(), target);
|
||||
} else {
|
||||
FileUtils.moveFile(rec.getPostProcessedFile(), target);
|
||||
}
|
||||
rec.setPostProcessedFile(target);
|
||||
if (Objects.equals(src, rec.getAbsoluteFile())) {
|
||||
rec.setAbsoluteFile(target);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package ctbrec.recorder.postprocessing;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Arrays;
|
||||
|
@ -14,9 +13,8 @@ import ctbrec.Config;
|
|||
import ctbrec.OS;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.io.IoUtils;
|
||||
import ctbrec.io.StreamRedirectThread;
|
||||
import ctbrec.recorder.FFmpeg;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||
|
||||
public class Remux extends AbstractPostProcessor {
|
||||
|
||||
|
@ -32,70 +30,64 @@ public class Remux extends AbstractPostProcessor {
|
|||
|
||||
@Override
|
||||
public boolean postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException {
|
||||
final File inputFile;
|
||||
if (rec.getPostProcessedFile().isDirectory()) {
|
||||
inputFile = new File(rec.getPostProcessedFile(), "playlist.m3u8");
|
||||
} else {
|
||||
inputFile = rec.getPostProcessedFile();
|
||||
}
|
||||
String fileExt = getConfig().get(FILE_EXT);
|
||||
File remuxedFile = new File(rec.getPostProcessedFile().getAbsolutePath() + '.' + fileExt);
|
||||
String[] cmdline = prepareCommandline(inputFile, remuxedFile);
|
||||
File executionDir = rec.getPostProcessedFile().isDirectory() ? rec.getPostProcessedFile() : rec.getPostProcessedFile().getParentFile();
|
||||
LOG.info("Executing {} in working directory {}", Arrays.toString(cmdline), executionDir);
|
||||
|
||||
File ffmpegLog = new File(System.getProperty("java.io.tmpdir"), "remux_" + rec.getId() + ".log");
|
||||
FFmpeg ffmpeg = new FFmpeg.Builder()
|
||||
.logOutput(config.getSettings().logFFmpegOutput)
|
||||
.logFile(ffmpegLog)
|
||||
.onExit(exitCode -> finalizeStep(exitCode, rec, inputFile, remuxedFile))
|
||||
.build();
|
||||
ffmpeg.exec(cmdline, new String[0], executionDir);
|
||||
int exitCode = ffmpeg.waitFor();
|
||||
return exitCode != 1;
|
||||
}
|
||||
|
||||
private void finalizeStep(int exitCode, Recording rec, File inputFile, File remuxedFile) {
|
||||
if (exitCode != 1) {
|
||||
try {
|
||||
rec.setPostProcessedFile(remuxedFile);
|
||||
if (inputFile.getName().equals("playlist.m3u8")) {
|
||||
IoUtils.deleteDirectory(inputFile.getParentFile());
|
||||
if (Objects.equals(inputFile.getParentFile(), rec.getAbsoluteFile())) {
|
||||
rec.setAbsoluteFile(remuxedFile);
|
||||
}
|
||||
} else {
|
||||
Files.deleteIfExists(inputFile.toPath());
|
||||
if (Objects.equals(inputFile, rec.getAbsoluteFile())) {
|
||||
rec.setAbsoluteFile(remuxedFile);
|
||||
}
|
||||
}
|
||||
rec.setSingleFile(true);
|
||||
rec.setSizeInByte(remuxedFile.length());
|
||||
IoUtils.deleteEmptyParents(inputFile.getParentFile());
|
||||
rec.getAssociatedFiles().remove(inputFile.getCanonicalPath());
|
||||
rec.getAssociatedFiles().add(remuxedFile.getCanonicalPath());
|
||||
} catch (IOException e) {
|
||||
LOG.error("Couldn't finalize remux post-processing step", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String[] prepareCommandline(File inputFile, File remuxedFile) throws IOException {
|
||||
String[] args = getConfig().get(FFMPEG_ARGS).split(" ");
|
||||
String[] argsPlusFile = new String[args.length + 3];
|
||||
File inputFile = rec.getPostProcessedFile();
|
||||
if (inputFile.isDirectory()) {
|
||||
inputFile = new File(inputFile, "playlist.m3u8");
|
||||
}
|
||||
int i = 0;
|
||||
argsPlusFile[i++] = "-i";
|
||||
argsPlusFile[i++] = inputFile.getCanonicalPath();
|
||||
System.arraycopy(args, 0, argsPlusFile, i, args.length);
|
||||
File remuxedFile = new File(rec.getPostProcessedFile().getAbsolutePath() + '.' + fileExt);
|
||||
argsPlusFile[argsPlusFile.length - 1] = remuxedFile.getAbsolutePath();
|
||||
String[] cmdline = OS.getFFmpegCommand(argsPlusFile);
|
||||
File executionDir = rec.getPostProcessedFile().isDirectory() ? rec.getPostProcessedFile() : rec.getPostProcessedFile().getParentFile();
|
||||
LOG.info("Executing {} in working directory {}", Arrays.toString(cmdline), executionDir);
|
||||
Process ffmpeg = Runtime.getRuntime().exec(cmdline, new String[0], executionDir);
|
||||
setupLogging(ffmpeg, rec);
|
||||
rec.setPostProcessedFile(remuxedFile);
|
||||
if (inputFile.getName().equals("playlist.m3u8")) {
|
||||
IoUtils.deleteDirectory(inputFile.getParentFile());
|
||||
if (Objects.equals(inputFile.getParentFile(), rec.getAbsoluteFile())) {
|
||||
rec.setAbsoluteFile(remuxedFile);
|
||||
}
|
||||
} else {
|
||||
Files.deleteIfExists(inputFile.toPath());
|
||||
if (Objects.equals(inputFile, rec.getAbsoluteFile())) {
|
||||
rec.setAbsoluteFile(remuxedFile);
|
||||
}
|
||||
}
|
||||
rec.setSingleFile(true);
|
||||
rec.setSizeInByte(remuxedFile.length());
|
||||
IoUtils.deleteEmptyParents(inputFile.getParentFile());
|
||||
rec.getAssociatedFiles().remove(inputFile.getCanonicalPath());
|
||||
rec.getAssociatedFiles().add(remuxedFile.getCanonicalPath());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setupLogging(Process ffmpeg, Recording rec) throws IOException, InterruptedException {
|
||||
int exitCode = 1;
|
||||
File video = rec.getPostProcessedFile();
|
||||
File ffmpegLog = new File(video.getParentFile(), video.getName() + ".ffmpeg.log");
|
||||
rec.getAssociatedFiles().add(ffmpegLog.getCanonicalPath());
|
||||
try (FileOutputStream mergeLogStream = new FileOutputStream(ffmpegLog)) {
|
||||
Thread stdout = new Thread(new StreamRedirectThread(ffmpeg.getInputStream(), mergeLogStream));
|
||||
Thread stderr = new Thread(new StreamRedirectThread(ffmpeg.getErrorStream(), mergeLogStream));
|
||||
stdout.start();
|
||||
stderr.start();
|
||||
exitCode = ffmpeg.waitFor();
|
||||
LOG.debug("FFmpeg exited with code {}", exitCode);
|
||||
stdout.join();
|
||||
stderr.join();
|
||||
mergeLogStream.flush();
|
||||
}
|
||||
if (exitCode != 1) {
|
||||
if (ffmpegLog.exists()) {
|
||||
Files.delete(ffmpegLog.toPath());
|
||||
rec.getAssociatedFiles().remove(ffmpegLog.getCanonicalPath());
|
||||
}
|
||||
} else {
|
||||
rec.getAssociatedFiles().add(ffmpegLog.getAbsolutePath());
|
||||
LOG.info("FFmpeg exit code was {}. Logfile: {}", exitCode, ffmpegLog.getAbsolutePath());
|
||||
throw new ProcessExitedUncleanException("FFmpeg exit code was " + exitCode);
|
||||
}
|
||||
return OS.getFFmpegCommand(argsPlusFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -13,7 +13,7 @@ import org.slf4j.LoggerFactory;
|
|||
import ctbrec.Config;
|
||||
import ctbrec.OS;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.io.StreamRedirectThread;
|
||||
import ctbrec.io.StreamRedirector;
|
||||
import ctbrec.recorder.RecordingManager;
|
||||
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||
|
||||
|
@ -61,11 +61,11 @@ public class Script extends AbstractPlaceholderAwarePostProcessor {
|
|||
}
|
||||
|
||||
private void startLogging(Process process, FileOutputStream logStream) {
|
||||
Thread std = new Thread(new StreamRedirectThread(process.getInputStream(), logStream));
|
||||
Thread std = new Thread(new StreamRedirector(process.getInputStream(), logStream));
|
||||
std.setName("Process stdout pipe");
|
||||
std.setDaemon(true);
|
||||
std.start();
|
||||
Thread err = new Thread(new StreamRedirectThread(process.getErrorStream(), logStream));
|
||||
Thread err = new Thread(new StreamRedirector(process.getErrorStream(), logStream));
|
||||
err.setName("Process stderr pipe");
|
||||
err.setDaemon(true);
|
||||
err.start();
|
||||
|
|
|
@ -2,9 +2,6 @@ package ctbrec.recorder.postprocessing;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.NotImplementedExcetion;
|
||||
import ctbrec.Recording;
|
||||
|
@ -12,7 +9,6 @@ import ctbrec.recorder.RecordingManager;
|
|||
|
||||
public class Webhook extends AbstractPlaceholderAwarePostProcessor {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Webhook.class);
|
||||
public static final String URL = "webhook.url";
|
||||
public static final String HEADERS = "webhook.headers";
|
||||
public static final String METHOD = "webhook.method";
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package ctbrec.sites;
|
||||
|
||||
import ctbrec.Model;
|
||||
|
||||
public class ModelOfflineException extends RuntimeException {
|
||||
|
||||
public ModelOfflineException(Model model) {
|
||||
super("Model " + model + " is offline");
|
||||
}
|
||||
}
|
|
@ -2,16 +2,19 @@ package ctbrec.sites.cam4;
|
|||
|
||||
import static ctbrec.Model.State.*;
|
||||
import static ctbrec.io.HttpConstants.*;
|
||||
import static java.util.regex.Pattern.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -50,60 +53,48 @@ public class Cam4Model extends AbstractModel {
|
|||
if (ignoreCache || onlineState == UNKNOWN) {
|
||||
try {
|
||||
loadModelDetails();
|
||||
} catch (ModelDetailsEmptyException e) {
|
||||
return false;
|
||||
getPlaylistUrl();
|
||||
} catch (Exception e) {
|
||||
onlineState = OFFLINE;
|
||||
}
|
||||
}
|
||||
return onlineState == ONLINE && !privateRoom && playlistUrl != null && !playlistUrl.isEmpty();
|
||||
}
|
||||
|
||||
private void loadModelDetails() throws IOException, ModelDetailsEmptyException {
|
||||
String url = site.getBaseUrl() + "/getBroadcasting?usernames=" + getName();
|
||||
LOG.trace("Loading model details {}", url);
|
||||
Request req = new Request.Builder().url(url).build();
|
||||
try (Response response = site.getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
JSONArray json = new JSONArray(response.body().string());
|
||||
if (json.length() == 0) {
|
||||
onlineState = OFFLINE;
|
||||
throw new ModelDetailsEmptyException("Model details are empty");
|
||||
}
|
||||
JSONObject details = json.getJSONObject(0);
|
||||
String showType = details.getString("showType");
|
||||
setOnlineStateByShowType(showType);
|
||||
playlistUrl = details.getString("hlsPreviewUrl");
|
||||
privateRoom = details.getBoolean("privateRoom");
|
||||
if (privateRoom) {
|
||||
onlineState = PRIVATE;
|
||||
}
|
||||
if (details.has("resolution")) {
|
||||
String res = details.getString("resolution");
|
||||
String[] tokens = res.split(":");
|
||||
resolution = new int[] { Integer.parseInt(tokens[0]), Integer.parseInt(tokens[1]) };
|
||||
}
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
}
|
||||
private void loadModelDetails() throws IOException {
|
||||
JSONObject roomState = new Cam4WsClient(Config.getInstance(), (Cam4)getSite(), this).getRoomState();
|
||||
if(LOG.isTraceEnabled()) LOG.trace(roomState.toString(2));
|
||||
String state = roomState.optString("newShowsState");
|
||||
setOnlineStateByShowType(state);
|
||||
privateRoom = roomState.optBoolean("privateRoom");
|
||||
setDescription(roomState.optString("status"));
|
||||
}
|
||||
|
||||
public void setOnlineStateByShowType(String showType) {
|
||||
switch(showType) {
|
||||
case "NORMAL":
|
||||
case "ACCEPTING":
|
||||
case "GROUP_SHOW_SELLING_TICKETS":
|
||||
case "GS_SELLING_TICKETS":
|
||||
case "GS_SELLING_TICKETS_UNSUCCESSFUL":
|
||||
onlineState = ONLINE;
|
||||
break;
|
||||
case "PRIVATE_SHOW":
|
||||
case "INSIDE_PS":
|
||||
onlineState = PRIVATE;
|
||||
break;
|
||||
case "INSIDE_GS":
|
||||
case "GROUP_SHOW":
|
||||
onlineState = GROUP;
|
||||
break;
|
||||
case "PAUSED":
|
||||
onlineState = AWAY;
|
||||
break;
|
||||
case "OFFLINE":
|
||||
onlineState = OFFLINE;
|
||||
break;
|
||||
default:
|
||||
LOG.debug("Unknown show type [{}]", showType);
|
||||
LOG.debug("############################## Unknown show type [{}]", showType);
|
||||
onlineState = UNKNOWN;
|
||||
}
|
||||
|
||||
|
@ -117,7 +108,7 @@ public class Cam4Model extends AbstractModel {
|
|||
if(onlineState == UNKNOWN) {
|
||||
try {
|
||||
loadModelDetails();
|
||||
} catch (ModelDetailsEmptyException e) {
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Couldn't load model details {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
@ -126,19 +117,58 @@ public class Cam4Model extends AbstractModel {
|
|||
}
|
||||
|
||||
private String getPlaylistUrl() throws IOException {
|
||||
if(playlistUrl == null || playlistUrl.trim().isEmpty()) {
|
||||
try {
|
||||
loadModelDetails();
|
||||
if (playlistUrl == null) {
|
||||
throw new IOException("Couldn't determine playlist url");
|
||||
}
|
||||
} catch (ModelDetailsEmptyException e) {
|
||||
throw new IOException(e);
|
||||
if (playlistUrl == null || playlistUrl.trim().isEmpty()) {
|
||||
String page = loadModelPage();
|
||||
Matcher m = Pattern.compile("hlsUrl\\s*:\\s*'(.*?)'", DOTALL | MULTILINE).matcher(page);
|
||||
if (m.find()) {
|
||||
playlistUrl = m.group(1);
|
||||
} else {
|
||||
getPlaylistUrlFromStreamUrl();
|
||||
}
|
||||
if (playlistUrl == null) {
|
||||
throw new IOException("Couldn't determine playlist url");
|
||||
}
|
||||
}
|
||||
return playlistUrl;
|
||||
}
|
||||
|
||||
private void getPlaylistUrlFromStreamUrl() throws IOException {
|
||||
String url = getSite().getBaseUrl() + "/_profile/streamURL?username=" + getName();
|
||||
Request req = new Request.Builder() // @formatter:off
|
||||
.url(url)
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
.header(ACCEPT, "*/*")
|
||||
.header(ACCEPT_LANGUAGE, "*")
|
||||
.header(REFERER, getUrl())
|
||||
.build(); // @formatter:on
|
||||
try (Response response = site.getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
JSONObject json = new JSONObject(response.body().string());
|
||||
LOG.trace(json.toString(2));
|
||||
if (json.has("canUseCDN")) {
|
||||
if (json.getBoolean("canUseCDN")) {
|
||||
playlistUrl = json.getString("cdnURL");
|
||||
} else {
|
||||
playlistUrl = json.getString("edgeURL");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String loadModelPage() throws IOException {
|
||||
Request req = new Request.Builder().url(getUrl()).build();
|
||||
try (Response response = site.getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
return response.body().string();
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
MasterPlaylist masterPlaylist = getMasterPlaylist();
|
||||
|
@ -160,8 +190,9 @@ public class Cam4Model extends AbstractModel {
|
|||
}
|
||||
|
||||
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
|
||||
LOG.debug("Loading master playlist [{}]", getPlaylistUrl());
|
||||
Request req = new Request.Builder().url(getPlaylistUrl()).build();
|
||||
String playlistUrl = getPlaylistUrl();
|
||||
LOG.trace("Loading master playlist [{}]", playlistUrl);
|
||||
Request req = new Request.Builder().url(playlistUrl).build();
|
||||
|
||||
try (Response response = site.getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
|
@ -171,7 +202,7 @@ public class Cam4Model extends AbstractModel {
|
|||
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||
return master;
|
||||
} else {
|
||||
throw new HttpException(response.code(), "Couldn't download HLS playlist");
|
||||
throw new HttpException(response.code(), "Couldn't download HLS playlist " + playlistUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -192,19 +223,27 @@ public class Cam4Model extends AbstractModel {
|
|||
if(resolution == null) {
|
||||
if(failFast) {
|
||||
return new int[2];
|
||||
} else {
|
||||
try {
|
||||
if(onlineState != OFFLINE) {
|
||||
loadModelDetails();
|
||||
} else {
|
||||
resolution = new int[2];
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new ExecutionException(e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
if(!isOnline()) {
|
||||
return new int[2];
|
||||
}
|
||||
List<StreamSource> sources = getStreamSources();
|
||||
Collections.sort(sources);
|
||||
StreamSource best = sources.get(sources.size()-1);
|
||||
resolution = new int[] {best.width, best.height};
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
|
||||
resolution = new int[2];
|
||||
} catch (ExecutionException | IOException | ParseException | PlaylistException e) {
|
||||
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
|
||||
resolution = new int[2];
|
||||
}
|
||||
return resolution;
|
||||
} else {
|
||||
return resolution;
|
||||
}
|
||||
return resolution;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -276,4 +315,13 @@ public class Cam4Model extends AbstractModel {
|
|||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUrl(String url) {
|
||||
String normalizedUrl = url.toLowerCase();
|
||||
if (normalizedUrl.endsWith("/")) {
|
||||
normalizedUrl = normalizedUrl.substring(0, normalizedUrl.length() - 1);
|
||||
}
|
||||
super.setUrl(normalizedUrl);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
package ctbrec.sites.cam4;
|
||||
|
||||
import static ctbrec.io.HttpConstants.*;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.sites.ModelOfflineException;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.WebSocket;
|
||||
import okhttp3.WebSocketListener;
|
||||
import okio.ByteString;
|
||||
|
||||
public class Cam4WsClient {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Cam4WsClient.class);
|
||||
|
||||
private Cam4 site;
|
||||
private Cam4Model model;
|
||||
private Config config;
|
||||
private String shard;
|
||||
private String token;
|
||||
private WebSocket websocket;
|
||||
private int r = 1;
|
||||
private Map<String, CompletableFuture<String>> responseFuturesByPath = new HashMap<>();
|
||||
private Map<Integer, CompletableFuture<String>> responseFuturesBySequence = new HashMap<>();
|
||||
|
||||
public Cam4WsClient(Config config, Cam4 site, Cam4Model model) {
|
||||
this.config = config;
|
||||
this.site = site;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public JSONObject getRoomState() throws IOException {
|
||||
requestAccessToken();
|
||||
if (connectAndAuthorize()) {
|
||||
return requestRoomState();
|
||||
} else {
|
||||
throw new IOException("Connect or authorize failed");
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject requestRoomState() throws IOException {
|
||||
String p = "chatRooms/" + model.getName() + "/roomState";
|
||||
CompletableFuture<String> roomStateFuture = send(p, "{\"t\":\"d\",\"d\":{\"r\":" + (r++) + ",\"a\":\"q\",\"b\":{\"p\":\"" + p + "\",\"h\":\"\"}}}");
|
||||
try {
|
||||
JSONObject roomState = parseRoomStateResponse(roomStateFuture.get(1, TimeUnit.SECONDS));
|
||||
websocket.close(1000, "");
|
||||
return roomState;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Interrupted while getting room state with websocket");
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean connectAndAuthorize() throws IOException {
|
||||
CompletableFuture<Boolean> connectedAndAuthorized = openWebsocketConnection();
|
||||
try {
|
||||
return connectedAndAuthorized.get(1, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("Interrupted while connecting with websocket");
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<String> send(String p, String msg) {
|
||||
CompletableFuture<String> future = new CompletableFuture<>();
|
||||
LOG.trace("--> {}", msg);
|
||||
boolean sent = websocket.send(msg);
|
||||
if (!sent) {
|
||||
future.completeExceptionally(new IOException("send() returned false"));
|
||||
} else {
|
||||
responseFuturesByPath.put(p, future);
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
private void requestAccessToken() throws IOException {
|
||||
Request req = new Request.Builder() // @formatter:off
|
||||
.url("https://webchat.cam4.com/requestAccess?roomname=" + model.getName())
|
||||
.header(USER_AGENT, config.getSettings().httpUserAgent)
|
||||
.header(REFERER, Cam4.BASE_URI + '/' + model.getName())
|
||||
.header(ORIGIN, Cam4.BASE_URI)
|
||||
.header(ACCEPT, "*/*")
|
||||
.build(); // @formatter:on
|
||||
try (Response response = site.getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
JSONObject body = new JSONObject(response.body().string());
|
||||
if (body.optString("status").equals("success")) {
|
||||
shard = body.getString("shard").replace("https", "wss");
|
||||
token = body.getString("token");
|
||||
} else {
|
||||
throw new ModelOfflineException(model);
|
||||
}
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject parseRoomStateResponse(String msg) {
|
||||
JSONObject json = new JSONObject(msg);
|
||||
JSONObject d = json.getJSONObject("d");
|
||||
JSONObject b = d.getJSONObject("b");
|
||||
return b.getJSONObject("d");
|
||||
}
|
||||
|
||||
private CompletableFuture<Boolean> openWebsocketConnection() {
|
||||
CompletableFuture<Boolean> connectedAndAuthorized = new CompletableFuture<>();
|
||||
|
||||
String url = shard + ".ws?v=5";
|
||||
LOG.trace("Opening websocket {}", url);
|
||||
Request req = new Request.Builder() // @formatter:off
|
||||
.url(url)
|
||||
.header(USER_AGENT, config.getSettings().httpUserAgent)
|
||||
.header(REFERER, Cam4.BASE_URI + '/' + model.getName())
|
||||
.header(ORIGIN, Cam4.BASE_URI)
|
||||
.header(ACCEPT, "*/*")
|
||||
.build(); // @formatter:on
|
||||
|
||||
websocket = site.getHttpClient().newWebSocket(req, new WebSocketListener() {
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket, Response response) {
|
||||
super.onOpen(webSocket, response);
|
||||
try {
|
||||
LOG.trace("open: {}", response.body().string());
|
||||
} catch (IOException e) {
|
||||
LOG.error("Connection open, but couldn't get the response body", e);
|
||||
}
|
||||
send("", "{\"t\":\"d\",\"d\":{\"r\":" + (r++) + ",\"a\":\"s\",\"b\":{\"c\":{\"sdk.js.2-3-1\":1}}}}");
|
||||
send("", "{\"t\":\"d\",\"d\":{\"r\":" + (r++) + ",\"a\":\"auth\",\"b\":{\"cred\":\"" + token + "\"}}}");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosed(WebSocket webSocket, int code, String reason) {
|
||||
super.onClosed(webSocket, code, reason);
|
||||
LOG.trace("closed: {} {}", code, reason);
|
||||
connectedAndAuthorized.complete(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
||||
super.onFailure(webSocket, t, response);
|
||||
try {
|
||||
if (t instanceof EOFException) {
|
||||
return;
|
||||
}
|
||||
if(response != null) {
|
||||
LOG.error("failure {}: {}", model, response.body().string(), t);
|
||||
} else {
|
||||
LOG.error("failure {}:", model, t);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Connection failure and couldn't get the response body", e);
|
||||
} finally {
|
||||
connectedAndAuthorized.completeExceptionally(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, String text) {
|
||||
super.onMessage(webSocket, text);
|
||||
LOG.trace("msgt: {}", text);
|
||||
JSONObject response = new JSONObject(text);
|
||||
if (response.has("d")) {
|
||||
JSONObject d = response.getJSONObject("d");
|
||||
int responseSequence = d.optInt("r");
|
||||
if (responseSequence == 2) {
|
||||
JSONObject body = d.getJSONObject("b");
|
||||
String status = body.optString("s");
|
||||
connectedAndAuthorized.complete(status.equals("ok"));
|
||||
} else if (responseFuturesBySequence.containsKey(responseSequence)) {
|
||||
JSONObject body = d.getJSONObject("b");
|
||||
String status = body.optString("s");
|
||||
if (!status.equals("ok")) {
|
||||
CompletableFuture<String> future = responseFuturesBySequence.remove(responseSequence);
|
||||
future.completeExceptionally(new IOException(status));
|
||||
}
|
||||
} else if (d.has("b")) {
|
||||
JSONObject body = d.getJSONObject("b");
|
||||
String p = body.optString("p", "-");
|
||||
if (responseFuturesByPath.containsKey(p)) {
|
||||
CompletableFuture<String> future = responseFuturesByPath.remove(p);
|
||||
future.complete(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket webSocket, ByteString bytes) {
|
||||
super.onMessage(webSocket, bytes);
|
||||
LOG.trace("msgb: {}", bytes.hex());
|
||||
}
|
||||
});
|
||||
return connectedAndAuthorized;
|
||||
}
|
||||
}
|
|
@ -47,6 +47,7 @@ public class CamsodaModel extends AbstractModel {
|
|||
private static final Logger LOG = LoggerFactory.getLogger(CamsodaModel.class);
|
||||
private transient List<StreamSource> streamSources = null;
|
||||
private transient boolean isNew;
|
||||
private transient String gender;
|
||||
|
||||
private float sortOrder = 0;
|
||||
private Random random = new Random();
|
||||
|
@ -344,4 +345,12 @@ public class CamsodaModel extends AbstractModel {
|
|||
public void setNew(boolean isNew) {
|
||||
this.isNew = isNew;
|
||||
}
|
||||
|
||||
public String getGender() {
|
||||
return gender;
|
||||
}
|
||||
|
||||
public void setGender(String gender) {
|
||||
this.gender = gender;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ public class Chaturbate extends AbstractSite {
|
|||
|
||||
@Override
|
||||
public Double getTokenBalance() throws IOException {
|
||||
String username = Config.getInstance().getSettings().username;
|
||||
String username = Config.getInstance().getSettings().chaturbateUsername;
|
||||
if (username == null || username.trim().isEmpty()) {
|
||||
throw new IOException("Not logged in");
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ public class Chaturbate extends AbstractSite {
|
|||
|
||||
@Override
|
||||
public boolean credentialsAvailable() {
|
||||
String username = Config.getInstance().getSettings().username;
|
||||
String username = Config.getInstance().getSettings().chaturbateUsername;
|
||||
return username != null && !username.trim().isEmpty();
|
||||
}
|
||||
|
||||
|
|
|
@ -67,8 +67,8 @@ public class ChaturbateHttpClient extends HttpClient {
|
|||
LOG.debug("csrf token is {}", token);
|
||||
|
||||
RequestBody body = new FormBody.Builder()
|
||||
.add("username", Config.getInstance().getSettings().username)
|
||||
.add("password", Config.getInstance().getSettings().password)
|
||||
.add("username", Config.getInstance().getSettings().chaturbateUsername)
|
||||
.add("password", Config.getInstance().getSettings().chaturbatePassword)
|
||||
.add("next", "")
|
||||
.add("csrfmiddlewaretoken", token)
|
||||
.build();
|
||||
|
@ -103,7 +103,7 @@ public class ChaturbateHttpClient extends HttpClient {
|
|||
}
|
||||
|
||||
private boolean checkLogin() throws IOException {
|
||||
String url = "https://chaturbate.com/p/" + Config.getInstance().getSettings().username + "/";
|
||||
String url = "https://chaturbate.com/p/" + Config.getInstance().getSettings().chaturbateUsername + "/";
|
||||
Request req = new Request.Builder()
|
||||
.url(url)
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
|
|
|
@ -28,8 +28,8 @@ import okhttp3.Response;
|
|||
|
||||
public class MVLive extends AbstractSite {
|
||||
|
||||
public static final String WS_URL = "wss://live.manyvids.com";
|
||||
//public static final String WS_URL = "http://localhost:8080";
|
||||
public static final String APP_HOST = "app-v1.live.manyvids.com";
|
||||
public static final String WS_URL = "wss://" + APP_HOST;
|
||||
public static final String WS_ORIGIN = "https://live.manyvids.com";
|
||||
public static final String BASE_URL = "https://www.manyvids.com/MVLive/";
|
||||
|
||||
|
@ -111,7 +111,8 @@ public class MVLive extends AbstractSite {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void init() throws IOException {
|
||||
public void init() {
|
||||
// nothing special to do for manyvids
|
||||
}
|
||||
|
||||
public List<Model> getModels() throws IOException {
|
||||
|
@ -175,9 +176,8 @@ public class MVLive extends AbstractSite {
|
|||
String getMvToken() throws IOException {
|
||||
if (mvtoken == null) {
|
||||
Request request = new Request.Builder()
|
||||
.url(getBaseUrl())
|
||||
.url("https://www.manyvids.com/")
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
.header(REFERER, MVLive.BASE_URL)
|
||||
.build();
|
||||
try (Response response = getHttpClient().execute(request)) {
|
||||
if (response.isSuccessful()) {
|
||||
|
@ -245,7 +245,7 @@ public class MVLive extends AbstractSite {
|
|||
|
||||
@Override
|
||||
public Model createModelFromUrl(String url) {
|
||||
Matcher m = Pattern.compile("https://live.manyvids.com/stream/(.*?)(?:/.*?)?").matcher(url.trim());
|
||||
Matcher m = Pattern.compile("https://live.manyvids.com/(?:stream/)?(.*?)(?:/.*?)?").matcher(url.trim());
|
||||
if(m.matches()) {
|
||||
return createModel(m.group(1));
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import org.slf4j.LoggerFactory;
|
|||
import com.google.common.base.Objects;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.sites.manyvids.wsmsg.GetBroadcastHealth;
|
||||
import ctbrec.sites.manyvids.wsmsg.Message;
|
||||
import ctbrec.sites.manyvids.wsmsg.Ping;
|
||||
|
@ -40,17 +39,18 @@ public class MVLiveClient {
|
|||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MVLiveClient.class);
|
||||
|
||||
private final Map<String, Message> futureResponses = new HashMap<>();
|
||||
private final MVLiveHttpClient httpClient;
|
||||
private final Object streamUrlMonitor = new Object();
|
||||
private final Random rng = new Random();
|
||||
|
||||
private WebSocket ws;
|
||||
private Random rng = new Random();
|
||||
private volatile boolean running = false;
|
||||
private volatile boolean connecting = false;
|
||||
private Object streamUrlMonitor = new Object();
|
||||
private String masterPlaylist = null;
|
||||
private String roomNumber;
|
||||
private String roomId;
|
||||
private ScheduledExecutorService scheduler;
|
||||
private Map<String, Message> futureResponses = new HashMap<>();
|
||||
private MVLiveHttpClient httpClient;
|
||||
|
||||
public MVLiveClient(MVLiveHttpClient httpClient) {
|
||||
this.httpClient = httpClient;
|
||||
|
@ -61,7 +61,7 @@ public class MVLiveClient {
|
|||
|
||||
if (ws == null && !connecting) {
|
||||
httpClient.fetchAuthenticationCookies();
|
||||
JSONObject response = getRoomLocation(model);
|
||||
JSONObject response = model.getRoomLocation();
|
||||
roomNumber = response.optString("floorId");
|
||||
roomId = response.optString("roomId");
|
||||
int randomNumber = 100 + rng.nextInt(800);
|
||||
|
@ -73,22 +73,6 @@ public class MVLiveClient {
|
|||
}
|
||||
}
|
||||
|
||||
private JSONObject getRoomLocation(MVLiveModel model) throws IOException {
|
||||
Request req = new Request.Builder()
|
||||
.url(WS_ORIGIN + "/api/roomlocation/" + model.getDisplayName() + "?private=false")
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||
.header(COOKIE, getPhpSessionIdCookie())
|
||||
.build();
|
||||
try (Response response = httpClient.execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
return new JSONObject(response.body().string());
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getPhpSessionIdCookie() {
|
||||
List<Cookie> cookies = httpClient.getCookiesByName("PHPSESSID");
|
||||
return cookies.stream().map(c -> c.name() + "=" + c.value()).findFirst().orElse("");
|
||||
|
@ -96,7 +80,7 @@ public class MVLiveClient {
|
|||
|
||||
public void stop() {
|
||||
running = false;
|
||||
scheduler.shutdown();
|
||||
Optional.ofNullable(scheduler).ifPresent(ScheduledExecutorService::shutdown);
|
||||
ws.close(1000, "Good Bye"); // terminate normally (1000)
|
||||
ws = null;
|
||||
}
|
||||
|
@ -109,7 +93,7 @@ public class MVLiveClient {
|
|||
.header(ORIGIN, WS_ORIGIN)
|
||||
.header(COOKIE, getPhpSessionIdCookie())
|
||||
.build();
|
||||
WebSocket websocket = httpClient.newWebSocket(req, new WebSocketListener() {
|
||||
return httpClient.newWebSocket(req, new WebSocketListener() {
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket, Response response) {
|
||||
super.onOpen(webSocket, response);
|
||||
|
@ -157,7 +141,6 @@ public class MVLiveClient {
|
|||
@Override
|
||||
public void onMessage(WebSocket webSocket, String text) {
|
||||
super.onMessage(webSocket, text);
|
||||
//msgBuffer.append(text);
|
||||
LOG.trace("Message: {}", text);
|
||||
text = Optional.ofNullable(text).orElse("");
|
||||
if (Objects.equal("o", text)) {
|
||||
|
@ -202,7 +185,6 @@ public class MVLiveClient {
|
|||
LOG.debug("Binary Message: {}", bytes.hex());
|
||||
}
|
||||
});
|
||||
return websocket;
|
||||
}
|
||||
|
||||
void sendMessages(Message... messages) {
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
package ctbrec.sites.manyvids;
|
||||
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
|
||||
|
||||
public class MVLiveMergedHlsDownload extends MergedFfmpegHlsDownload {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MVLiveMergedHlsDownload.class);
|
||||
|
||||
private ScheduledExecutorService scheduler;
|
||||
private transient ScheduledExecutorService scheduler;
|
||||
|
||||
public MVLiveMergedHlsDownload(HttpClient client) {
|
||||
super(client);
|
||||
|
@ -31,7 +30,7 @@ public class MVLiveMergedHlsDownload extends MergedFfmpegHlsDownload {
|
|||
t.setPriority(Thread.MIN_PRIORITY);
|
||||
return t;
|
||||
});
|
||||
scheduler.scheduleAtFixedRate(() -> updateCloudFlareCookies(), 2, 2, TimeUnit.MINUTES);
|
||||
scheduler.scheduleAtFixedRate(this::updateCloudFlareCookies, 2, 2, TimeUnit.MINUTES);
|
||||
updateCloudFlareCookies();
|
||||
super.start();
|
||||
} finally {
|
||||
|
|
|
@ -2,6 +2,7 @@ package ctbrec.sites.manyvids;
|
|||
|
||||
import static ctbrec.Model.State.*;
|
||||
import static ctbrec.io.HttpConstants.*;
|
||||
import static ctbrec.sites.manyvids.MVLive.*;
|
||||
import static java.nio.charset.StandardCharsets.*;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
@ -116,8 +117,8 @@ public class MVLiveModel extends AbstractModel {
|
|||
}
|
||||
|
||||
public void updateCloudFlareCookies() throws IOException, InterruptedException {
|
||||
String url = MVLive.WS_ORIGIN + "/api/" + getRoomNumber() + "/player-settings/" + getDisplayName();
|
||||
LOG.trace("Getting CF cookies: {}", url);
|
||||
String url = "https://" + APP_HOST + "/api/" + getRoomNumber() + "/player-settings/" + getDisplayName();
|
||||
LOG.debug("Getting CF cookies: {}", url);
|
||||
Request req = new Request.Builder()
|
||||
.url(url)
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
|
@ -158,8 +159,8 @@ public class MVLiveModel extends AbstractModel {
|
|||
public JSONObject getRoomLocation() throws IOException {
|
||||
fetchGeneralCookies();
|
||||
httpClient.fetchAuthenticationCookies();
|
||||
String url = MVLive.WS_ORIGIN + "/api/roomlocation/" + getDisplayName() + "?private=false";
|
||||
LOG.trace("Fetching room location from {}", url);
|
||||
String url = "https://roompool.live.manyvids.com/roompool/" + getDisplayName() + "?private=false";
|
||||
LOG.debug("Fetching room location from {}", url);
|
||||
Request req = new Request.Builder()
|
||||
.url(url)
|
||||
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||
|
@ -169,8 +170,9 @@ public class MVLiveModel extends AbstractModel {
|
|||
.build();
|
||||
try (Response response = getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
JSONObject json = new JSONObject(response.body().string());
|
||||
LOG.trace("Room location response: {}", json.toString(2));
|
||||
String body = response.body().string();
|
||||
JSONObject json = new JSONObject(body);
|
||||
LOG.trace("Room location response: {}", json);
|
||||
return json;
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
|
|
|
@ -105,7 +105,7 @@ public class MyFreeCamsClient {
|
|||
}
|
||||
|
||||
String server = websocketServers.get(new Random().nextInt(websocketServers.size() - 1));
|
||||
String wsUrl = "ws://" + server + ".myfreecams.com:8080/fcsl";
|
||||
String wsUrl = "wss://" + server + ".myfreecams.com/fcsl";
|
||||
LOG.debug("Connecting to random websocket server {}", wsUrl);
|
||||
|
||||
Thread watchDog = new Thread(() -> {
|
||||
|
@ -664,6 +664,7 @@ public class MyFreeCamsClient {
|
|||
return camservString;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private boolean isBroadcasterOnWebRTC(SessionState state) {
|
||||
return (Optional.ofNullable(state).map(SessionState::getM).map(Model::getFlags).orElse(0) & 524288) == 524288;
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@ package ctbrec.sites.showup;
|
|||
import static ctbrec.io.HttpConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
@ -12,23 +10,22 @@ import java.util.Map;
|
|||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Settings;
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.io.HttpException;
|
||||
import okhttp3.Cookie;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class ShowupHttpClient extends HttpClient {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ShowupHttpClient.class);
|
||||
private String csrfToken;
|
||||
private boolean loggedIn = false;
|
||||
|
||||
protected ShowupHttpClient() {
|
||||
super("showup");
|
||||
|
@ -79,38 +76,40 @@ public class ShowupHttpClient extends HttpClient {
|
|||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean login() throws IOException {
|
||||
if (loggedIn) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// boolean cookiesWorked = checkLoginSuccess();
|
||||
// if (cookiesWorked) {
|
||||
// loggedIn = true;
|
||||
// LOG.debug("Logged in with cookies");
|
||||
// return true;
|
||||
// }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean checkLoginSuccess() throws IOException {
|
||||
public boolean login() throws IOException {
|
||||
Settings settings = Config.getInstance().getSettings();
|
||||
FormBody body = new FormBody.Builder()
|
||||
.add("is_ajax", "1")
|
||||
.add("email", settings.showupUsername)
|
||||
.add("password", settings.showupPassword)
|
||||
.add("remember", "1")
|
||||
.build();
|
||||
Request req = new Request.Builder()
|
||||
.url(Showup.BASE_URL + "/site/messages")
|
||||
//.url("http://dingens.showup.tv:1234/site/messages")
|
||||
.header(ACCEPT, "*/*")
|
||||
.url(Showup.BASE_URL)
|
||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
|
||||
.header(REFERER, Showup.BASE_URL + '/')
|
||||
.header(ORIGIN, Showup.BASE_URL)
|
||||
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||
.post(body)
|
||||
.build();
|
||||
|
||||
try (Response response = execute(req)) {
|
||||
if (response.isSuccessful() && response.code() == 200) {
|
||||
Files.write(Paths.get("/tmp/messages.html"), response.body().bytes());
|
||||
//return true;
|
||||
return false;
|
||||
if (response.isSuccessful()) {
|
||||
String responseBody = response.body().string();
|
||||
if (responseBody.startsWith("{")) {
|
||||
JSONObject json = new JSONObject(responseBody);
|
||||
return json.optString("status").equalsIgnoreCase("success");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean checkLoginSuccess() {
|
||||
return loggedIn;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ public class ShowupMergedDownload extends MergedFfmpegHlsDownload {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
|
||||
protected void startDownloadLoop(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
|
||||
try {
|
||||
SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri);
|
||||
emptyPlaylistCheck(lsp);
|
||||
|
@ -48,8 +48,8 @@ public class ShowupMergedDownload extends MergedFfmpegHlsDownload {
|
|||
BandwidthMeter.add(length);
|
||||
writeSegment(buffer, 0, length);
|
||||
keepGoing = running && !Thread.interrupted() && model.isOnline(true);
|
||||
if (livestreamDownload && splitRecording()) {
|
||||
break;
|
||||
if (livestreamDownload) {
|
||||
splitRecordingIfNecessary();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -179,7 +179,7 @@ public class Streamate extends AbstractSite {
|
|||
|
||||
@Override
|
||||
public boolean credentialsAvailable() {
|
||||
String username = Config.getInstance().getSettings().username;
|
||||
String username = Config.getInstance().getSettings().streamateUsername;
|
||||
return StringUtil.isNotBlank(username);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,14 +7,16 @@ import java.io.IOException;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.xml.bind.JAXBException;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
|
@ -29,6 +31,7 @@ import okhttp3.RequestBody;
|
|||
import okhttp3.Response;
|
||||
|
||||
public class StripchatModel extends AbstractModel {
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(StripchatModel.class);
|
||||
private String status = null;
|
||||
private int[] resolution = new int[] {0, 0};
|
||||
|
||||
|
@ -39,13 +42,33 @@ public class StripchatModel extends AbstractModel {
|
|||
if (jsonResponse.has("user")) {
|
||||
JSONObject user = jsonResponse.getJSONObject("user");
|
||||
status = user.optString("status");
|
||||
mapOnlineState(status);
|
||||
}
|
||||
}
|
||||
boolean online = Objects.equals(status, "public");
|
||||
if (online) {
|
||||
return onlineState == ONLINE;
|
||||
}
|
||||
|
||||
private void mapOnlineState(String status) {
|
||||
switch (status) {
|
||||
case "public":
|
||||
setOnlineState(ONLINE);
|
||||
break;
|
||||
case "idle":
|
||||
setOnlineState(AWAY);
|
||||
break;
|
||||
case "private":
|
||||
case "p2p":
|
||||
case "groupShow":
|
||||
setOnlineState(PRIVATE);
|
||||
break;
|
||||
case "off":
|
||||
setOnlineState(OFFLINE);
|
||||
break;
|
||||
default:
|
||||
LOG.debug("Unknown online state {} for model {}", status, getName());
|
||||
setOnlineState(OFFLINE);
|
||||
break;
|
||||
}
|
||||
return online;
|
||||
}
|
||||
|
||||
private JSONObject loadModelInfo() throws IOException {
|
||||
|
@ -84,7 +107,7 @@ public class StripchatModel extends AbstractModel {
|
|||
try (Response response = site.getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
JSONObject jsonResponse = new JSONObject(response.body().string());
|
||||
String streamName = jsonResponse.optString("streamName");
|
||||
String streamName = jsonResponse.optString("streamName", jsonResponse.optString(""));
|
||||
JSONObject viewServers = jsonResponse.getJSONObject("viewServers");
|
||||
String serverName = viewServers.optString("flashphoner-hls");
|
||||
JSONObject broadcastSettings = jsonResponse.getJSONObject("broadcastSettings");
|
||||
|
@ -92,18 +115,20 @@ public class StripchatModel extends AbstractModel {
|
|||
StreamSource best = new StreamSource();
|
||||
best.height = broadcastSettings.optInt("height");
|
||||
best.width = broadcastSettings.optInt("width");
|
||||
best.mediaPlaylistUrl = "https://b-" + serverName + ".stripst.com/hls/" + streamName + "/" + streamName + ".m3u8";
|
||||
best.mediaPlaylistUrl = "https://b-" + serverName + ".stripst.com/hls/" + streamName + '/' + streamName + ".m3u8";
|
||||
sources.add(best);
|
||||
JSONObject resolutions = broadcastSettings.optJSONObject("resolutions");
|
||||
if (resolutions instanceof JSONObject) {
|
||||
JSONArray heights = resolutions.names();
|
||||
JSONObject presets = broadcastSettings.optJSONObject("presets");
|
||||
Object defaultObject = presets.get("testing");
|
||||
if (defaultObject instanceof JSONObject) {
|
||||
JSONObject defaults = (JSONObject) defaultObject;
|
||||
JSONArray heights = defaults.names();
|
||||
for (int i = 0; i < heights.length(); i++) {
|
||||
String h = heights.getString(i);
|
||||
StreamSource streamSource = new StreamSource();
|
||||
streamSource.height = Integer.parseInt(h.replace("p", ""));
|
||||
streamSource.height = Integer.parseInt(h.replaceAll("[^\\d]", ""));
|
||||
streamSource.width = streamSource.height * best.getWidth() / best.getHeight();
|
||||
String source = streamName + "-" + streamSource.height + "p";
|
||||
streamSource.mediaPlaylistUrl = "https://b-" + serverName + ".stripst.com/hls/" + source + "/" + source + ".m3u8";
|
||||
String source = streamName + '_' + streamSource.height + 'p';
|
||||
streamSource.mediaPlaylistUrl = "https://b-" + serverName + ".stripst.com/hls/" + source + '/' + source + ".m3u8";
|
||||
sources.add(streamSource);
|
||||
}
|
||||
}
|
||||
|
@ -111,9 +136,13 @@ public class StripchatModel extends AbstractModel {
|
|||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
} catch(JSONException e) {
|
||||
System.err.println(getName());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void invalidateCacheEntries() {
|
||||
status = null;
|
||||
|
|
|
@ -54,6 +54,7 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
|
|||
|
||||
@Test
|
||||
public void testUtcTimeReplacement() {
|
||||
// without user defined pattern
|
||||
String date = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")
|
||||
.withLocale(Locale.US)
|
||||
.withZone(ZoneOffset.UTC)
|
||||
|
@ -61,12 +62,21 @@ public class AbstractPlaceholderAwarePostProcessorTest extends AbstractPpTest {
|
|||
String input = "asdf_${utcDateTime}_asdf";
|
||||
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
|
||||
// with user defined pattern
|
||||
date = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")
|
||||
.withLocale(Locale.US)
|
||||
.withZone(ZoneOffset.UTC)
|
||||
.format(rec.getStartDate());
|
||||
input = "asdf_${utcDateTime(yyyyMMdd-HHmmss)}_asdf";
|
||||
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
|
||||
// multiple occurences with user defined patterns
|
||||
date = DateTimeFormatter.ofPattern("yyyy-MM-dd/yyyy")
|
||||
.withLocale(Locale.US)
|
||||
.withZone(ZoneOffset.UTC)
|
||||
.format(rec.getStartDate());
|
||||
input = "asdf_${utcDateTime(yyyy)}-${utcDateTime(MM)}-${utcDateTime(dd)}/${utcDateTime(yyyy)}_asdf";
|
||||
assertEquals("asdf_" + date + "_asdf", placeHolderAwarePp.fillInPlaceHolders(input, rec, config));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
.idea
|
||||
master.iml
|
|
@ -1,17 +1,17 @@
|
|||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>3.10.3</version>
|
||||
<version>3.10.10</version>
|
||||
|
||||
<modules>
|
||||
<module>../common</module>
|
||||
<module>../client</module>
|
||||
<module>../server</module>
|
||||
<module>../client</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
|
@ -63,7 +63,7 @@
|
|||
<dependency>
|
||||
<groupId>com.squareup.moshi</groupId>
|
||||
<artifactId>moshi</artifactId>
|
||||
<version>1.5.0</version>
|
||||
<version>1.6.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
|
@ -73,7 +73,7 @@
|
|||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>1.7.25</version>
|
||||
<version>1.7.30</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
|
|
|
@ -7,3 +7,5 @@
|
|||
/jre/
|
||||
/server-local.sh
|
||||
ctbrec.pid
|
||||
/.idea/
|
||||
*.iml
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
<artifactId>server</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<version>3.10.3</version>
|
||||
<relativePath>../master</relativePath>
|
||||
</parent>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<version>3.10.10</version>
|
||||
<relativePath>../master</relativePath>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
|
|
|
@ -19,6 +19,7 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Settings;
|
||||
import ctbrec.Settings.SplitStrategy;
|
||||
|
||||
public class ConfigServlet extends AbstractCtbrecServlet {
|
||||
|
||||
|
@ -27,7 +28,7 @@ public class ConfigServlet extends AbstractCtbrecServlet {
|
|||
private Settings settings;
|
||||
|
||||
public enum DataType {
|
||||
STRING, BOOLEAN, INTEGER, LONG, DOUBLE
|
||||
STRING, BOOLEAN, INTEGER, LONG, DOUBLE, SPLIT_STRATEGY
|
||||
}
|
||||
|
||||
public ConfigServlet(Config config) {
|
||||
|
@ -62,7 +63,9 @@ public class ConfigServlet extends AbstractCtbrecServlet {
|
|||
addParameter("postProcessingThreads", "Post-Processing Threads", DataType.INTEGER, settings.postProcessingThreads, json);
|
||||
addParameter("recordingsDir", "Recordings Directory", DataType.STRING, settings.recordingsDir, json);
|
||||
addParameter("recordSingleFile", "Record Single File", DataType.BOOLEAN, settings.recordSingleFile, json);
|
||||
addParameter("splitRecordings", "Split Recordings (secs)", DataType.INTEGER, settings.splitRecordings, json);
|
||||
addParameter("splitStrategy", "Split Strategy", DataType.SPLIT_STRATEGY, settings.splitStrategy, json);
|
||||
addParameter("splitRecordingsAfterSecs", "Split Recordings After (secs)", DataType.INTEGER, settings.splitRecordingsAfterSecs, json);
|
||||
addParameter("splitRecordingsBiggerThanBytes", "Split Recordings Bigger Than (bytes)", DataType.LONG, settings.splitRecordingsBiggerThanBytes, json);
|
||||
addParameter("transportLayerSecurity", "Transport Layer Security (TLS)", DataType.BOOLEAN, settings.transportLayerSecurity, json);
|
||||
addParameter("webinterface", "Web-Interface", DataType.BOOLEAN, settings.webinterface, json);
|
||||
addParameter("webinterfaceUsername", "Web-Interface User", DataType.STRING, settings.webinterfaceUsername, json);
|
||||
|
@ -153,6 +156,8 @@ public class ConfigServlet extends AbstractCtbrecServlet {
|
|||
case DOUBLE:
|
||||
corrected = Double.parseDouble(value.toString());
|
||||
break;
|
||||
case SPLIT_STRATEGY:
|
||||
corrected = SplitStrategy.valueOf(value.toString());
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -50,7 +50,8 @@ public class HlsServlet extends AbstractCtbrecServlet {
|
|||
boolean idOnly = request.indexOf('/') < 0;
|
||||
if (idOnly) {
|
||||
requestFile = rec.get().getPostProcessedFile();
|
||||
requestedFilePath = requestFile.getCanonicalPath();
|
||||
serveSegment(req, resp, requestFile);
|
||||
return;
|
||||
} else {
|
||||
requestedFilePath = request.substring(request.indexOf('/'));
|
||||
requestFile = new File(requestedFilePath);
|
||||
|
@ -73,7 +74,6 @@ public class HlsServlet extends AbstractCtbrecServlet {
|
|||
}
|
||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||
writeResponse(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"Authentication failed\"}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,6 @@ public class HlsServlet extends AbstractCtbrecServlet {
|
|||
|
||||
private void serveSegment(HttpServletRequest req, HttpServletResponse resp, File requestedFile) throws IOException {
|
||||
MimetypesFileTypeMap map = new MimetypesFileTypeMap();
|
||||
//String mimetype = requestedFile.getName().endsWith(".mp4") ? "video/mp4" : "application/octet-stream";
|
||||
String mimetype = map.getContentType(requestedFile);
|
||||
LOG.debug("Serving {} as {}", requestedFile.getName(), mimetype);
|
||||
serveFile(req, resp, requestedFile, mimetype);
|
||||
|
|
|
@ -273,7 +273,7 @@
|
|||
});
|
||||
} else {
|
||||
$('#addModelByUrl').autocomplete({
|
||||
source: ["BongaCams:", "Cam4:", "Camsoda:", "Chaturbate:", "Fc2Live:", "Flirt4Free:", "LiveJasmin:", "MyFreeCams:", "Showup:", "Streamate:", "Stripchat:"]
|
||||
source: ["BongaCams:", "Cam4:", "Camsoda:", "Chaturbate:", "Fc2Live:", "Flirt4Free:", "LiveJasmin:", "MyFreeCams:", "MVLive:", "Showup:", "Streamate:", "Stripchat:"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue