Merge branch 'dev' into manyvids
# Conflicts: # common/src/main/java/ctbrec/io/HttpClient.java
This commit is contained in:
commit
db186e65f4
51
CHANGELOG.md
51
CHANGELOG.md
|
@ -1,3 +1,54 @@
|
||||||
|
3.8.6
|
||||||
|
========================
|
||||||
|
* Added setting to disable the online check for paused models
|
||||||
|
* Speed up shutdown process by stopping all recordings simultaneously
|
||||||
|
* Fixed Streamate followed tab once again
|
||||||
|
* Fixed: Flirt4Free models loose their name after some time
|
||||||
|
* Made loading of config file more robust for Flirt4Free models
|
||||||
|
* Added tab which shows the log output
|
||||||
|
|
||||||
|
3.8.5
|
||||||
|
========================
|
||||||
|
* Fixed Stripchat followed tab. It didn't work, if you have many favorited
|
||||||
|
models
|
||||||
|
* Fixed: Some Stripchat models didn't get recorded
|
||||||
|
* Fixed: Some LiveJasmin models didn't get recorded
|
||||||
|
* Added support for temporary recordings. On the recording tab you can now set
|
||||||
|
a date, when to stop recording a model and what to do afterwards
|
||||||
|
(pause or remove the model)
|
||||||
|
* Changed the look of the model table in the web interface a bit
|
||||||
|
|
||||||
|
3.8.4
|
||||||
|
========================
|
||||||
|
* Added support for xHamsterLive (go to Settings -> Sites -> Stripchat,
|
||||||
|
switch to xHamsterLive, enter your credentials and restart)
|
||||||
|
* Fixed follow / unfollow for Stripchat
|
||||||
|
* Enable rerun PP for multiple recordings
|
||||||
|
* Fixed bug, which prevented recordings to finish properly on app
|
||||||
|
shutdown. Recordings now shouldn't end up in state waiting anymore
|
||||||
|
|
||||||
|
3.8.3
|
||||||
|
========================
|
||||||
|
* Fixed Streamate
|
||||||
|
* Fixed favorites tab for Cam4; kind of, because only the online tab works.
|
||||||
|
I currently don't see a way to retrieve the offline favorites
|
||||||
|
* Fixed favorites tab for CamSoda
|
||||||
|
* Fixed CamSoda recordings
|
||||||
|
* Added external login dialog for Stripchat to support the captcha
|
||||||
|
|
||||||
|
3.8.2
|
||||||
|
========================
|
||||||
|
* Fixed misconfiguration in global connection pool, which caused a lot of
|
||||||
|
threads to spawn while browsing in the thumbnail overviews
|
||||||
|
* Improved memory handling for the thumbnail overviews. Thumbnail images were
|
||||||
|
not released, when a tab was switched. This caused a huge memory consumption,
|
||||||
|
if you opened a lot of different tabs.
|
||||||
|
* Fixed a bug in MFC websocket client, which caused to spawn a bunch of
|
||||||
|
"keep-alive" threads, if there was a problem with the connection
|
||||||
|
* Reworked the settings tab
|
||||||
|
* Fire recording finished event, if a download from the server is finished
|
||||||
|
* Ignore min/max resolution, if the resolution is unknown
|
||||||
|
|
||||||
3.8.1
|
3.8.1
|
||||||
========================
|
========================
|
||||||
* Fixed recent MFC error
|
* Fixed recent MFC error
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
export JAVA_HOME=/opt/jdk-11.0.1
|
|
||||||
mvn clean
|
mvn clean
|
||||||
mvn -Djavafx.platform=win package verify
|
mvn -Djavafx.platform=win package verify
|
||||||
mvn -Djavafx.platform=linux package verify
|
mvn -Djavafx.platform=linux package verify
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>3.8.1</version>
|
<version>3.8.6</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ch.qos.logback</groupId>
|
<groupId>ch.qos.logback</groupId>
|
||||||
<artifactId>logback-classic</artifactId>
|
<artifactId>logback-classic</artifactId>
|
||||||
<scope>runtime</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
|
@ -91,7 +91,7 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.vladsch.flexmark</groupId>
|
<groupId>com.vladsch.flexmark</groupId>
|
||||||
<artifactId>flexmark-all</artifactId>
|
<artifactId>flexmark</artifactId>
|
||||||
<version>0.40.34</version>
|
<version>0.40.34</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -140,8 +140,8 @@
|
||||||
<versionInfo>
|
<versionInfo>
|
||||||
<fileVersion>${project.version}.0</fileVersion>
|
<fileVersion>${project.version}.0</fileVersion>
|
||||||
<txtFileVersion>${project.version}</txtFileVersion>
|
<txtFileVersion>${project.version}</txtFileVersion>
|
||||||
<fileDescription>Recorder for Charturbate streams</fileDescription>
|
<fileDescription>Software to record live streams</fileDescription>
|
||||||
<copyright>2018 0xboobface</copyright>
|
<copyright>2020 0xboobface</copyright>
|
||||||
<productVersion>${project.version}.0</productVersion>
|
<productVersion>${project.version}.0</productVersion>
|
||||||
<txtProductVersion>${project.version}</txtProductVersion>
|
<txtProductVersion>${project.version}</txtProductVersion>
|
||||||
<productName>CTB Recorder</productName>
|
<productName>CTB Recorder</productName>
|
||||||
|
|
|
@ -62,6 +62,7 @@ import ctbrec.ui.tabs.RecordingsTab;
|
||||||
import ctbrec.ui.tabs.SiteTab;
|
import ctbrec.ui.tabs.SiteTab;
|
||||||
import ctbrec.ui.tabs.TabSelectionListener;
|
import ctbrec.ui.tabs.TabSelectionListener;
|
||||||
import ctbrec.ui.tabs.UpdateTab;
|
import ctbrec.ui.tabs.UpdateTab;
|
||||||
|
import ctbrec.ui.tabs.logging.LoggingTab;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.application.HostServices;
|
import javafx.application.HostServices;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
@ -143,8 +144,6 @@ public class CamrecApplication extends Application {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startOnlineMonitor() {
|
private void startOnlineMonitor() {
|
||||||
onlineMonitor = new OnlineMonitor(recorder);
|
|
||||||
onlineMonitor.start();
|
|
||||||
for (Site site : sites) {
|
for (Site site : sites) {
|
||||||
if(site.isEnabled()) {
|
if(site.isEnabled()) {
|
||||||
try {
|
try {
|
||||||
|
@ -155,6 +154,8 @@ public class CamrecApplication extends Application {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onlineMonitor = new OnlineMonitor(recorder, config);
|
||||||
|
onlineMonitor.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void logEnvironment() {
|
private void logEnvironment() {
|
||||||
|
@ -193,6 +194,7 @@ public class CamrecApplication extends Application {
|
||||||
tabPane.getTabs().add(new NewsTab());
|
tabPane.getTabs().add(new NewsTab());
|
||||||
tabPane.getTabs().add(new DonateTabFx());
|
tabPane.getTabs().add(new DonateTabFx());
|
||||||
tabPane.getTabs().add(new HelpTab());
|
tabPane.getTabs().add(new HelpTab());
|
||||||
|
tabPane.getTabs().add(new LoggingTab());
|
||||||
|
|
||||||
switchToStartTab();
|
switchToStartTab();
|
||||||
writeColorSchemeStyleSheet();
|
writeColorSchemeStyleSheet();
|
||||||
|
|
|
@ -13,6 +13,7 @@ import com.squareup.moshi.JsonReader;
|
||||||
import com.squareup.moshi.JsonWriter;
|
import com.squareup.moshi.JsonWriter;
|
||||||
|
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
|
import ctbrec.SubsequentAction;
|
||||||
import ctbrec.recorder.download.Download;
|
import ctbrec.recorder.download.Download;
|
||||||
import ctbrec.recorder.download.HttpHeaderFactory;
|
import ctbrec.recorder.download.HttpHeaderFactory;
|
||||||
import ctbrec.recorder.download.StreamSource;
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
@ -286,4 +287,24 @@ public class JavaFxModel implements Model {
|
||||||
public HttpHeaderFactory getHttpHeaderFactory() {
|
public HttpHeaderFactory getHttpHeaderFactory() {
|
||||||
return delegate.getHttpHeaderFactory();
|
return delegate.getHttpHeaderFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Instant getRecordUntil() {
|
||||||
|
return delegate.getRecordUntil();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRecordUntil(Instant instant) {
|
||||||
|
delegate.setRecordUntil(instant);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SubsequentAction getRecordUntilSubsequentAction() {
|
||||||
|
return delegate.getRecordUntilSubsequentAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRecordUntilSubsequentAction(SubsequentAction action) {
|
||||||
|
delegate.setRecordUntilSubsequentAction(action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import javafx.scene.control.Dialog;
|
||||||
import javafx.scene.control.TextArea;
|
import javafx.scene.control.TextArea;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
@ -87,6 +88,23 @@ public class Dialogs {
|
||||||
return dialog.showAndWait();
|
return dialog.showAndWait();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Boolean showCustomInput(Scene parent, String title, Region region) {
|
||||||
|
Dialog<?> dialog = new Dialog<>();
|
||||||
|
dialog.setTitle(title);
|
||||||
|
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
dialog.initModality(Modality.APPLICATION_MODAL);
|
||||||
|
dialog.setResizable(true);
|
||||||
|
InputStream icon = Dialogs.class.getResourceAsStream("/icon.png");
|
||||||
|
Stage stage = (Stage) dialog.getDialogPane().getScene().getWindow();
|
||||||
|
stage.getIcons().add(new Image(icon));
|
||||||
|
if (parent != null) {
|
||||||
|
stage.getScene().getStylesheets().addAll(parent.getStylesheets());
|
||||||
|
}
|
||||||
|
dialog.getDialogPane().setContent(region);
|
||||||
|
dialog.showAndWait();
|
||||||
|
return dialog.getResult() == ButtonType.OK;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean showConfirmDialog(String title, String message, String header, Scene parent) {
|
public static boolean showConfirmDialog(String title, String message, String header, Scene parent) {
|
||||||
AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, message, parent, YES, NO);
|
AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, message, parent, YES, NO);
|
||||||
confirm.setTitle(title);
|
confirm.setTitle(title);
|
||||||
|
|
|
@ -86,6 +86,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
private DiscreteRange<Integer> rangeValues = new DiscreteRange<>(values, labels);
|
private DiscreteRange<Integer> rangeValues = new DiscreteRange<>(values, labels);
|
||||||
private SimpleIntegerProperty concurrentRecordings;
|
private SimpleIntegerProperty concurrentRecordings;
|
||||||
private SimpleIntegerProperty onlineCheckIntervalInSecs;
|
private SimpleIntegerProperty onlineCheckIntervalInSecs;
|
||||||
|
private SimpleBooleanProperty onlineCheckSkipsPausedModels;
|
||||||
private SimpleLongProperty leaveSpaceOnDevice;
|
private SimpleLongProperty leaveSpaceOnDevice;
|
||||||
private SimpleIntegerProperty minimumLengthInSecs;
|
private SimpleIntegerProperty minimumLengthInSecs;
|
||||||
private SimpleStringProperty ffmpegParameters;
|
private SimpleStringProperty ffmpegParameters;
|
||||||
|
@ -149,6 +150,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
postProcessing = new SimpleFileProperty(null, "postProcessing", settings.postProcessing);
|
postProcessing = new SimpleFileProperty(null, "postProcessing", settings.postProcessing);
|
||||||
postProcessingThreads = new SimpleIntegerProperty(null, "postProcessingThreads", settings.postProcessingThreads);
|
postProcessingThreads = new SimpleIntegerProperty(null, "postProcessingThreads", settings.postProcessingThreads);
|
||||||
removeRecordingAfterPp = new SimpleBooleanProperty(null, "removeRecordingAfterPostProcessing", settings.removeRecordingAfterPostProcessing);
|
removeRecordingAfterPp = new SimpleBooleanProperty(null, "removeRecordingAfterPostProcessing", settings.removeRecordingAfterPostProcessing);
|
||||||
|
onlineCheckSkipsPausedModels = new SimpleBooleanProperty(null, "onlineCheckSkipsPausedModels", settings.onlineCheckSkipsPausedModels);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createGui() {
|
private void createGui() {
|
||||||
|
@ -187,10 +189,11 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
Setting.of("Split recordings after (minutes)", splitAfter).converter(SplitAfterOption.converter()),
|
Setting.of("Split recordings after (minutes)", splitAfter).converter(SplitAfterOption.converter()),
|
||||||
Setting.of("Restrict Resolution", resolutionRange, "Only record streams with resolution within the given range"),
|
Setting.of("Restrict Resolution", resolutionRange, "Only record streams with resolution within the given range"),
|
||||||
Setting.of("Concurrent Recordings (0 = unlimited)", concurrentRecordings),
|
Setting.of("Concurrent Recordings (0 = unlimited)", concurrentRecordings),
|
||||||
Setting.of("Check online state every (seconds)", onlineCheckIntervalInSecs, "Check every x seconds, if a model came online"),
|
|
||||||
Setting.of("Leave space on device (GiB)", leaveSpaceOnDevice, "Stop recording, if the free space on the device gets below this threshold").converter(new GigabytesConverter()),
|
Setting.of("Leave space on device (GiB)", leaveSpaceOnDevice, "Stop recording, if the free space on the device gets below this threshold").converter(new GigabytesConverter()),
|
||||||
Setting.of("FFmpeg parameters", ffmpegParameters, "FFmpeg parameters to use when merging stream segments"),
|
Setting.of("FFmpeg parameters", ffmpegParameters, "FFmpeg parameters to use when merging stream segments"),
|
||||||
Setting.of("File Extension", fileExtension, "File extension to use for recordings")
|
Setting.of("File Extension", fileExtension, "File extension to use for recordings"),
|
||||||
|
Setting.of("Check online state every (seconds)", onlineCheckIntervalInSecs, "Check every x seconds, if a model came online"),
|
||||||
|
Setting.of("Skip online check for paused models", onlineCheckSkipsPausedModels, "Skip online check for paused models")
|
||||||
),
|
),
|
||||||
Group.of("Location",
|
Group.of("Location",
|
||||||
Setting.of("Record Location", recordLocal),
|
Setting.of("Record Location", recordLocal),
|
||||||
|
@ -236,6 +239,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
prefs.getSetting("minimumResolution").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("recordingsDirStructure").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||||
prefs.getSetting("onlineCheckIntervalInSecs").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
prefs.getSetting("onlineCheckIntervalInSecs").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||||
|
prefs.getSetting("onlineCheckSkipsPausedModels").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||||
prefs.getSetting("minimumSpaceLeftInBytes").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
prefs.getSetting("minimumSpaceLeftInBytes").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||||
prefs.getSetting("postProcessing").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
prefs.getSetting("postProcessing").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||||
prefs.getSetting("postProcessingThreads").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
prefs.getSetting("postProcessingThreads").ifPresent(s -> bindEnabledProperty(s, recordLocal.not()));
|
||||||
|
|
|
@ -63,6 +63,7 @@ public class BongaCamsElectronLoginDialog {
|
||||||
}
|
}
|
||||||
String password = Config.getInstance().getSettings().bongaPassword;
|
String password = Config.getInstance().getSettings().bongaPassword;
|
||||||
if (password != null && !password.trim().isEmpty()) {
|
if (password != null && !password.trim().isEmpty()) {
|
||||||
|
password = password.replace("'", "\\'");
|
||||||
browser.executeJavaScript("$('input[name=\"log_in[password]\"]').attr('value','" + password + "')");
|
browser.executeJavaScript("$('input[name=\"log_in[password]\"]').attr('value','" + password + "')");
|
||||||
}
|
}
|
||||||
String[] simplify = new String[] {
|
String[] simplify = new String[] {
|
||||||
|
|
|
@ -62,6 +62,7 @@ public class Cam4ElectronLoginDialog {
|
||||||
}
|
}
|
||||||
String password = Config.getInstance().getSettings().cam4Password;
|
String password = Config.getInstance().getSettings().cam4Password;
|
||||||
if (password != null && !password.trim().isEmpty()) {
|
if (password != null && !password.trim().isEmpty()) {
|
||||||
|
password = password.replace("'", "\\'");
|
||||||
browser.executeJavaScript("document.querySelector('#loginPageForm input[name=\"password\"]').value = '" + password + "';");
|
browser.executeJavaScript("document.querySelector('#loginPageForm input[name=\"password\"]').value = '" + password + "';");
|
||||||
}
|
}
|
||||||
browser.executeJavaScript("document.getElementById('footer').setAttribute('style', 'display:none');");
|
browser.executeJavaScript("document.getElementById('footer').setAttribute('style', 'display:none');");
|
||||||
|
|
|
@ -3,19 +3,13 @@ package ctbrec.ui.sites.cam4;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.jsoup.nodes.Element;
|
import org.json.JSONArray;
|
||||||
import org.jsoup.select.Elements;
|
import org.json.JSONObject;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import ctbrec.Config;
|
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
import ctbrec.io.HtmlParser;
|
|
||||||
import ctbrec.io.HttpException;
|
import ctbrec.io.HttpException;
|
||||||
import ctbrec.sites.cam4.Cam4;
|
import ctbrec.sites.cam4.Cam4;
|
||||||
import ctbrec.sites.cam4.Cam4Model;
|
import ctbrec.sites.cam4.Cam4Model;
|
||||||
|
@ -27,7 +21,6 @@ import okhttp3.Response;
|
||||||
|
|
||||||
public class Cam4FollowedUpdateService extends PaginatedScheduledService {
|
public class Cam4FollowedUpdateService extends PaginatedScheduledService {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(Cam4FollowedUpdateService.class);
|
|
||||||
private Cam4 site;
|
private Cam4 site;
|
||||||
private boolean showOnline = true;
|
private boolean showOnline = true;
|
||||||
|
|
||||||
|
@ -50,46 +43,28 @@ public class Cam4FollowedUpdateService extends PaginatedScheduledService {
|
||||||
// login first
|
// login first
|
||||||
SiteUiFactory.getUi(site).login();
|
SiteUiFactory.getUi(site).login();
|
||||||
List<Model> models = new ArrayList<>();
|
List<Model> models = new ArrayList<>();
|
||||||
String username = Config.getInstance().getSettings().cam4Username;
|
String url = site.getBaseUrl() + "/directoryCams?directoryJson=true&online=" + showOnline + "&url=true&friends=true&favorites=true&resultsPerPage=90";
|
||||||
String url = site.getBaseUrl() + '/' + username + "/edit/friends_favorites";
|
|
||||||
Request req = new Request.Builder().url(url).build();
|
Request req = new Request.Builder().url(url).build();
|
||||||
try(Response response = site.getHttpClient().execute(req)) {
|
try (Response response = site.getHttpClient().execute(req)) {
|
||||||
if(response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
String content = response.body().string();
|
String content = response.body().string();
|
||||||
Elements cells = HtmlParser.getTags(content, "div#favorites div.ff_thumb");
|
JSONObject json = new JSONObject(content);
|
||||||
for (Element cell : cells) {
|
JSONArray users = json.getJSONArray("users");
|
||||||
String cellHtml = cell.html();
|
for (int i = 0; i < users.length(); i++) {
|
||||||
Element link = HtmlParser.getTag(cellHtml, "div.ff_img a");
|
JSONObject modelJson = users.getJSONObject(i);
|
||||||
String path = link.attr("href");
|
String username = modelJson.optString("username");
|
||||||
String modelName = path.substring(1);
|
Cam4Model model = site.createModel(username);
|
||||||
Cam4Model model = (Cam4Model) site.createModel(modelName);
|
model.setPreview(modelJson.optString("snapshotImageLink"));
|
||||||
model.setPreview("https://snapshots.xcdnpro.com/thumbnails/"+model.getName()+"?s=" + System.currentTimeMillis());
|
model.setOnlineStateByShowType(modelJson.optString("showType"));
|
||||||
model.setOnlineStateByShowType(parseOnlineState(cellHtml));
|
model.setDescription(modelJson.optString("statusMessage"));
|
||||||
models.add(model);
|
models.add(model);
|
||||||
}
|
}
|
||||||
return models.stream()
|
return models;
|
||||||
.filter(m -> {
|
|
||||||
try {
|
|
||||||
return m.isOnline() == showOnline;
|
|
||||||
} catch (IOException | ExecutionException e) {
|
|
||||||
LOG.error("Couldn't determine online state", e);
|
|
||||||
return false;
|
|
||||||
} catch(InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
LOG.error("Couldn't determine online state", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}).collect(Collectors.toList());
|
|
||||||
} else {
|
} else {
|
||||||
throw new HttpException(response.code(), response.message());
|
throw new HttpException(response.code(), response.message());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String parseOnlineState(String cellHtml) {
|
|
||||||
Element state = HtmlParser.getTag(cellHtml, "div.ff_name div");
|
|
||||||
return state.attr("class").equals("online") ? "NORMAL" : "OFFLINE";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package ctbrec.ui.sites.camsoda;
|
package ctbrec.ui.sites.camsoda;
|
||||||
|
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import ctbrec.sites.camsoda.Camsoda;
|
import ctbrec.sites.camsoda.Camsoda;
|
||||||
|
import ctbrec.sites.camsoda.CamsodaModel;
|
||||||
import ctbrec.ui.tabs.FollowedTab;
|
import ctbrec.ui.tabs.FollowedTab;
|
||||||
import ctbrec.ui.tabs.ThumbOverviewTab;
|
import ctbrec.ui.tabs.ThumbOverviewTab;
|
||||||
import javafx.concurrent.WorkerStateEvent;
|
import javafx.concurrent.WorkerStateEvent;
|
||||||
|
@ -18,9 +21,10 @@ public class CamsodaFollowedTab extends ThumbOverviewTab implements FollowedTab
|
||||||
boolean showOnline = true;
|
boolean showOnline = true;
|
||||||
|
|
||||||
public CamsodaFollowedTab(String title, Camsoda camsoda) {
|
public CamsodaFollowedTab(String title, Camsoda camsoda) {
|
||||||
super(title, new CamsodaFollowedUpdateService(camsoda), camsoda);
|
super(title, new CamsodaUpdateService(camsoda.getBaseUrl() + "/api/v1/browse/following", true, camsoda, m -> true), camsoda);
|
||||||
status = new Label("Logging in...");
|
status = new Label("Logging in...");
|
||||||
grid.getChildren().add(status);
|
grid.getChildren().add(status);
|
||||||
|
((CamsodaUpdateService)updateService).setFilter(createFilter(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -40,9 +44,9 @@ public class CamsodaFollowedTab extends ThumbOverviewTab implements FollowedTab
|
||||||
HBox.setMargin(online, new Insets(5, 5, 5, 40));
|
HBox.setMargin(online, new Insets(5, 5, 5, 40));
|
||||||
HBox.setMargin(offline, new Insets(5, 5, 5, 5));
|
HBox.setMargin(offline, new Insets(5, 5, 5, 5));
|
||||||
online.setSelected(true);
|
online.setSelected(true);
|
||||||
group.selectedToggleProperty().addListener((e) -> {
|
group.selectedToggleProperty().addListener(e -> {
|
||||||
|
showOnline = online.isSelected();
|
||||||
queue.clear();
|
queue.clear();
|
||||||
((CamsodaFollowedUpdateService)updateService).showOnline(online.isSelected());
|
|
||||||
updateService.restart();
|
updateService.restart();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -78,4 +82,18 @@ public class CamsodaFollowedTab extends ThumbOverviewTab implements FollowedTab
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Predicate<CamsodaModel> createFilter(CamsodaFollowedTab tab) {
|
||||||
|
return m -> {
|
||||||
|
try {
|
||||||
|
return m.isOnline() == tab.showOnline;
|
||||||
|
} catch(InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,79 +0,0 @@
|
||||||
package ctbrec.ui.sites.camsoda;
|
|
||||||
|
|
||||||
import static ctbrec.Model.State.*;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import ctbrec.Model;
|
|
||||||
import ctbrec.io.HttpException;
|
|
||||||
import ctbrec.sites.camsoda.Camsoda;
|
|
||||||
import ctbrec.sites.camsoda.CamsodaModel;
|
|
||||||
import ctbrec.ui.SiteUiFactory;
|
|
||||||
import ctbrec.ui.tabs.PaginatedScheduledService;
|
|
||||||
import javafx.concurrent.Task;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
public class CamsodaFollowedUpdateService extends PaginatedScheduledService {
|
|
||||||
private Camsoda camsoda;
|
|
||||||
private boolean showOnline = true;
|
|
||||||
|
|
||||||
public CamsodaFollowedUpdateService(Camsoda camsoda) {
|
|
||||||
this.camsoda = camsoda;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Task<List<Model>> createTask() {
|
|
||||||
return new Task<List<Model>>() {
|
|
||||||
@Override
|
|
||||||
public List<Model> call() throws IOException {
|
|
||||||
List<Model> models = new ArrayList<>();
|
|
||||||
String url = camsoda.getBaseUrl() + "/api/v1/user/current";
|
|
||||||
SiteUiFactory.getUi(camsoda).login();
|
|
||||||
Request request = new Request.Builder().url(url).build();
|
|
||||||
try(Response response = camsoda.getHttpClient().execute(request)) {
|
|
||||||
if (response.isSuccessful()) {
|
|
||||||
JSONObject json = new JSONObject(response.body().string());
|
|
||||||
if(json.has("status") && json.getBoolean("status")) {
|
|
||||||
JSONObject user = json.getJSONObject("user");
|
|
||||||
JSONArray following = user.getJSONArray("following");
|
|
||||||
for (int i = 0; i < following.length(); i++) {
|
|
||||||
JSONObject m = following.getJSONObject(i);
|
|
||||||
CamsodaModel model = (CamsodaModel) camsoda.createModel(m.getString("followname"));
|
|
||||||
boolean online = m.getInt("online") == 1;
|
|
||||||
model.setOnlineState(online ? ONLINE : OFFLINE);
|
|
||||||
model.setPreview("https://md.camsoda.com/thumbs/" + model.getName() + ".jpg");
|
|
||||||
models.add(model);
|
|
||||||
}
|
|
||||||
return models.stream()
|
|
||||||
.filter((m) -> {
|
|
||||||
try {
|
|
||||||
return m.isOnline() == showOnline;
|
|
||||||
} catch (IOException | ExecutionException | InterruptedException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}).collect(Collectors.toList());
|
|
||||||
} else {
|
|
||||||
response.close();
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new HttpException(response.code(), response.message());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void showOnline(boolean online) {
|
|
||||||
this.showOnline = online;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,7 @@
|
||||||
package ctbrec.ui.sites.camsoda;
|
package ctbrec.ui.sites.camsoda;
|
||||||
|
|
||||||
|
import static ctbrec.Model.State.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -60,53 +62,55 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
|
||||||
|
|
||||||
protected List<CamsodaModel> loadOnlineModels() throws IOException {
|
protected List<CamsodaModel> loadOnlineModels() throws IOException {
|
||||||
List<CamsodaModel> models = new ArrayList<>();
|
List<CamsodaModel> models = new ArrayList<>();
|
||||||
if(loginRequired && StringUtil.isBlank(ctbrec.Config.getInstance().getSettings().username)) {
|
if(loginRequired && StringUtil.isBlank(ctbrec.Config.getInstance().getSettings().camsodaUsername)) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
} else {
|
} else {
|
||||||
String url = CamsodaUpdateService.this.url;
|
|
||||||
LOG.debug("Fetching page {}", url);
|
LOG.debug("Fetching page {}", url);
|
||||||
if(loginRequired) {
|
if(loginRequired) {
|
||||||
SiteUiFactory.getUi(camsoda).login();
|
SiteUiFactory.getUi(camsoda).login();
|
||||||
}
|
}
|
||||||
Request request = new Request.Builder().url(url).build();
|
Request request = new Request.Builder().url(url).build();
|
||||||
try(Response response = camsoda.getHttpClient().execute(request)) {
|
try (Response response = camsoda.getHttpClient().execute(request)) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
JSONObject json = new JSONObject(response.body().string());
|
String body = response.body().string();
|
||||||
if(json.has("status") && json.getBoolean("status")) {
|
JSONObject json = new JSONObject(body);
|
||||||
|
if (json.optBoolean("status")) {
|
||||||
JSONArray template = json.getJSONArray("template");
|
JSONArray template = json.getJSONArray("template");
|
||||||
JSONArray results = json.getJSONArray("results");
|
JSONArray results = json.getJSONArray("results");
|
||||||
for (int i = 0; i < results.length(); i++) {
|
for (int i = 0; i < results.length(); i++) {
|
||||||
JSONObject result = results.getJSONObject(i);
|
JSONObject result = results.getJSONObject(i);
|
||||||
try {
|
try {
|
||||||
if(result.has("tpl")) {
|
CamsodaModel model;
|
||||||
|
if (result.has("tpl")) {
|
||||||
JSONArray tpl = result.getJSONArray("tpl");
|
JSONArray tpl = result.getJSONArray("tpl");
|
||||||
String name = tpl.getString(getTemplateIndex(template, "username"));
|
String name = tpl.getString(getTemplateIndex(template, "username"));
|
||||||
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
|
model = (CamsodaModel) camsoda.createModel(name);
|
||||||
model.setDescription(tpl.getString(getTemplateIndex(template, "subject_html")));
|
model.setDescription(tpl.getString(getTemplateIndex(template, "subject_html")));
|
||||||
model.setSortOrder(tpl.getFloat(getTemplateIndex(template, "sort_value")));
|
model.setSortOrder(tpl.getFloat(getTemplateIndex(template, "sort_value")));
|
||||||
String preview = "https:" + tpl.getString(getTemplateIndex(template, "thumb"));
|
String preview = "https:" + tpl.getString(getTemplateIndex(template, "thumb"));
|
||||||
model.setPreview(preview);
|
model.setPreview(preview);
|
||||||
String displayName = tpl.getString(getTemplateIndex(template, "display_name"));
|
String displayName = tpl.getString(getTemplateIndex(template, "display_name"));
|
||||||
model.setDisplayName(displayName.replaceAll("[^a-zA-Z0-9]", ""));
|
model.setDisplayName(displayName.replaceAll("[^a-zA-Z0-9]", ""));
|
||||||
if(model.getDisplayName().isBlank()) {
|
if (model.getDisplayName().isBlank()) {
|
||||||
model.setDisplayName(name);
|
model.setDisplayName(name);
|
||||||
}
|
}
|
||||||
model.setNew(result.optBoolean("new"));
|
model.setNew(result.optBoolean("new"));
|
||||||
|
model.setOnlineState(tpl.getString(getTemplateIndex(template, "stream_name")).isEmpty() ? OFFLINE : ONLINE);
|
||||||
models.add(model);
|
models.add(model);
|
||||||
} else {
|
} else {
|
||||||
String name = result.getString("username");
|
String name = result.getString("username");
|
||||||
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
|
model = (CamsodaModel) camsoda.createModel(name);
|
||||||
model.setSortOrder(result.getFloat("sort_value"));
|
model.setSortOrder(result.getFloat("sort_value"));
|
||||||
if(result.has("status")) {
|
if (result.has("status")) {
|
||||||
model.setOnlineStateByStatus(result.getString("status"));
|
model.setOnlineStateByStatus(result.getString("status"));
|
||||||
}
|
}
|
||||||
if(result.has("display_name")) {
|
if (result.has("display_name")) {
|
||||||
model.setDisplayName(result.getString("display_name").replaceAll("[^a-zA-Z0-9]", ""));
|
model.setDisplayName(result.getString("display_name").replaceAll("[^a-zA-Z0-9]", ""));
|
||||||
if(model.getDisplayName().isBlank()) {
|
if (model.getDisplayName().isBlank()) {
|
||||||
model.setDisplayName(name);
|
model.setDisplayName(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(result.has("thumb")) {
|
if (result.has("thumb")) {
|
||||||
String previewUrl = "https:" + result.getString("thumb");
|
String previewUrl = "https:" + result.getString("thumb");
|
||||||
model.setPreview(previewUrl);
|
model.setPreview(previewUrl);
|
||||||
}
|
}
|
||||||
|
@ -138,4 +142,8 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
|
||||||
}
|
}
|
||||||
throw new NoSuchElementException(string + " not found in template: " + template.toString());
|
throw new NoSuchElementException(string + " not found in template: " + template.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFilter(Predicate<CamsodaModel> filter) {
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ public class LiveJasminElectronLoginDialog {
|
||||||
}
|
}
|
||||||
String password = Config.getInstance().getSettings().livejasminPassword;
|
String password = Config.getInstance().getSettings().livejasminPassword;
|
||||||
if (password != null && !password.trim().isEmpty()) {
|
if (password != null && !password.trim().isEmpty()) {
|
||||||
|
password = password.replace("'", "\\'");
|
||||||
browser.executeJavaScript("document.querySelector('#login_form input[name=\"password\"]').value = '" + password + "';");
|
browser.executeJavaScript("document.querySelector('#login_form input[name=\"password\"]').value = '" + password + "';");
|
||||||
}
|
}
|
||||||
browser.executeJavaScript("document.getElementById('header_container').setAttribute('style', 'display:none');");
|
browser.executeJavaScript("document.getElementById('header_container').setAttribute('style', 'display:none');");
|
||||||
|
|
|
@ -526,6 +526,7 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener {
|
||||||
updateService.cancel();
|
updateService.cancel();
|
||||||
}
|
}
|
||||||
saveData();
|
saveData();
|
||||||
|
observableModels.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveData() {
|
private void saveData() {
|
||||||
|
|
|
@ -84,6 +84,7 @@ public class ShowupElectronLoginDialog {
|
||||||
}
|
}
|
||||||
String password = Config.getInstance().getSettings().showupPassword;
|
String password = Config.getInstance().getSettings().showupPassword;
|
||||||
if (password != null && !password.trim().isEmpty()) {
|
if (password != null && !password.trim().isEmpty()) {
|
||||||
|
password = password.replace("'", "\\'");
|
||||||
browser.executeJavaScript("$('input[name=\"password\"]').attr('value','" + password + "')");
|
browser.executeJavaScript("$('input[name=\"password\"]').attr('value','" + password + "')");
|
||||||
}
|
}
|
||||||
browser.executeJavaScript("$('input[name=\"remember\"]').attr('value','true')");
|
browser.executeJavaScript("$('input[name=\"remember\"]').attr('value','true')");
|
||||||
|
|
|
@ -41,7 +41,7 @@ public class StreamateFollowedService extends PaginatedScheduledService {
|
||||||
public StreamateFollowedService(Streamate streamate) {
|
public StreamateFollowedService(Streamate streamate) {
|
||||||
this.streamate = streamate;
|
this.streamate = streamate;
|
||||||
this.httpClient = (StreamateHttpClient) streamate.getHttpClient();
|
this.httpClient = (StreamateHttpClient) streamate.getHttpClient();
|
||||||
this.url = "https://member.naiadsystems.com/search/favorites?domain=streamate.com&skipXmentSelection=true";
|
this.url = "https://member.naiadsystems.com/search/v3/favorites?skipXmentSelection=true&skinConfig=%7B%7D&filters=";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -12,8 +12,11 @@ import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.CheckBox;
|
import javafx.scene.control.CheckBox;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.PasswordField;
|
import javafx.scene.control.PasswordField;
|
||||||
|
import javafx.scene.control.RadioButton;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
|
|
||||||
public class StripchatConfigUI extends AbstractConfigUI {
|
public class StripchatConfigUI extends AbstractConfigUI {
|
||||||
|
@ -44,6 +47,26 @@ public class StripchatConfigUI extends AbstractConfigUI {
|
||||||
GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
layout.add(enabled, 1, row++);
|
layout.add(enabled, 1, row++);
|
||||||
|
|
||||||
|
l = new Label("Site");
|
||||||
|
layout.add(l, 0, row);
|
||||||
|
ToggleGroup toggleGroup = new ToggleGroup();
|
||||||
|
RadioButton optionA = new RadioButton("Stripchat");
|
||||||
|
optionA.setSelected(!Config.getInstance().getSettings().stripchatUseXhamster);
|
||||||
|
optionA.setToggleGroup(toggleGroup);
|
||||||
|
RadioButton optionB = new RadioButton("xHamsterLive");
|
||||||
|
optionB.setSelected(!optionA.isSelected());
|
||||||
|
optionB.setToggleGroup(toggleGroup);
|
||||||
|
optionA.selectedProperty().addListener((obs, oldV, newV) -> {
|
||||||
|
Config.getInstance().getSettings().stripchatUseXhamster = !newV;
|
||||||
|
save();
|
||||||
|
});
|
||||||
|
HBox hbox = new HBox();
|
||||||
|
hbox.getChildren().addAll(optionA, optionB);
|
||||||
|
HBox.setMargin(optionA, new Insets(5));
|
||||||
|
HBox.setMargin(optionB, new Insets(5));
|
||||||
|
GridPane.setMargin(hbox, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
layout.add(hbox, 1, row++);
|
||||||
|
|
||||||
layout.add(new Label("Stripchat User"), 0, row);
|
layout.add(new Label("Stripchat User"), 0, row);
|
||||||
TextField username = new TextField(Config.getInstance().getSettings().stripchatUsername);
|
TextField username = new TextField(Config.getInstance().getSettings().stripchatUsername);
|
||||||
username.textProperty().addListener((ob, o, n) -> {
|
username.textProperty().addListener((ob, o, n) -> {
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
package ctbrec.ui.sites.stripchat;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.sites.stripchat.Stripchat;
|
||||||
|
import ctbrec.ui.ExternalBrowser;
|
||||||
|
import okhttp3.Cookie;
|
||||||
|
import okhttp3.Cookie.Builder;
|
||||||
|
import okhttp3.CookieJar;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
|
||||||
|
public class StripchatElectronLoginDialog {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(StripchatElectronLoginDialog.class);
|
||||||
|
public String DOMAIN = Stripchat.domain;
|
||||||
|
public String URL = Stripchat.baseUri;
|
||||||
|
private CookieJar cookieJar;
|
||||||
|
private ExternalBrowser browser;
|
||||||
|
|
||||||
|
public StripchatElectronLoginDialog(CookieJar cookieJar) throws IOException {
|
||||||
|
this.cookieJar = cookieJar;
|
||||||
|
browser = ExternalBrowser.getInstance();
|
||||||
|
try {
|
||||||
|
JSONObject config = new JSONObject();
|
||||||
|
config.put("url", URL);
|
||||||
|
config.put("w", 640);
|
||||||
|
config.put("h", 640);
|
||||||
|
JSONObject msg = new JSONObject();
|
||||||
|
msg.put("config", config);
|
||||||
|
browser.run(msg, msgHandler);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new IOException("Couldn't wait for login dialog", e);
|
||||||
|
} finally {
|
||||||
|
browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Consumer<String> msgHandler = (line) -> {
|
||||||
|
if(!line.startsWith("{")) {
|
||||||
|
System.err.println(line);
|
||||||
|
} else {
|
||||||
|
JSONObject json = new JSONObject(line);
|
||||||
|
if(json.has("url")) {
|
||||||
|
String url = json.getString("url");
|
||||||
|
|
||||||
|
if(url.endsWith(DOMAIN) || url.endsWith(DOMAIN + '/')) {
|
||||||
|
try {
|
||||||
|
browser.executeJavaScript("document.querySelector('button[class~=\"btn-visitors-agreement-accept\"]').click();");
|
||||||
|
browser.executeJavaScript("document.querySelector('div[class~=\"header-dropdown\"] a[class~=\"dropdown-link\"]').click();");
|
||||||
|
browser.executeJavaScript("document.querySelector('a[class~=\"btn\"][href*=\"login\"]').click();");
|
||||||
|
String username = Config.getInstance().getSettings().stripchatUsername;
|
||||||
|
if (username != null && !username.trim().isEmpty()) {
|
||||||
|
browser.executeJavaScript("document.querySelector('#login_login_or_email').value = '" + username + "';");
|
||||||
|
}
|
||||||
|
String password = Config.getInstance().getSettings().stripchatPassword;
|
||||||
|
if (password != null && !password.trim().isEmpty()) {
|
||||||
|
password = password.replace("'", "\\'");
|
||||||
|
browser.executeJavaScript("document.querySelector('#login_password').value = '" + password + "';");
|
||||||
|
}
|
||||||
|
browser.executeJavaScript("document.querySelector('#recaptcha-checkbox-border').click();");
|
||||||
|
browser.executeJavaScript("document.querySelector('*[class~=btn-login]').addEventListener('click', function() {window.setTimeout(function() {location.reload()}, 2000)});");
|
||||||
|
} catch(Exception e) {
|
||||||
|
LOG.warn("Couldn't auto fill username and password for Stripchat", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.has("cookies")) {
|
||||||
|
JSONArray _cookies = json.getJSONArray("cookies");
|
||||||
|
boolean sessionCookieFound = false;
|
||||||
|
for (int i = 0; i < _cookies.length(); i++) {
|
||||||
|
JSONObject cookie = _cookies.getJSONObject(i);
|
||||||
|
if (cookie.getString("domain").contains(DOMAIN)) {
|
||||||
|
String domain = cookie.getString("domain");
|
||||||
|
if (domain.startsWith(".")) {
|
||||||
|
domain = domain.substring(1);
|
||||||
|
}
|
||||||
|
Cookie c = createCookie(domain, cookie);
|
||||||
|
cookieJar.saveFromResponse(HttpUrl.parse(url), Collections.singletonList(c));
|
||||||
|
c = createCookie(DOMAIN, cookie);
|
||||||
|
cookieJar.saveFromResponse(HttpUrl.parse(Stripchat.baseUri), Collections.singletonList(c));
|
||||||
|
if (c.name().contains("_com_sessionId")) {
|
||||||
|
sessionCookieFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(sessionCookieFound) {
|
||||||
|
try {
|
||||||
|
browser.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Couldn't send close request to browser", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private Cookie createCookie(String domain, JSONObject cookie) {
|
||||||
|
Builder b = new Cookie.Builder()
|
||||||
|
.path(cookie.getString("path"))
|
||||||
|
.domain(domain)
|
||||||
|
.name(cookie.getString("name"))
|
||||||
|
.value(cookie.getString("value"))
|
||||||
|
.expiresAt(Double.valueOf(cookie.optDouble("expirationDate")).longValue());
|
||||||
|
if(cookie.optBoolean("hostOnly")) {
|
||||||
|
b.hostOnlyDomain(domain);
|
||||||
|
}
|
||||||
|
if(cookie.optBoolean("httpOnly")) {
|
||||||
|
b.httpOnly();
|
||||||
|
}
|
||||||
|
if(cookie.optBoolean("secure")) {
|
||||||
|
b.secure();
|
||||||
|
}
|
||||||
|
return b.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,14 +4,10 @@ import ctbrec.sites.stripchat.Stripchat;
|
||||||
import ctbrec.ui.tabs.FollowedTab;
|
import ctbrec.ui.tabs.FollowedTab;
|
||||||
import ctbrec.ui.tabs.ThumbOverviewTab;
|
import ctbrec.ui.tabs.ThumbOverviewTab;
|
||||||
import javafx.concurrent.WorkerStateEvent;
|
import javafx.concurrent.WorkerStateEvent;
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.RadioButton;
|
|
||||||
import javafx.scene.control.ToggleGroup;
|
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.input.KeyEvent;
|
import javafx.scene.input.KeyEvent;
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
|
|
||||||
public class StripchatFollowedTab extends ThumbOverviewTab implements FollowedTab {
|
public class StripchatFollowedTab extends ThumbOverviewTab implements FollowedTab {
|
||||||
private Label status;
|
private Label status;
|
||||||
|
@ -26,25 +22,6 @@ public class StripchatFollowedTab extends ThumbOverviewTab implements FollowedTa
|
||||||
@Override
|
@Override
|
||||||
protected void createGui() {
|
protected void createGui() {
|
||||||
super.createGui();
|
super.createGui();
|
||||||
addOnlineOfflineSelector();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addOnlineOfflineSelector() {
|
|
||||||
ToggleGroup group = new ToggleGroup();
|
|
||||||
RadioButton online = new RadioButton("online");
|
|
||||||
online.setToggleGroup(group);
|
|
||||||
RadioButton offline = new RadioButton("offline");
|
|
||||||
offline.setToggleGroup(group);
|
|
||||||
pagination.getChildren().add(online);
|
|
||||||
pagination.getChildren().add(offline);
|
|
||||||
HBox.setMargin(online, new Insets(5, 5, 5, 40));
|
|
||||||
HBox.setMargin(offline, new Insets(5, 5, 5, 5));
|
|
||||||
online.setSelected(true);
|
|
||||||
group.selectedToggleProperty().addListener(e -> {
|
|
||||||
queue.clear();
|
|
||||||
((StripchatFollowedUpdateService)updateService).showOnline(online.isSelected());
|
|
||||||
updateService.restart();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -4,8 +4,8 @@ import static ctbrec.io.HttpConstants.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
@ -24,8 +24,8 @@ import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
|
||||||
public class StripchatFollowedUpdateService extends PaginatedScheduledService {
|
public class StripchatFollowedUpdateService extends PaginatedScheduledService {
|
||||||
|
private static final int PAGE_SIZE = 30;
|
||||||
private Stripchat stripchat;
|
private Stripchat stripchat;
|
||||||
private boolean showOnline = true;
|
|
||||||
|
|
||||||
public StripchatFollowedUpdateService(Stripchat stripchat) {
|
public StripchatFollowedUpdateService(Stripchat stripchat) {
|
||||||
this.stripchat = stripchat;
|
this.stripchat = stripchat;
|
||||||
|
@ -36,22 +36,33 @@ public class StripchatFollowedUpdateService extends PaginatedScheduledService {
|
||||||
return new Task<List<Model>>() {
|
return new Task<List<Model>>() {
|
||||||
@Override
|
@Override
|
||||||
public List<Model> call() throws IOException {
|
public List<Model> call() throws IOException {
|
||||||
|
int startIndex = (getPage() - 1) * PAGE_SIZE;
|
||||||
JSONArray favoriteModelIds = loadFavoriteModelIds();
|
JSONArray favoriteModelIds = loadFavoriteModelIds();
|
||||||
List<Model> models = loadModels(favoriteModelIds);
|
List<Integer> modelIdsToLoad = new ArrayList<>(PAGE_SIZE);
|
||||||
|
List<Model> models;
|
||||||
|
if (startIndex < favoriteModelIds.length()) {
|
||||||
|
int modelsOnPage = Math.min(PAGE_SIZE, favoriteModelIds.length() - startIndex - 1);
|
||||||
|
for (int i = 0; i < modelsOnPage; i++) {
|
||||||
|
modelIdsToLoad.add(favoriteModelIds.getInt(startIndex + i));
|
||||||
|
}
|
||||||
|
models = loadModels(modelIdsToLoad);
|
||||||
|
} else {
|
||||||
|
models = Collections.emptyList();
|
||||||
|
}
|
||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Model> loadModels(JSONArray favoriteModelIds) throws IOException {
|
private List<Model> loadModels(List<Integer> modelIdsToLoad) throws IOException {
|
||||||
List<Model> models = new ArrayList<>();
|
List<Model> models = new ArrayList<>();
|
||||||
HttpUrl.Builder urlBuilder = HttpUrl.parse(stripchat.getBaseUrl() + "/api/front/models/list").newBuilder();
|
HttpUrl.Builder urlBuilder = HttpUrl.parse(stripchat.getBaseUrl() + "/api/front/models/list").newBuilder();
|
||||||
for (int i = 0; i < favoriteModelIds.length(); i++) {
|
for (int i = 0; i < modelIdsToLoad.size(); i++) {
|
||||||
urlBuilder.addQueryParameter("modelIds["+i+"]", Integer.toString(favoriteModelIds.getInt(i)));
|
urlBuilder.addQueryParameter("modelIds["+i+"]", modelIdsToLoad.get(i).toString());
|
||||||
}
|
}
|
||||||
Request request = new Request.Builder()
|
Request request = new Request.Builder()
|
||||||
.url(urlBuilder.build())
|
.url(urlBuilder.build())
|
||||||
.header(ACCEPT, "*/*")
|
.header(ACCEPT, "*/*")
|
||||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
.header(REFERER, Stripchat.BASE_URI + "/favorites")
|
.header(REFERER, Stripchat.baseUri + "/favorites")
|
||||||
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
||||||
.build();
|
.build();
|
||||||
try (Response response = stripchat.getHttpClient().execute(request)) {
|
try (Response response = stripchat.getHttpClient().execute(request)) {
|
||||||
|
@ -64,12 +75,9 @@ public class StripchatFollowedUpdateService extends PaginatedScheduledService {
|
||||||
StripchatModel model = stripchat.createModel(user.optString("username"));
|
StripchatModel model = stripchat.createModel(user.optString("username"));
|
||||||
model.setDescription(user.optString("description"));
|
model.setDescription(user.optString("description"));
|
||||||
model.setPreview(user.optString("previewUrlThumbBig"));
|
model.setPreview(user.optString("previewUrlThumbBig"));
|
||||||
boolean online = Objects.equals(user.optString("status"), "public");
|
|
||||||
if (showOnline == online) {
|
|
||||||
models.add(model);
|
models.add(model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new HttpException(response.code(), response.message());
|
throw new HttpException(response.code(), response.message());
|
||||||
}
|
}
|
||||||
|
@ -79,21 +87,22 @@ public class StripchatFollowedUpdateService extends PaginatedScheduledService {
|
||||||
|
|
||||||
private JSONArray loadFavoriteModelIds() throws IOException {
|
private JSONArray loadFavoriteModelIds() throws IOException {
|
||||||
SiteUiFactory.getUi(stripchat).login();
|
SiteUiFactory.getUi(stripchat).login();
|
||||||
|
stripchat.getHttpClient().login();
|
||||||
long userId = ((StripchatHttpClient) stripchat.getHttpClient()).getUserId();
|
long userId = ((StripchatHttpClient) stripchat.getHttpClient()).getUserId();
|
||||||
String url = stripchat.getBaseUrl() + "/api/front/users/" + userId + "/favorites";
|
String url = stripchat.getBaseUrl() + "/api/front/users/" + userId + "/favorites";
|
||||||
Request request = new Request.Builder()
|
Request request = new Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
.header(ORIGIN, Stripchat.BASE_URI)
|
.header(ORIGIN, Stripchat.baseUri)
|
||||||
.header(REFERER, Stripchat.BASE_URI + "/favorites")
|
.header(REFERER, Stripchat.baseUri + "/favorites")
|
||||||
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
||||||
.build();
|
.build();
|
||||||
try (Response response = stripchat.getHttpClient().execute(request)) {
|
try (Response response = stripchat.getHttpClient().execute(request)) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
JSONObject json = new JSONObject(response.body().string());
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
if(json.has("userIds")) {
|
if (json.has("modelIds")) {
|
||||||
JSONArray userIds = json.getJSONArray("userIds");
|
JSONArray userIds = json.getJSONArray("modelIds");
|
||||||
return userIds;
|
return userIds;
|
||||||
} else {
|
} else {
|
||||||
return new JSONArray();
|
return new JSONArray();
|
||||||
|
@ -105,8 +114,4 @@ public class StripchatFollowedUpdateService extends PaginatedScheduledService {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void showOnline(boolean online) {
|
|
||||||
this.showOnline = online;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,30 @@
|
||||||
package ctbrec.ui.sites.stripchat;
|
package ctbrec.ui.sites.stripchat;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import ctbrec.sites.stripchat.Stripchat;
|
import ctbrec.sites.stripchat.Stripchat;
|
||||||
|
import ctbrec.sites.stripchat.StripchatHttpClient;
|
||||||
|
import ctbrec.ui.controls.Dialogs;
|
||||||
import ctbrec.ui.sites.AbstractSiteUi;
|
import ctbrec.ui.sites.AbstractSiteUi;
|
||||||
import ctbrec.ui.sites.ConfigUI;
|
import ctbrec.ui.sites.ConfigUI;
|
||||||
import ctbrec.ui.tabs.TabProvider;
|
import ctbrec.ui.tabs.TabProvider;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
|
||||||
public class StripchatSiteUi extends AbstractSiteUi {
|
public class StripchatSiteUi extends AbstractSiteUi {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(StripchatSiteUi.class);
|
||||||
|
|
||||||
private StripchatTabProvider tabProvider;
|
private StripchatTabProvider tabProvider;
|
||||||
private StripchatConfigUI configUi;
|
private StripchatConfigUI configUi;
|
||||||
private Stripchat stripchat;
|
private Stripchat site;
|
||||||
|
|
||||||
public StripchatSiteUi(Stripchat stripchat) {
|
public StripchatSiteUi(Stripchat stripchat) {
|
||||||
this.stripchat = stripchat;
|
this.site = stripchat;
|
||||||
tabProvider = new StripchatTabProvider(stripchat);
|
tabProvider = new StripchatTabProvider(stripchat);
|
||||||
configUi = new StripchatConfigUI(stripchat);
|
configUi = new StripchatConfigUI(stripchat);
|
||||||
}
|
}
|
||||||
|
@ -31,7 +41,40 @@ public class StripchatSiteUi extends AbstractSiteUi {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized boolean login() throws IOException {
|
public synchronized boolean login() throws IOException {
|
||||||
boolean automaticLogin = stripchat.login();
|
boolean automaticLogin = site.login();
|
||||||
return automaticLogin;
|
if (automaticLogin) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
|
Runnable showDialog = () -> {
|
||||||
|
// login with external browser
|
||||||
|
try {
|
||||||
|
new StripchatElectronLoginDialog(site.getHttpClient().getCookieJar());
|
||||||
|
} catch (Exception e1) {
|
||||||
|
LOG.error("Error logging in with external browser", e1);
|
||||||
|
Dialogs.showError("Login error", "Couldn't login to " + site.getName(), e1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
queue.put(true);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOG.error("Error while signaling termination", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Platform.runLater(showDialog);
|
||||||
|
try {
|
||||||
|
queue.take();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOG.error("Error while waiting for login dialog to close", e);
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
StripchatHttpClient httpClient = (StripchatHttpClient) site.getHttpClient();
|
||||||
|
boolean loggedIn = httpClient.checkLoginSuccess();
|
||||||
|
return loggedIn;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
package ctbrec.ui.tabs;
|
package ctbrec.ui.tabs;
|
||||||
|
|
||||||
|
import static ctbrec.SubsequentAction.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
@ -25,6 +29,7 @@ import ctbrec.Config;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
import ctbrec.Recording;
|
import ctbrec.Recording;
|
||||||
import ctbrec.StringUtil;
|
import ctbrec.StringUtil;
|
||||||
|
import ctbrec.SubsequentAction;
|
||||||
import ctbrec.recorder.Recorder;
|
import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import ctbrec.ui.AutosizeAlert;
|
import ctbrec.ui.AutosizeAlert;
|
||||||
|
@ -59,8 +64,10 @@ import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ContextMenu;
|
import javafx.scene.control.ContextMenu;
|
||||||
|
import javafx.scene.control.DatePicker;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.MenuItem;
|
import javafx.scene.control.MenuItem;
|
||||||
|
import javafx.scene.control.RadioButton;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.control.SelectionMode;
|
import javafx.scene.control.SelectionMode;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
|
@ -71,6 +78,7 @@ import javafx.scene.control.TableColumn.SortType;
|
||||||
import javafx.scene.control.TableRow;
|
import javafx.scene.control.TableRow;
|
||||||
import javafx.scene.control.TableView;
|
import javafx.scene.control.TableView;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.control.cell.CheckBoxTableCell;
|
import javafx.scene.control.cell.CheckBoxTableCell;
|
||||||
import javafx.scene.control.cell.PropertyValueFactory;
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
|
@ -84,6 +92,7 @@ import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.FlowPane;
|
import javafx.scene.layout.FlowPane;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.util.Callback;
|
import javafx.util.Callback;
|
||||||
|
@ -612,6 +621,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
pauseRecording.setOnAction(e -> pauseRecording(selectedModels));
|
pauseRecording.setOnAction(e -> pauseRecording(selectedModels));
|
||||||
MenuItem resumeRecording = new MenuItem("Resume Recording");
|
MenuItem resumeRecording = new MenuItem("Resume Recording");
|
||||||
resumeRecording.setOnAction(e -> resumeRecording(selectedModels));
|
resumeRecording.setOnAction(e -> resumeRecording(selectedModels));
|
||||||
|
MenuItem stopRecordingAt = new MenuItem("Stop Recording at Date");
|
||||||
|
stopRecordingAt.setOnAction(e -> setStopDate(selectedModels.get(0)));
|
||||||
MenuItem openInBrowser = new MenuItem("Open in Browser");
|
MenuItem openInBrowser = new MenuItem("Open in Browser");
|
||||||
openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl()));
|
openInBrowser.setOnAction(e -> DesktopIntegration.open(selectedModels.get(0).getUrl()));
|
||||||
MenuItem openInPlayer = new MenuItem("Open in Player");
|
MenuItem openInPlayer = new MenuItem("Open in Player");
|
||||||
|
@ -630,6 +641,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
ContextMenu menu = new ContextMenu(stop);
|
ContextMenu menu = new ContextMenu(stop);
|
||||||
if (selectedModels.size() == 1) {
|
if (selectedModels.size() == 1) {
|
||||||
menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording);
|
menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording);
|
||||||
|
menu.getItems().add(stopRecordingAt);
|
||||||
} else {
|
} else {
|
||||||
menu.getItems().addAll(resumeRecording, pauseRecording);
|
menu.getItems().addAll(resumeRecording, pauseRecording);
|
||||||
}
|
}
|
||||||
|
@ -646,6 +658,46 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setStopDate(JavaFxModel model) {
|
||||||
|
DatePicker datePicker = new DatePicker();
|
||||||
|
GridPane grid = new GridPane();
|
||||||
|
grid.setHgap(10);
|
||||||
|
grid.setVgap(10);
|
||||||
|
grid.setPadding(new Insets(20, 150, 10, 10));
|
||||||
|
grid.add(new Label("Stop at"), 0, 0);
|
||||||
|
grid.add(datePicker, 1, 0);
|
||||||
|
grid.add(new Label("And then"), 0, 1);
|
||||||
|
ToggleGroup toggleGroup = new ToggleGroup();
|
||||||
|
RadioButton pauseButton = new RadioButton("pause recording");
|
||||||
|
pauseButton.setSelected(model.getRecordUntilSubsequentAction() == PAUSE);
|
||||||
|
pauseButton.setToggleGroup(toggleGroup);
|
||||||
|
RadioButton removeButton = new RadioButton("remove model");
|
||||||
|
removeButton.setSelected(model.getRecordUntilSubsequentAction() == REMOVE);
|
||||||
|
removeButton.setToggleGroup(toggleGroup);
|
||||||
|
HBox row = new HBox();
|
||||||
|
row.getChildren().addAll(pauseButton, removeButton);
|
||||||
|
HBox.setMargin(pauseButton, new Insets(5));
|
||||||
|
HBox.setMargin(removeButton, new Insets(5));
|
||||||
|
grid.add(row, 1, 1);
|
||||||
|
if (model.getRecordUntil().toEpochMilli() != Model.RECORD_INDEFINITELY) {
|
||||||
|
LocalDate localDate = LocalDate.ofInstant(model.getRecordUntil(), ZoneId.systemDefault());
|
||||||
|
datePicker.setValue(localDate);
|
||||||
|
}
|
||||||
|
boolean userClickedOk = Dialogs.showCustomInput(getTabPane().getScene(), "Stop Recording at", grid);
|
||||||
|
if (userClickedOk) {
|
||||||
|
SubsequentAction action = pauseButton.isSelected() ? PAUSE : REMOVE;
|
||||||
|
LOG.info("Stop at {} and {}", datePicker.getValue(), action);
|
||||||
|
Instant stopAt = Instant.from(datePicker.getValue().atStartOfDay().atZone(ZoneId.systemDefault()));
|
||||||
|
model.setRecordUntil(stopAt);
|
||||||
|
model.setRecordUntilSubsequentAction(action);
|
||||||
|
try {
|
||||||
|
recorder.stopRecordingAt(model.getDelegate());
|
||||||
|
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
|
||||||
|
Dialogs.showError(getTabPane().getScene(), "Error", "Couln't set stop date", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ignore(ObservableList<JavaFxModel> selectedModels) {
|
private void ignore(ObservableList<JavaFxModel> selectedModels) {
|
||||||
for (JavaFxModel fxModel : selectedModels) {
|
for (JavaFxModel fxModel : selectedModels) {
|
||||||
Model modelToIgnore = fxModel.getDelegate();
|
Model modelToIgnore = fxModel.getDelegate();
|
||||||
|
|
|
@ -31,6 +31,8 @@ import ctbrec.Config;
|
||||||
import ctbrec.Recording;
|
import ctbrec.Recording;
|
||||||
import ctbrec.Recording.State;
|
import ctbrec.Recording.State;
|
||||||
import ctbrec.StringUtil;
|
import ctbrec.StringUtil;
|
||||||
|
import ctbrec.event.EventBusHolder;
|
||||||
|
import ctbrec.event.RecordingStateChangedEvent;
|
||||||
import ctbrec.recorder.ProgressListener;
|
import ctbrec.recorder.ProgressListener;
|
||||||
import ctbrec.recorder.Recorder;
|
import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.recorder.RecordingPinnedException;
|
import ctbrec.recorder.RecordingPinnedException;
|
||||||
|
@ -437,15 +439,13 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem rerunPostProcessing = new MenuItem("Rerun Post-Processing");
|
MenuItem rerunPostProcessing = new MenuItem("Rerun Post-Processing");
|
||||||
rerunPostProcessing.setOnAction(e -> triggerPostProcessing(first));
|
rerunPostProcessing.setOnAction(e -> triggerPostProcessing(recordings));
|
||||||
if (first.getStatus() == FAILED || first.getStatus() == WAITING || first.getStatus() == FINISHED) {
|
|
||||||
contextMenu.getItems().add(rerunPostProcessing);
|
contextMenu.getItems().add(rerunPostProcessing);
|
||||||
}
|
rerunPostProcessing.setDisable(!recordings.stream().allMatch(Recording::canBePostProcessed));
|
||||||
|
|
||||||
if(recordings.size() > 1) {
|
if(recordings.size() > 1) {
|
||||||
openInPlayer.setDisable(true);
|
openInPlayer.setDisable(true);
|
||||||
openDir.setDisable(true);
|
openDir.setDisable(true);
|
||||||
rerunPostProcessing.setDisable(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return contextMenu;
|
return contextMenu;
|
||||||
|
@ -565,14 +565,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
new Thread(() -> DesktopIntegration.open(tsFile.getParent())).start();
|
new Thread(() -> DesktopIntegration.open(tsFile.getParent())).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void triggerPostProcessing(JavaFxRecording first) {
|
private void triggerPostProcessing(List<JavaFxRecording> recs) {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
|
for (JavaFxRecording rec : recs) {
|
||||||
try {
|
try {
|
||||||
recorder.rerunPostProcessing(first.getDelegate());
|
recorder.rerunPostProcessing(rec.getDelegate());
|
||||||
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
|
||||||
showErrorDialog("Error while starting post-processing", "The post-processing could not be started", e1);
|
showErrorDialog("Error while starting post-processing", "The post-processing could not be started", e1);
|
||||||
LOG.error("Error while starting post-processing", e1);
|
LOG.error("Error while starting post-processing", e1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -633,6 +635,8 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
recording.setStatus(FINISHED);
|
recording.setStatus(FINISHED);
|
||||||
recording.setProgress(-1);
|
recording.setProgress(-1);
|
||||||
|
RecordingStateChangedEvent evt = new RecordingStateChangedEvent(target, recording.getStatus(), recording.getModel(), recording.getStartDate());
|
||||||
|
EventBusHolder.BUS.post(evt);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -644,4 +644,8 @@ public class ThumbCell extends StackPane {
|
||||||
return new int[2];
|
return new int[2];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void releaseResources() {
|
||||||
|
iv.setImage(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -872,6 +872,15 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||||
updateService.cancel();
|
updateService.cancel();
|
||||||
}
|
}
|
||||||
queue.clear();
|
queue.clear();
|
||||||
|
|
||||||
|
for (Iterator<Node> iterator = grid.getChildren().iterator(); iterator.hasNext();) {
|
||||||
|
Node node = iterator.next();
|
||||||
|
if(node instanceof ThumbCell) {
|
||||||
|
ThumbCell thumbCell = (ThumbCell) node;
|
||||||
|
thumbCell.releaseResources();
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void suspendUpdates(boolean suspend) {
|
void suspendUpdates(boolean suspend) {
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package ctbrec.ui.tabs.logging;
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.spi.LoggingEvent;
|
||||||
|
import ch.qos.logback.core.ConsoleAppender;
|
||||||
|
import ctbrec.event.EventBusHolder;
|
||||||
|
|
||||||
|
public class CtbrecAppender extends ConsoleAppender<LoggingEvent> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void append(LoggingEvent event) {
|
||||||
|
EventBusHolder.BUS.post(event);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,194 @@
|
||||||
|
package ctbrec.ui.tabs.logging;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.LoggerContext;
|
||||||
|
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
|
||||||
|
import ch.qos.logback.classic.spi.IThrowableProxy;
|
||||||
|
import ch.qos.logback.classic.spi.LoggingEvent;
|
||||||
|
import ch.qos.logback.classic.spi.StackTraceElementProxy;
|
||||||
|
import ctbrec.event.EventBusHolder;
|
||||||
|
import ctbrec.ui.controls.SearchBox;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.ContextMenu;
|
||||||
|
import javafx.scene.control.MenuItem;
|
||||||
|
import javafx.scene.control.SelectionMode;
|
||||||
|
import javafx.scene.control.Tab;
|
||||||
|
import javafx.scene.control.TableColumn;
|
||||||
|
import javafx.scene.control.TableView;
|
||||||
|
import javafx.scene.input.Clipboard;
|
||||||
|
import javafx.scene.input.ClipboardContent;
|
||||||
|
import javafx.scene.input.ContextMenuEvent;
|
||||||
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
|
||||||
|
public class LoggingTab extends Tab {
|
||||||
|
|
||||||
|
private static final int HISTORY_LENGTH = 10_000;
|
||||||
|
private SearchBox filter = new SearchBox();
|
||||||
|
private TableView<LoggingEvent> table = new TableView<>();
|
||||||
|
private ObservableList<LoggingEvent> history = FXCollections.observableList(Collections.synchronizedList(new LinkedList<>()));
|
||||||
|
private ObservableList<LoggingEvent> filteredEvents = FXCollections.observableArrayList();
|
||||||
|
private DateTimeFormatter timeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||||
|
private volatile boolean tabClosed = false;
|
||||||
|
private ContextMenu popup;
|
||||||
|
private Object eventBustSubscriber = new Object() {
|
||||||
|
@Subscribe
|
||||||
|
public void publishLoggingEevent(LoggingEvent event) {
|
||||||
|
if (!tabClosed) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
history.add(event);
|
||||||
|
if (history.size() > HISTORY_LENGTH - 1) {
|
||||||
|
history.remove(0);
|
||||||
|
}
|
||||||
|
filter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void filter() {
|
||||||
|
filteredEvents.clear();
|
||||||
|
filteredEvents.addAll(history.stream().filter(evt -> {
|
||||||
|
String q = filter.getText().toLowerCase();
|
||||||
|
return evt.getLevel().toString().toLowerCase().contains(q)
|
||||||
|
|| createLogMessage(evt).toLowerCase().contains(q);
|
||||||
|
}).collect(Collectors.toList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoggingTab() {
|
||||||
|
setText("Logging");
|
||||||
|
subscribeToEventBus();
|
||||||
|
|
||||||
|
table = new TableView<>(filteredEvents);
|
||||||
|
|
||||||
|
int idx = 0;
|
||||||
|
TableColumn<LoggingEvent, String> level = createTableColumn("Level", 65, idx++);
|
||||||
|
level.setCellValueFactory(cdf -> new SimpleStringProperty(cdf.getValue().getLevel().toString()));
|
||||||
|
table.getColumns().add(level);
|
||||||
|
|
||||||
|
TableColumn<LoggingEvent, String> time = createTableColumn("Timestamp", 200, idx++);
|
||||||
|
time.setCellValueFactory(cdf -> {
|
||||||
|
Instant instant = Instant.ofEpochMilli(cdf.getValue().getTimeStamp());
|
||||||
|
return new SimpleStringProperty(instant.atZone(ZoneId.systemDefault()).format(timeFormatter));
|
||||||
|
});
|
||||||
|
table.getColumns().add(time);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
table.getColumns().add(location);
|
||||||
|
|
||||||
|
TableColumn<LoggingEvent, String> msg = createTableColumn("Message", 2000, idx++);
|
||||||
|
msg.setCellValueFactory(cdf -> new SimpleStringProperty(createLogMessage(cdf.getValue())));
|
||||||
|
table.getColumns().add(msg);
|
||||||
|
|
||||||
|
BorderPane layout = new BorderPane();
|
||||||
|
BorderPane.setMargin(table, new Insets(10));
|
||||||
|
BorderPane.setMargin(filter, new Insets(10, 10, 0, 10));
|
||||||
|
layout.setCenter(table);
|
||||||
|
layout.setTop(filter);
|
||||||
|
|
||||||
|
setContent(layout);
|
||||||
|
setOnClosed(evt -> {
|
||||||
|
EventBusHolder.BUS.unregister(eventBustSubscriber);
|
||||||
|
tabClosed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
filter.setPromptText("Search");
|
||||||
|
filter.textProperty().addListener( (observableValue, oldValue, newValue) -> filter());
|
||||||
|
|
||||||
|
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||||
|
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
|
||||||
|
popup = createContextMenu();
|
||||||
|
if (popup != null) {
|
||||||
|
popup.show(table, event.getScreenX(), event.getScreenY());
|
||||||
|
}
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
|
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
|
||||||
|
if (popup != null) {
|
||||||
|
popup.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContextMenu createContextMenu() {
|
||||||
|
final ObservableList<LoggingEvent> selectedEvents = table.getSelectionModel().getSelectedItems();
|
||||||
|
if (selectedEvents.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
MenuItem copy = new MenuItem("Copy");
|
||||||
|
copy.setOnAction(e -> {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
String formattedMessages = getFormattedMessages(selectedEvents);
|
||||||
|
final Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||||
|
final ClipboardContent content = new ClipboardContent();
|
||||||
|
content.putString(formattedMessages);
|
||||||
|
clipboard.setContent(content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ContextMenu menu = new ContextMenu(copy);
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFormattedMessages(ObservableList<LoggingEvent> selectedEvents) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||||
|
LoggerContext loggerContext = rootLogger.getLoggerContext();
|
||||||
|
loggerContext.reset();
|
||||||
|
|
||||||
|
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
|
||||||
|
encoder.setContext(loggerContext);
|
||||||
|
encoder.setPattern("%date %level [%thread] %logger{10} [%file:%line] %msg%n");
|
||||||
|
encoder.start();
|
||||||
|
|
||||||
|
for (LoggingEvent evt : selectedEvents) {
|
||||||
|
byte[] encode = encoder.encode(evt);
|
||||||
|
sb.append(new String(encode));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createLogMessage(LoggingEvent evt) {
|
||||||
|
StringBuilder sb = new StringBuilder(evt.getFormattedMessage());
|
||||||
|
if(evt.getThrowableProxy() != null) {
|
||||||
|
IThrowableProxy throwableProxy = evt.getThrowableProxy();
|
||||||
|
sb.append('\n').append(throwableProxy.getClassName()).append(':').append(' ').append(throwableProxy.getMessage());
|
||||||
|
for (StackTraceElementProxy step : throwableProxy.getStackTraceElementProxyArray()) {
|
||||||
|
sb.append('\n').append('\t').append(step.getSTEAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> TableColumn<LoggingEvent, T> createTableColumn(String text, int width, int idx) {
|
||||||
|
TableColumn<LoggingEvent, T> tc = new TableColumn<>(text);
|
||||||
|
tc.setPrefWidth(width);
|
||||||
|
tc.setUserData(idx);
|
||||||
|
return tc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void subscribeToEventBus() {
|
||||||
|
EventBusHolder.BUS.register(eventBustSubscriber);
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,8 @@ the port ctbrec tries to connect to, if it is run in remote mode.
|
||||||
|
|
||||||
- **livePreviews** (app only) - Enables the live preview feature in the app.
|
- **livePreviews** (app only) - Enables the live preview feature in the app.
|
||||||
|
|
||||||
|
- **minimumResolution** - [1 - 2147483647]. Sets the minimum video height for a recording. ctbrec tries to find a stream quality, which is higher than or equal to this value. If the only provided stream quality is below this threshold, ctbrec won't record the stream.
|
||||||
|
|
||||||
- **maximumResolution** - [1 - 2147483647]. Sets the maximum video height for a recording. ctbrec tries to find a stream quality, which is lower than or equal to this value. If the only provided stream quality is above this threshold, ctbrec won't record the stream.
|
- **maximumResolution** - [1 - 2147483647]. Sets the maximum video height for a recording. ctbrec tries to find a stream quality, which is lower than or equal to this value. If the only provided stream quality is above this threshold, ctbrec won't record the stream.
|
||||||
|
|
||||||
- **minimumLengthInSeconds** - [0 - 2147483647] Automatically delete recordings, which are shorter than this amount of seconds. 0 disables this feature.
|
- **minimumLengthInSeconds** - [0 - 2147483647] Automatically delete recordings, which are shorter than this amount of seconds. 0 disables this feature.
|
||||||
|
@ -49,6 +51,8 @@ the port ctbrec tries to connect to, if it is run in remote mode.
|
||||||
|
|
||||||
- **onlineCheckIntervalInSecs** - [1 - 2147483647] How often ctbrec checks, if a model is online. This is not a guaranteed interval: If you record many models, the online check for all models can take longer than this interval. A minute is a reasonable value, but you can go lower, if you don't want to miss a anything. But don't go too low, or you risk to do too many requests in a short amount of time and get banned by some sites.
|
- **onlineCheckIntervalInSecs** - [1 - 2147483647] How often ctbrec checks, if a model is online. This is not a guaranteed interval: If you record many models, the online check for all models can take longer than this interval. A minute is a reasonable value, but you can go lower, if you don't want to miss a anything. But don't go too low, or you risk to do too many requests in a short amount of time and get banned by some sites.
|
||||||
|
|
||||||
|
- **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** - Absolute path to a script, which is executed once a recording is finished. See [Post-Processing](/docs/PostProcessing.md).
|
||||||
|
|
||||||
- **recordingsDir** - Where ctbrec saves the recordings.
|
- **recordingsDir** - Where ctbrec saves the recordings.
|
||||||
|
|
|
@ -9,6 +9,12 @@
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<appender name="GUI" class="ctbrec.ui.tabs.logging.CtbrecAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
<appender name="FILE"
|
<appender name="FILE"
|
||||||
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
<file>ctbrec.log</file>
|
<file>ctbrec.log</file>
|
||||||
|
@ -32,6 +38,7 @@
|
||||||
<root level="DEBUG">
|
<root level="DEBUG">
|
||||||
<appender-ref ref="STDOUT" />
|
<appender-ref ref="STDOUT" />
|
||||||
<appender-ref ref="FILE" />
|
<appender-ref ref="FILE" />
|
||||||
|
<appender-ref ref="GUI" />
|
||||||
</root>
|
</root>
|
||||||
|
|
||||||
<logger name="ctbrec.LoggingInterceptor" level="info"/>
|
<logger name="ctbrec.LoggingInterceptor" level="info"/>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>3.8.1</version>
|
<version>3.8.6</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,8 @@ public abstract class AbstractModel implements Model {
|
||||||
protected State onlineState = State.UNKNOWN;
|
protected State onlineState = State.UNKNOWN;
|
||||||
private Instant lastSeen;
|
private Instant lastSeen;
|
||||||
private Instant lastRecorded;
|
private Instant lastRecorded;
|
||||||
|
private Instant recordUntil;
|
||||||
|
private SubsequentAction recordUntilSubsequentAction;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
||||||
|
@ -231,6 +233,26 @@ public abstract class AbstractModel implements Model {
|
||||||
this.lastRecorded = lastRecorded;
|
this.lastRecorded = lastRecorded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Instant getRecordUntil() {
|
||||||
|
return Optional.ofNullable(recordUntil).orElse(Instant.ofEpochMilli(RECORD_INDEFINITELY));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRecordUntil(Instant recordUntil) {
|
||||||
|
this.recordUntil = recordUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SubsequentAction getRecordUntilSubsequentAction() {
|
||||||
|
return Optional.ofNullable(recordUntilSubsequentAction).orElse(SubsequentAction.PAUSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRecordUntilSubsequentAction(SubsequentAction recordUntilSubsequentAction) {
|
||||||
|
this.recordUntilSubsequentAction = recordUntilSubsequentAction;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Download createDownload() {
|
public Download createDownload() {
|
||||||
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
|
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
|
||||||
|
|
|
@ -20,6 +20,8 @@ import ctbrec.sites.Site;
|
||||||
|
|
||||||
public interface Model extends Comparable<Model>, Serializable {
|
public interface Model extends Comparable<Model>, Serializable {
|
||||||
|
|
||||||
|
public static final long RECORD_INDEFINITELY = 9000000000000000000l;
|
||||||
|
|
||||||
public enum State {
|
public enum State {
|
||||||
ONLINE("online"),
|
ONLINE("online"),
|
||||||
OFFLINE("offline"),
|
OFFLINE("offline"),
|
||||||
|
@ -128,4 +130,10 @@ public interface Model extends Comparable<Model>, Serializable {
|
||||||
|
|
||||||
public HttpHeaderFactory getHttpHeaderFactory();
|
public HttpHeaderFactory getHttpHeaderFactory();
|
||||||
|
|
||||||
|
public Instant getRecordUntil();
|
||||||
|
public void setRecordUntil(Instant instant);
|
||||||
|
|
||||||
|
public SubsequentAction getRecordUntilSubsequentAction();
|
||||||
|
public void setRecordUntilSubsequentAction(SubsequentAction action);
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
package ctbrec;
|
package ctbrec;
|
||||||
|
|
||||||
|
import static ctbrec.Recording.State.*;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
@ -272,4 +274,8 @@ public class Recording implements Serializable {
|
||||||
public void refresh() {
|
public void refresh() {
|
||||||
sizeInByte = getSize();
|
sizeInByte = getSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean canBePostProcessed() {
|
||||||
|
return getStatus() == FAILED || getStatus() == WAITING || getStatus() == FINISHED;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,7 @@ public class Settings {
|
||||||
public List<Model> models = new ArrayList<>();
|
public List<Model> models = new ArrayList<>();
|
||||||
public List<Model> modelsIgnored = new ArrayList<>();
|
public List<Model> modelsIgnored = new ArrayList<>();
|
||||||
public int onlineCheckIntervalInSecs = 60;
|
public int onlineCheckIntervalInSecs = 60;
|
||||||
|
public boolean onlineCheckSkipsPausedModels = false;
|
||||||
public int overviewUpdateIntervalInSecs = 10;
|
public int overviewUpdateIntervalInSecs = 10;
|
||||||
public String password = ""; // chaturbate password TODO maybe rename this onetime
|
public String password = ""; // chaturbate password TODO maybe rename this onetime
|
||||||
public String postProcessing = "";
|
public String postProcessing = "";
|
||||||
|
@ -122,6 +123,7 @@ public class Settings {
|
||||||
public String streamateUsername = "";
|
public String streamateUsername = "";
|
||||||
public String stripchatUsername = "";
|
public String stripchatUsername = "";
|
||||||
public String stripchatPassword = "";
|
public String stripchatPassword = "";
|
||||||
|
public boolean stripchatUseXhamster = false;
|
||||||
public boolean transportLayerSecurity = true;
|
public boolean transportLayerSecurity = true;
|
||||||
public int thumbWidth = 180;
|
public int thumbWidth = 180;
|
||||||
public boolean updateThumbnails = true;
|
public boolean updateThumbnails = true;
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
package ctbrec;
|
||||||
|
|
||||||
|
public enum SubsequentAction {
|
||||||
|
PAUSE,
|
||||||
|
REMOVE
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ public class EventBusHolder {
|
||||||
|
|
||||||
private EventBusHolder() {}
|
private EventBusHolder() {}
|
||||||
|
|
||||||
public static final EventBus BUS = new AsyncEventBus(Executors.newFixedThreadPool(10, r -> {
|
public static final EventBus BUS = new AsyncEventBus(Executors.newFixedThreadPool(2, r -> {
|
||||||
Thread t = new Thread(r);
|
Thread t = new Thread(r);
|
||||||
t.setName("EventBus-" + UUID.randomUUID().toString().substring(0, 8));
|
t.setName("EventBus-" + UUID.randomUUID().toString().substring(0, 8));
|
||||||
t.setPriority(Thread.NORM_PRIORITY - 1);
|
t.setPriority(Thread.NORM_PRIORITY - 1);
|
||||||
|
|
|
@ -16,6 +16,7 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -28,7 +29,6 @@ import javax.net.ssl.X509TrustManager;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.google.common.base.Objects;
|
|
||||||
import com.squareup.moshi.JsonAdapter;
|
import com.squareup.moshi.JsonAdapter;
|
||||||
import com.squareup.moshi.Moshi;
|
import com.squareup.moshi.Moshi;
|
||||||
|
|
||||||
|
@ -127,9 +127,8 @@ public abstract class HttpClient {
|
||||||
.cookieJar(cookieJar)
|
.cookieJar(cookieJar)
|
||||||
.connectionPool(GLOBAL_HTTP_CONN_POOL)
|
.connectionPool(GLOBAL_HTTP_CONN_POOL)
|
||||||
.connectTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS)
|
.connectTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS)
|
||||||
.readTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS)
|
.readTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS);
|
||||||
//.addInterceptor(new LoggingInterceptor())
|
//.addInterceptor(new LoggingInterceptor());
|
||||||
.connectionPool(new ConnectionPool(50, 10, TimeUnit.MINUTES));
|
|
||||||
|
|
||||||
ProxyType proxyType = Config.getInstance().getSettings().proxyType;
|
ProxyType proxyType = Config.getInstance().getSettings().proxyType;
|
||||||
if (proxyType == ProxyType.HTTP) {
|
if (proxyType == ProxyType.HTTP) {
|
||||||
|
@ -278,7 +277,7 @@ public abstract class HttpClient {
|
||||||
for (List<Cookie> cookieList : cookies.values()) {
|
for (List<Cookie> cookieList : cookies.values()) {
|
||||||
for (Cookie cookie : cookieList) {
|
for (Cookie cookie : cookieList) {
|
||||||
for (String cookieName : names) {
|
for (String cookieName : names) {
|
||||||
if (Objects.equal(cookieName, cookie.name())) {
|
if (Objects.equals(cookieName, cookie.name())) {
|
||||||
result.add(cookie);
|
result.add(cookie);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@ public class HttpConstants {
|
||||||
|
|
||||||
public static final String ACCEPT = "Accept";
|
public static final String ACCEPT = "Accept";
|
||||||
public static final String ACCEPT_LANGUAGE = "Accept-Language";
|
public static final String ACCEPT_LANGUAGE = "Accept-Language";
|
||||||
public static final String COOKIE = "Cookie";
|
|
||||||
public static final String CONNECTION = "Connection";
|
public static final String CONNECTION = "Connection";
|
||||||
public static final String CONTENT_TYPE = "Content-Type";
|
public static final String CONTENT_TYPE = "Content-Type";
|
||||||
|
public static final String COOKIE = "Cookie";
|
||||||
public static final String KEEP_ALIVE = "keep-alive";
|
public static final String KEEP_ALIVE = "keep-alive";
|
||||||
public static final String MIMETYPE_APPLICATION_JSON = "application/json";
|
public static final String MIMETYPE_APPLICATION_JSON = "application/json";
|
||||||
public static final String ORIGIN = "Origin";
|
public static final String ORIGIN = "Origin";
|
||||||
|
|
|
@ -4,15 +4,24 @@ import java.io.IOException;
|
||||||
|
|
||||||
public class HttpException extends IOException {
|
public class HttpException extends IOException {
|
||||||
|
|
||||||
private int code;
|
private final String url;
|
||||||
private String msg;
|
private final int code;
|
||||||
|
private final String msg;
|
||||||
|
|
||||||
public HttpException(int code, String msg) {
|
public HttpException(int code, String msg) {
|
||||||
super(code + " - " + msg);
|
super(code + " - " + msg);
|
||||||
|
this.url = "";
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.msg = msg;
|
this.msg = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HttpException(String url, int code, String msg) {
|
||||||
|
super(code + " - " + msg + " - " + url);
|
||||||
|
this.code = code;
|
||||||
|
this.msg = msg;
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
public int getResponseCode() {
|
public int getResponseCode() {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
@ -20,4 +29,8 @@ public class HttpException extends IOException {
|
||||||
public String getResponseMessage() {
|
public String getResponseMessage() {
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import com.squareup.moshi.JsonReader.Token;
|
||||||
import com.squareup.moshi.JsonWriter;
|
import com.squareup.moshi.JsonWriter;
|
||||||
|
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
|
import ctbrec.SubsequentAction;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import ctbrec.sites.chaturbate.ChaturbateModel;
|
import ctbrec.sites.chaturbate.ChaturbateModel;
|
||||||
|
|
||||||
|
@ -74,6 +75,10 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
||||||
model.setLastSeen(Instant.ofEpochMilli(reader.nextLong()));
|
model.setLastSeen(Instant.ofEpochMilli(reader.nextLong()));
|
||||||
} else if(key.equals("lastRecorded")) {
|
} else if(key.equals("lastRecorded")) {
|
||||||
model.setLastRecorded(Instant.ofEpochMilli(reader.nextLong()));
|
model.setLastRecorded(Instant.ofEpochMilli(reader.nextLong()));
|
||||||
|
} else if(key.equals("recordUntil")) {
|
||||||
|
model.setRecordUntil(Instant.ofEpochMilli(reader.nextLong()));
|
||||||
|
} else if(key.equals("recordUntilSubsequentAction")) {
|
||||||
|
model.setRecordUntilSubsequentAction(SubsequentAction.valueOf(reader.nextString()));
|
||||||
} else if(key.equals("siteSpecific")) {
|
} else if(key.equals("siteSpecific")) {
|
||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
try {
|
try {
|
||||||
|
@ -115,6 +120,8 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
||||||
writer.name("suspended").value(model.isSuspended());
|
writer.name("suspended").value(model.isSuspended());
|
||||||
writer.name("lastSeen").value(model.getLastSeen().toEpochMilli());
|
writer.name("lastSeen").value(model.getLastSeen().toEpochMilli());
|
||||||
writer.name("lastRecorded").value(model.getLastRecorded().toEpochMilli());
|
writer.name("lastRecorded").value(model.getLastRecorded().toEpochMilli());
|
||||||
|
writer.name("recordUntil").value(model.getRecordUntil().toEpochMilli());
|
||||||
|
writer.name("recordUntilSubsequentAction").value(model.getRecordUntilSubsequentAction().name());
|
||||||
writer.name("siteSpecific");
|
writer.name("siteSpecific");
|
||||||
writer.beginObject();
|
writer.beginObject();
|
||||||
model.writeSiteSpecificData(writer);
|
model.writeSiteSpecificData(writer);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package ctbrec.recorder;
|
package ctbrec.recorder;
|
||||||
|
|
||||||
|
import static ctbrec.SubsequentAction.*;
|
||||||
import static ctbrec.event.Event.Type.*;
|
import static ctbrec.event.Event.Type.*;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -10,6 +11,7 @@ import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -20,8 +22,10 @@ import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorCompletionService;
|
import java.util.concurrent.ExecutorCompletionService;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
@ -59,7 +63,7 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
private volatile boolean recording = true;
|
private volatile boolean recording = true;
|
||||||
private ReentrantLock recorderLock = new ReentrantLock();
|
private ReentrantLock recorderLock = new ReentrantLock();
|
||||||
private RecorderHttpClient client = new RecorderHttpClient();
|
private RecorderHttpClient client = new RecorderHttpClient();
|
||||||
private long lastSpaceMessage = 0;
|
private long lastPreconditionMessage = 0;
|
||||||
private Map<Model, Recording> recordingProcesses = Collections.synchronizedMap(new HashMap<>());
|
private Map<Model, Recording> recordingProcesses = Collections.synchronizedMap(new HashMap<>());
|
||||||
private RecordingManager recordingManager;
|
private RecordingManager recordingManager;
|
||||||
|
|
||||||
|
@ -213,38 +217,106 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
private void startRecordingProcess(Model model) throws IOException {
|
private void startRecordingProcess(Model model) throws IOException {
|
||||||
recorderLock.lock();
|
recorderLock.lock();
|
||||||
try {
|
try {
|
||||||
if (!recording) {
|
checkRecordingPreconditions(model);
|
||||||
// recorder is not in recording mode
|
LOG.info("Starting recording for model {}", model.getName());
|
||||||
|
Download download = createDownload(model);
|
||||||
|
Recording rec = createRecording(download);
|
||||||
|
completionService.submit(createDownloadJob(rec));
|
||||||
|
} catch (RecordUntilExpiredException e) {
|
||||||
|
LOG.info("Precondition not met to record {}: {}", model, e.getLocalizedMessage());
|
||||||
|
executeRecordUntilSubsequentAction(model);
|
||||||
|
} catch (PreconditionNotMetException e) {
|
||||||
|
LOG.info("Precondition not met to record {}: {}", model, e.getLocalizedMessage());
|
||||||
return;
|
return;
|
||||||
|
} finally {
|
||||||
|
recorderLock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (model.isSuspended()) {
|
private Download createDownload(Model model) {
|
||||||
LOG.info("Recording for model {} is suspended.", model);
|
Download download = model.createDownload();
|
||||||
return;
|
download.init(config, model, Instant.now());
|
||||||
|
Objects.requireNonNull(download.getStartTime(),
|
||||||
|
"At this point the download should have set a startTime. Make sure to set a startTime in " + download.getClass() + ".init()");
|
||||||
|
LOG.debug("Downloading with {}", download.getClass().getSimpleName());
|
||||||
|
return download;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recordingProcesses.containsKey(model)) {
|
private Callable<Recording> createDownloadJob(Recording rec) {
|
||||||
LOG.error("A recording for model {} is already running", model);
|
return () -> {
|
||||||
return;
|
try {
|
||||||
|
setRecordingStatus(rec, State.RECORDING);
|
||||||
|
rec.getModel().setLastRecorded(rec.getStartDate());
|
||||||
|
recordingManager.saveRecording(rec);
|
||||||
|
rec.getDownload().start();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Download for {} failed. Download state: {}", rec.getModel().getName(), rec.getStatus(), e);
|
||||||
|
}
|
||||||
|
boolean deleted = deleteIfEmpty(rec);
|
||||||
|
setRecordingStatus(rec, deleted ? State.DELETED : State.WAITING);
|
||||||
|
if (!deleted) {
|
||||||
|
// only save the status, if the recording has not been deleted, otherwise we recreate the metadata file
|
||||||
|
recordingManager.saveRecording(rec);
|
||||||
|
}
|
||||||
|
return rec;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!models.contains(model)) {
|
private void executeRecordUntilSubsequentAction(Model model) throws IOException {
|
||||||
LOG.info("Model {} has been removed. Restarting of recording cancelled.", model);
|
if (model.getRecordUntilSubsequentAction() == PAUSE) {
|
||||||
return;
|
model.setSuspended(true);
|
||||||
|
} else if (model.getRecordUntilSubsequentAction() == REMOVE) {
|
||||||
|
try {
|
||||||
|
LOG.info("Removing {} because the recording timeframe ended at {}", model, model.getRecordUntil().atZone(ZoneId.systemDefault()));
|
||||||
|
stopRecording(model);
|
||||||
|
} catch (InvalidKeyException | NoSuchAlgorithmException e1) {
|
||||||
|
LOG.error("Error while stopping recording", e1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// reset values, so that model can be recorded again
|
||||||
|
model.setRecordUntil(null);
|
||||||
|
model.setRecordUntilSubsequentAction(PAUSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Recording createRecording(Download download) throws IOException {
|
||||||
|
Model model = download.getModel();
|
||||||
|
Recording rec = new Recording();
|
||||||
|
rec.setDownload(download);
|
||||||
|
rec.setPath(download.getPath(model).replaceAll("\\\\", "/"));
|
||||||
|
rec.setModel(model);
|
||||||
|
rec.setStartDate(download.getStartTime());
|
||||||
|
rec.setSingleFile(download.isSingleFile());
|
||||||
|
recordingProcesses.put(model, rec);
|
||||||
|
recordingManager.add(rec);
|
||||||
|
return rec;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkRecordingPreconditions(Model model) throws IOException {
|
||||||
|
ensureRecorderIsActive();
|
||||||
|
ensureModelIsNotSuspended(model);
|
||||||
|
ensureRecordUntilIsInFuture(model);
|
||||||
|
ensureNoRecordingRunningForModel(model);
|
||||||
|
ensureModelShouldBeRecorded(model);
|
||||||
|
ensureEnoughSpaceForRecording();
|
||||||
|
ensureDownloadSlotAvailable(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureRecordUntilIsInFuture(Model model) {
|
||||||
|
if (Instant.now().isAfter(model.getRecordUntil())) {
|
||||||
|
throw new RecordUntilExpiredException(model.getRecordUntil());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureEnoughSpaceForRecording() throws IOException {
|
||||||
if (!enoughSpaceForRecording()) {
|
if (!enoughSpaceForRecording()) {
|
||||||
long now = System.currentTimeMillis();
|
throw new PreconditionNotMetException("Not enough disk space for recording");
|
||||||
if ((now - lastSpaceMessage) > TimeUnit.MINUTES.toMillis(1)) {
|
|
||||||
LOG.info("Not enough space for recording, not starting recording for {}", model);
|
|
||||||
lastSpaceMessage = now;
|
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensureDownloadSlotAvailable(Model model) {
|
||||||
if (!downloadSlotAvailable()) {
|
if (!downloadSlotAvailable()) {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
if ((now - lastSpaceMessage) > TimeUnit.MINUTES.toMillis(1)) {
|
if ((now - lastPreconditionMessage) > TimeUnit.MINUTES.toMillis(1)) {
|
||||||
LOG.info("The number of downloads is maxed out");
|
LOG.info("The number of downloads is maxed out");
|
||||||
}
|
}
|
||||||
// check, if we can stop a recording for a model with lower priority
|
// check, if we can stop a recording for a model with lower priority
|
||||||
|
@ -255,47 +327,32 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
LOG.info("Stopping recording for {}. Prio {} < {}", lowerPrioModel.getName(), lowerPrioModel.getPriority(), model.getPriority());
|
LOG.info("Stopping recording for {}. Prio {} < {}", lowerPrioModel.getName(), lowerPrioModel.getPriority(), model.getPriority());
|
||||||
stopRecordingProcess(lowerPrioModel);
|
stopRecordingProcess(lowerPrioModel);
|
||||||
} else {
|
} else {
|
||||||
if ((now - lastSpaceMessage) > TimeUnit.MINUTES.toMillis(1)) {
|
throw new PreconditionNotMetException("Other models have higher prio, not starting recording for " + model.getName());
|
||||||
LOG.info("Other models have higher prio, not starting recording for {}", model.getName());
|
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.info("Starting recording for model {}", model.getName());
|
private void ensureModelShouldBeRecorded(Model model) {
|
||||||
Download download = model.createDownload();
|
if (!models.contains(model)) {
|
||||||
download.init(config, model, Instant.now());
|
throw new PreconditionNotMetException("Model " + model + " has been removed. Restarting of recording cancelled.");
|
||||||
Objects.requireNonNull(download.getStartTime(),
|
}
|
||||||
"At this point the download should have set a startTime. Make sure to set a startTime in " + download.getClass() + ".init()");
|
}
|
||||||
LOG.debug("Downloading with {}", download.getClass().getSimpleName());
|
|
||||||
|
|
||||||
Recording rec = new Recording();
|
private void ensureNoRecordingRunningForModel(Model model) {
|
||||||
rec.setDownload(download);
|
if (recordingProcesses.containsKey(model)) {
|
||||||
rec.setPath(download.getPath(model).replaceAll("\\\\", "/"));
|
throw new PreconditionNotMetException("A recording for model " + model + " is already running");
|
||||||
rec.setModel(model);
|
|
||||||
rec.setStartDate(download.getStartTime());
|
|
||||||
rec.setSingleFile(download.isSingleFile());
|
|
||||||
recordingProcesses.put(model, rec);
|
|
||||||
recordingManager.add(rec);
|
|
||||||
completionService.submit(() -> {
|
|
||||||
try {
|
|
||||||
setRecordingStatus(rec, State.RECORDING);
|
|
||||||
model.setLastRecorded(rec.getStartDate());
|
|
||||||
recordingManager.saveRecording(rec);
|
|
||||||
download.start();
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.error("Download for {} failed. Download state: {}", model.getName(), rec.getStatus(), e);
|
|
||||||
}
|
}
|
||||||
boolean deleted = deleteIfEmpty(rec);
|
|
||||||
setRecordingStatus(rec, deleted ? State.DELETED : State.WAITING);
|
|
||||||
if (!deleted) {
|
|
||||||
// only save the status, if the recording has not been deleted, otherwise we recreate the metadata file
|
|
||||||
recordingManager.saveRecording(rec);
|
|
||||||
}
|
}
|
||||||
return rec;
|
|
||||||
});
|
private void ensureModelIsNotSuspended(Model model) {
|
||||||
} finally {
|
if (model.isSuspended()) {
|
||||||
recorderLock.unlock();
|
throw new PreconditionNotMetException("Recording for model " + model + " is suspended");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureRecorderIsActive() {
|
||||||
|
if (!recording) {
|
||||||
|
throw new PreconditionNotMetException("Recorder is not in recording mode");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -449,14 +506,26 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
try {
|
try {
|
||||||
// make a copy to avoid ConcurrentModificationException
|
// make a copy to avoid ConcurrentModificationException
|
||||||
List<Recording> toStop = new ArrayList<>(recordingProcesses.values());
|
List<Recording> toStop = new ArrayList<>(recordingProcesses.values());
|
||||||
|
if (!toStop.isEmpty()) {
|
||||||
|
ExecutorService shutdownPool = Executors.newFixedThreadPool(toStop.size());
|
||||||
|
List<Future<?>> shutdownFutures = new ArrayList<>(toStop.size());
|
||||||
for (Recording rec : toStop) {
|
for (Recording rec : toStop) {
|
||||||
Optional.ofNullable(rec.getDownload()).ifPresent(Download::stop);
|
Optional.ofNullable(rec.getDownload()).ifPresent(d -> {
|
||||||
|
shutdownFutures.add(shutdownPool.submit(() -> d.stop()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
shutdownPool.shutdown();
|
||||||
|
try {
|
||||||
|
shutdownPool.awaitTermination(10, TimeUnit.MINUTES);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
recorderLock.unlock();
|
recorderLock.unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait for post-processing to finish
|
// wait for downloads to finish
|
||||||
LOG.info("Waiting for downloads to finish");
|
LOG.info("Waiting for downloads to finish");
|
||||||
for (int i = 0; i < 60; i++) {
|
for (int i = 0; i < 60; i++) {
|
||||||
if (!recordingProcesses.isEmpty()) {
|
if (!recordingProcesses.isEmpty()) {
|
||||||
|
@ -471,11 +540,12 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
|
|
||||||
// shutdown threadpools
|
// shutdown threadpools
|
||||||
try {
|
try {
|
||||||
LOG.info("Shutting down pools");
|
LOG.info("Shutting down download pool");
|
||||||
downloadPool.shutdown();
|
downloadPool.shutdown();
|
||||||
ppPool.shutdown();
|
|
||||||
client.shutdown();
|
client.shutdown();
|
||||||
downloadPool.awaitTermination(1, TimeUnit.MINUTES);
|
downloadPool.awaitTermination(1, TimeUnit.MINUTES);
|
||||||
|
LOG.info("Shutting down post-processing pool");
|
||||||
|
ppPool.shutdown();
|
||||||
int minutesToWait = 10;
|
int minutesToWait = 10;
|
||||||
LOG.info("Waiting {} minutes (max) for post-processing to finish", minutesToWait);
|
LOG.info("Waiting {} minutes (max) for post-processing to finish", minutesToWait);
|
||||||
ppPool.awaitTermination(minutesToWait, TimeUnit.MINUTES);
|
ppPool.awaitTermination(minutesToWait, TimeUnit.MINUTES);
|
||||||
|
@ -555,11 +625,11 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
return getModels().stream().filter(m -> {
|
return getModels().stream().filter(m -> {
|
||||||
try {
|
try {
|
||||||
return m.isOnline();
|
return m.isOnline();
|
||||||
} catch (IOException | ExecutionException e) {
|
|
||||||
return false;
|
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
return false;
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}).collect(Collectors.toList());
|
}).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
@ -700,4 +770,31 @@ public class NextGenLocalRecorder implements Recorder {
|
||||||
rec.setNote(note);
|
rec.setNote(note);
|
||||||
recordingManager.saveRecording(rec);
|
recordingManager.saveRecording(rec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopRecordingAt(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
|
recorderLock.lock();
|
||||||
|
try {
|
||||||
|
int index = models.indexOf(model);
|
||||||
|
if (index >= 0) {
|
||||||
|
Model m = models.get(index);
|
||||||
|
m.setRecordUntil(model.getRecordUntil());
|
||||||
|
m.setRecordUntilSubsequentAction(model.getRecordUntilSubsequentAction());
|
||||||
|
config.save();
|
||||||
|
} else {
|
||||||
|
throw new NoSuchElementException("Model " + model.getName() + " [" + model.getUrl() + "] not found in list of recorded models");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recordingProcesses.containsKey(model)) {
|
||||||
|
Recording rec = recordingProcesses.get(model);
|
||||||
|
rec.getDownload().stop();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
recorderLock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Instant.now().isAfter(model.getRecordUntil())) {
|
||||||
|
executeRecordUntilSubsequentAction(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,9 +38,11 @@ public class OnlineMonitor extends Thread {
|
||||||
private Map<Model, Model.State> states = new HashMap<>();
|
private Map<Model, Model.State> states = new HashMap<>();
|
||||||
|
|
||||||
private Map<String, ExecutorService> executors = new HashMap<>();
|
private Map<String, ExecutorService> executors = new HashMap<>();
|
||||||
|
private Config config;
|
||||||
|
|
||||||
public OnlineMonitor(Recorder recorder) {
|
public OnlineMonitor(Recorder recorder, Config config) {
|
||||||
this.recorder = recorder;
|
this.recorder = recorder;
|
||||||
|
this.config = config;
|
||||||
setName("OnlineMonitor");
|
setName("OnlineMonitor");
|
||||||
setDaemon(true);
|
setDaemon(true);
|
||||||
}
|
}
|
||||||
|
@ -80,8 +82,12 @@ public class OnlineMonitor extends Thread {
|
||||||
// submit online check jobs to the executor for the model's site
|
// submit online check jobs to the executor for the model's site
|
||||||
List<Future<?>> futures = new LinkedList<>();
|
List<Future<?>> futures = new LinkedList<>();
|
||||||
for (Model model : models) {
|
for (Model model : models) {
|
||||||
|
if (config.getSettings().onlineCheckSkipsPausedModels && model.isSuspended()) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
futures.add(updateModel(model));
|
futures.add(updateModel(model));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// wait for all jobs to finish
|
// wait for all jobs to finish
|
||||||
for (Future<?> future : futures) {
|
for (Future<?> future : futures) {
|
||||||
try {
|
try {
|
||||||
|
@ -134,7 +140,7 @@ public class OnlineMonitor extends Thread {
|
||||||
|
|
||||||
private void suspendUntilNextIteration(List<Model> models, Duration timeCheckTook) {
|
private void suspendUntilNextIteration(List<Model> models, Duration timeCheckTook) {
|
||||||
LOG.trace("Online check for {} models took {} seconds", models.size(), timeCheckTook.getSeconds());
|
LOG.trace("Online check for {} models took {} seconds", models.size(), timeCheckTook.getSeconds());
|
||||||
long sleepTime = Config.getInstance().getSettings().onlineCheckIntervalInSecs;
|
long sleepTime = config.getSettings().onlineCheckIntervalInSecs;
|
||||||
if(timeCheckTook.getSeconds() < sleepTime) {
|
if(timeCheckTook.getSeconds() < sleepTime) {
|
||||||
try {
|
try {
|
||||||
if (running) {
|
if (running) {
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package ctbrec.recorder;
|
||||||
|
|
||||||
|
public class PreconditionNotMetException extends RuntimeException {
|
||||||
|
public PreconditionNotMetException() {
|
||||||
|
super("Precondition not met");
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreconditionNotMetException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PreconditionNotMetException(String message, Throwable t) {
|
||||||
|
super(message, t);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package ctbrec.recorder;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
public class RecordUntilExpiredException extends PreconditionNotMetException {
|
||||||
|
private Instant until;
|
||||||
|
|
||||||
|
public RecordUntilExpiredException(Instant until) {
|
||||||
|
this.until = until;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMessage() {
|
||||||
|
LocalDateTime dateTime = LocalDateTime.ofInstant(until, ZoneId.systemDefault());
|
||||||
|
String date = DateTimeFormatter.ISO_LOCAL_DATE.format(dateTime);
|
||||||
|
return "Recording expired at " + date;
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ public interface Recorder {
|
||||||
public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
|
public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
|
||||||
|
|
||||||
public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
|
public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
|
||||||
|
public void stopRecordingAt(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
|
||||||
|
|
||||||
public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
|
public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,11 @@ public class RemoteRecorder implements Recorder {
|
||||||
sendRequest("stop", model);
|
sendRequest("stop", model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopRecordingAt(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
|
sendRequest("stopAt", model);
|
||||||
|
}
|
||||||
|
|
||||||
private void sendRequest(String action, Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
private void sendRequest(String action, Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
|
||||||
String payload = modelRequestAdapter.toJson(new ModelRequest(action, model));
|
String payload = modelRequestAdapter.toJson(new ModelRequest(action, model));
|
||||||
LOG.debug("Sending request to recording server: {}", payload);
|
LOG.debug("Sending request to recording server: {}", payload);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import java.text.DecimalFormat;
|
||||||
|
|
||||||
public class StreamSource implements Comparable<StreamSource> {
|
public class StreamSource implements Comparable<StreamSource> {
|
||||||
public static final int ORIGIN = Integer.MAX_VALUE - 1;
|
public static final int ORIGIN = Integer.MAX_VALUE - 1;
|
||||||
|
public static final int UNKNOWN = Integer.MAX_VALUE;
|
||||||
public int bandwidth;
|
public int bandwidth;
|
||||||
public int width;
|
public int width;
|
||||||
public int height;
|
public int height;
|
||||||
|
@ -45,7 +46,7 @@ public class StreamSource implements Comparable<StreamSource> {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
DecimalFormat df = new DecimalFormat("0.00");
|
DecimalFormat df = new DecimalFormat("0.00");
|
||||||
float mbit = bandwidth / 1024.0f / 1024.0f;
|
float mbit = bandwidth / 1024.0f / 1024.0f;
|
||||||
if (height == Integer.MAX_VALUE) {
|
if (height == UNKNOWN) {
|
||||||
return "unknown resolution (" + df.format(mbit) + " Mbit/s)";
|
return "unknown resolution (" + df.format(mbit) + " Mbit/s)";
|
||||||
} else if (height == ORIGIN) {
|
} else if (height == ORIGIN) {
|
||||||
return "Origin";
|
return "Origin";
|
||||||
|
@ -61,7 +62,7 @@ public class StreamSource implements Comparable<StreamSource> {
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(StreamSource o) {
|
public int compareTo(StreamSource o) {
|
||||||
int heightDiff = height - o.height;
|
int heightDiff = height - o.height;
|
||||||
if(heightDiff != 0 && height != Integer.MAX_VALUE && o.height != Integer.MAX_VALUE) {
|
if(heightDiff != 0 && height != UNKNOWN && o.height != UNKNOWN) {
|
||||||
return heightDiff;
|
return heightDiff;
|
||||||
} else {
|
} else {
|
||||||
return bandwidth - o.bandwidth;
|
return bandwidth - o.bandwidth;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package ctbrec.recorder.download.hls;
|
package ctbrec.recorder.download.hls;
|
||||||
|
|
||||||
import static ctbrec.io.HttpConstants.*;
|
import static ctbrec.io.HttpConstants.*;
|
||||||
|
import static ctbrec.io.HttpConstants.ORIGIN;
|
||||||
|
import static ctbrec.recorder.download.StreamSource.*;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -64,7 +66,7 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
|
||||||
protected volatile boolean running = false;
|
protected volatile boolean running = false;
|
||||||
protected Model model = new UnknownModel();
|
protected Model model = new UnknownModel();
|
||||||
protected transient LinkedBlockingQueue<Runnable> downloadQueue = new LinkedBlockingQueue<>(50);
|
protected transient LinkedBlockingQueue<Runnable> downloadQueue = new LinkedBlockingQueue<>(50);
|
||||||
protected transient ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue, createThreadFactory());
|
protected transient ExecutorService downloadThreadPool = new ThreadPoolExecutor(0, 5, 20, TimeUnit.SECONDS, downloadQueue, createThreadFactory());
|
||||||
protected State state = State.UNKNOWN;
|
protected State state = State.UNKNOWN;
|
||||||
private int playlistEmptyCount = 0;
|
private int playlistEmptyCount = 0;
|
||||||
|
|
||||||
|
@ -160,12 +162,12 @@ public abstract class AbstractHlsDownload extends AbstractDownload {
|
||||||
LOG.debug("{} selected {}", model.getName(), streamSources.get(model.getStreamUrlIndex()));
|
LOG.debug("{} selected {}", model.getName(), streamSources.get(model.getStreamUrlIndex()));
|
||||||
url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl();
|
url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl();
|
||||||
} else {
|
} else {
|
||||||
// filter out stream resolutions, which are too high
|
// filter out stream resolutions, which are out of range of the configured min and max
|
||||||
int minRes = Config.getInstance().getSettings().minimumResolution;
|
int minRes = Config.getInstance().getSettings().minimumResolution;
|
||||||
int maxRes = Config.getInstance().getSettings().maximumResolution;
|
int maxRes = Config.getInstance().getSettings().maximumResolution;
|
||||||
List<StreamSource> filteredStreamSources = streamSources.stream()
|
List<StreamSource> filteredStreamSources = streamSources.stream()
|
||||||
.filter(src -> src.height == 0 || minRes <= src.height)
|
.filter(src -> src.height == 0 || src.height == UNKNOWN || minRes <= src.height)
|
||||||
.filter(src -> src.height == 0 || maxRes >= src.height)
|
.filter(src -> src.height == 0 || src.height == UNKNOWN || maxRes >= src.height)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
if (filteredStreamSources.isEmpty()) {
|
if (filteredStreamSources.isEmpty()) {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
package ctbrec.recorder.download.hls;
|
package ctbrec.recorder.download.hls;
|
||||||
|
|
||||||
|
import static java.util.Optional.*;
|
||||||
|
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
@ -14,12 +17,13 @@ import java.time.ZonedDateTime;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -56,6 +60,7 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
private transient OutputStream ffmpegStdIn;
|
private transient OutputStream ffmpegStdIn;
|
||||||
protected transient Thread ffmpegThread;
|
protected transient Thread ffmpegThread;
|
||||||
private transient Object ffmpegStartMonitor = new Object();
|
private transient Object ffmpegStartMonitor = new Object();
|
||||||
|
private Queue<Future<byte[]>> downloads = new LinkedList<>();
|
||||||
|
|
||||||
public MergedFfmpegHlsDownload(HttpClient client) {
|
public MergedFfmpegHlsDownload(HttpClient client) {
|
||||||
super(client);
|
super(client);
|
||||||
|
@ -104,6 +109,7 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
LOG.debug("Starting to download segments");
|
LOG.debug("Starting to download segments");
|
||||||
downloadSegments(segments, true);
|
downloadSegments(segments, true);
|
||||||
ffmpegThread.join();
|
ffmpegThread.join();
|
||||||
|
LOG.debug("FFmpeg thread terminated");
|
||||||
}
|
}
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
throw new IOException("Couldn't parse stream information", e);
|
throw new IOException("Couldn't parse stream information", e);
|
||||||
|
@ -178,7 +184,8 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ffmpegThread.setName("FFmpeg");
|
String name = "FFmpeg " + ofNullable(model).map(Model::getName).orElse("").trim();
|
||||||
|
ffmpegThread.setName(name);
|
||||||
ffmpegThread.start();
|
ffmpegThread.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,6 +236,9 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
LOG.info("Unexpected error while downloading {}", model, e);
|
LOG.info("Unexpected error while downloading {}", model, e);
|
||||||
}
|
}
|
||||||
running = false;
|
running = false;
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
LOG.info("Malformed URL {} - {}", model, segmentPlaylistUri, e);
|
||||||
|
running = false;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.info("Unexpected error while downloading {}", model, e);
|
LOG.info("Unexpected error while downloading {}", model, e);
|
||||||
running = false;
|
running = false;
|
||||||
|
@ -250,7 +260,7 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
int skip = nextSegment - lsp.seq;
|
int skip = nextSegment - lsp.seq;
|
||||||
|
|
||||||
// add segments to download threadpool
|
// add segments to download threadpool
|
||||||
Queue<Future<byte[]>> downloads = new LinkedList<>();
|
downloads.clear();
|
||||||
if (downloadQueue.remainingCapacity() == 0) {
|
if (downloadQueue.remainingCapacity() == 0) {
|
||||||
LOG.warn("Download to slow for this stream. Download queue is full. Skipping segment");
|
LOG.warn("Download to slow for this stream. Download queue is full. Skipping segment");
|
||||||
} else {
|
} else {
|
||||||
|
@ -274,11 +284,15 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
private void writeFinishedSegments(Queue<Future<byte[]>> downloads) throws ExecutionException, IOException {
|
private void writeFinishedSegments(Queue<Future<byte[]>> downloads) throws ExecutionException, IOException {
|
||||||
for (Future<byte[]> downloadFuture : downloads) {
|
for (Future<byte[]> downloadFuture : downloads) {
|
||||||
try {
|
try {
|
||||||
byte[] segmentData = downloadFuture.get();
|
byte[] segmentData = downloadFuture.get(30, TimeUnit.SECONDS);
|
||||||
writeSegment(segmentData);
|
writeSegment(segmentData);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
LOG.error("Error while downloading segment", e);
|
LOG.error("Error while downloading segment", e);
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
LOG.info("Segment download took too long for {}. Not waiting for it any longer", getModel());
|
||||||
|
} catch (CancellationException e) {
|
||||||
|
LOG.info("Segment download cancelled");
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
Throwable cause = e.getCause();
|
Throwable cause = e.getCause();
|
||||||
if (cause instanceof MissingSegmentException) {
|
if (cause instanceof MissingSegmentException) {
|
||||||
|
@ -286,7 +300,7 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
LOG.debug("Error while downloading segment, because model {} is offline. Stopping now", model.getName());
|
LOG.debug("Error while downloading segment, because model {} is offline. Stopping now", model.getName());
|
||||||
running = false;
|
running = false;
|
||||||
} else {
|
} else {
|
||||||
LOG.debug("Segment not available, but model {} still online. Going on", Optional.ofNullable(model).map(Model::getName).orElse("n/a"));
|
LOG.debug("Segment not available, but model {} still online. Going on", ofNullable(model).map(Model::getName).orElse("n/a"));
|
||||||
}
|
}
|
||||||
} else if (cause instanceof HttpException) {
|
} else if (cause instanceof HttpException) {
|
||||||
HttpException he = (HttpException) cause;
|
HttpException he = (HttpException) cause;
|
||||||
|
@ -295,10 +309,10 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
running = false;
|
running = false;
|
||||||
} else {
|
} else {
|
||||||
if (he.getResponseCode() == 404) {
|
if (he.getResponseCode() == 404) {
|
||||||
LOG.info("Playlist for {} not found [HTTP 404]. Stopping now", Optional.ofNullable(model).map(Model::getName).orElse("n/a"));
|
LOG.info("Playlist for {} not found [HTTP 404]. Stopping now", ofNullable(model).map(Model::getName).orElse("n/a"));
|
||||||
running = false;
|
running = false;
|
||||||
} else if (he.getResponseCode() == 403) {
|
} else if (he.getResponseCode() == 403) {
|
||||||
LOG.info("Playlist for {} not accessible [HTTP 403]. Stopping now", Optional.ofNullable(model).map(Model::getName).orElse("n/a"));
|
LOG.info("Playlist for {} not accessible [HTTP 403]. Stopping now", ofNullable(model).map(Model::getName).orElse("n/a"));
|
||||||
running = false;
|
running = false;
|
||||||
} else {
|
} else {
|
||||||
throw he;
|
throw he;
|
||||||
|
@ -374,9 +388,23 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
@Override
|
@Override
|
||||||
synchronized void internalStop() {
|
synchronized void internalStop() {
|
||||||
running = false;
|
running = false;
|
||||||
if (ffmpegStdIn != null) {
|
|
||||||
try {
|
try {
|
||||||
downloadQueue.clear();
|
downloadQueue.clear();
|
||||||
|
for (Future<?> future : downloads) {
|
||||||
|
future.cancel(true);
|
||||||
|
}
|
||||||
|
downloadThreadPool.shutdownNow();
|
||||||
|
LOG.debug("Waiting for segment download thread pool to terminate for model {}", getModel());
|
||||||
|
downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS);
|
||||||
|
LOG.debug("Segment download thread pool terminated for model {}", getModel());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOG.error("Interrupted while waiting for segment pool to shutdown");
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ffmpegStdIn != null) {
|
||||||
|
try {
|
||||||
ffmpegStdIn.close();
|
ffmpegStdIn.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.error("Couldn't terminate FFmpeg by closing stdin", e);
|
LOG.error("Couldn't terminate FFmpeg by closing stdin", e);
|
||||||
|
@ -385,7 +413,7 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
|
|
||||||
if (ffmpeg != null) {
|
if (ffmpeg != null) {
|
||||||
try {
|
try {
|
||||||
boolean waitFor = ffmpeg.waitFor(5, TimeUnit.MINUTES);
|
boolean waitFor = ffmpeg.waitFor(45, TimeUnit.SECONDS);
|
||||||
if (!waitFor && ffmpeg.isAlive()) {
|
if (!waitFor && ffmpeg.isAlive()) {
|
||||||
LOG.info("FFmpeg didn't terminate. Destroying the process with force!");
|
LOG.info("FFmpeg didn't terminate. Destroying the process with force!");
|
||||||
ffmpeg.destroyForcibly();
|
ffmpeg.destroyForcibly();
|
||||||
|
@ -415,7 +443,7 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
|
||||||
int maxTries = 3;
|
int maxTries = 3;
|
||||||
for (int i = 1; i <= maxTries && running; i++) {
|
for (int i = 1; i <= maxTries && running; i++) {
|
||||||
Builder builder = new Request.Builder().url(url);
|
Builder builder = new Request.Builder().url(url);
|
||||||
addHeaders(builder, Optional.ofNullable(model).map(Model::getHttpHeaderFactory).map(HttpHeaderFactory::createSegmentHeaders).orElse(new HashMap<>()));
|
addHeaders(builder, ofNullable(model).map(Model::getHttpHeaderFactory).map(HttpHeaderFactory::createSegmentHeaders).orElse(new HashMap<>()));
|
||||||
Request request = builder.build();
|
Request request = builder.build();
|
||||||
try (Response response = client.execute(request)) {
|
try (Response response = client.execute(request)) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
|
|
|
@ -44,7 +44,7 @@ public class Cam4 extends AbstractSite {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Model createModel(String name) {
|
public Cam4Model createModel(String name) {
|
||||||
Cam4Model m = new Cam4Model();
|
Cam4Model m = new Cam4Model();
|
||||||
m.setSite(this);
|
m.setSite(this);
|
||||||
m.setName(name);
|
m.setName(name);
|
||||||
|
|
|
@ -103,7 +103,7 @@ public class Cam4Model extends AbstractModel {
|
||||||
onlineState = OFFLINE;
|
onlineState = OFFLINE;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
LOG.debug("Unknown show type {}", showType);
|
LOG.debug("Unknown show type [{}]", showType);
|
||||||
onlineState = UNKNOWN;
|
onlineState = UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
@ -44,56 +45,82 @@ public class CamsodaModel extends AbstractModel {
|
||||||
private static final String EDGE_SERVERS = "edge_servers";
|
private static final String EDGE_SERVERS = "edge_servers";
|
||||||
private static final String STATUS = "status";
|
private static final String STATUS = "status";
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(CamsodaModel.class);
|
private static final Logger LOG = LoggerFactory.getLogger(CamsodaModel.class);
|
||||||
private String streamUrl;
|
|
||||||
private transient List<StreamSource> streamSources = null;
|
private transient List<StreamSource> streamSources = null;
|
||||||
private transient boolean isNew;
|
private transient boolean isNew;
|
||||||
|
|
||||||
private float sortOrder = 0;
|
private float sortOrder = 0;
|
||||||
private Random random = new Random();
|
private Random random = new Random();
|
||||||
int[] resolution = new int[2];
|
int[] resolution = new int[2];
|
||||||
boolean oldStreamUrl = true;
|
|
||||||
|
|
||||||
public String getStreamUrl() throws IOException {
|
public String getStreamUrl() throws IOException {
|
||||||
if (streamUrl == null) {
|
Request req = createJsonRequest(getTokenInfoUrl());
|
||||||
if(oldStreamUrl) {
|
JSONObject response = executeJsonRequest(req);
|
||||||
loadModel();
|
if (response.optInt(STATUS) == 1 && response.optJSONArray(EDGE_SERVERS) != null && response.optJSONArray(EDGE_SERVERS).length() > 0) {
|
||||||
|
String edgeServer = response.getJSONArray(EDGE_SERVERS).getString(0);
|
||||||
|
String streamName = response.getString(STREAM_NAME);
|
||||||
|
String token = response.getString("token");
|
||||||
|
return constructStreamUrl(edgeServer, streamName, token);
|
||||||
} else {
|
} else {
|
||||||
getNewStreamUrl();
|
throw new JSONException("JSON response has not the expected structure");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return streamUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getNewStreamUrl() throws IOException {
|
private String getTokenInfoUrl() {
|
||||||
String guestUsername = "guest_" + 10_000 + random.nextInt(50_000);
|
String guestUsername = "guest_" + 10_000 + random.nextInt(50_000);
|
||||||
String url = site.getBaseUrl() + "/api/v1/video/vtoken/" + getName() + "?username=" + guestUsername;
|
String tokenInfoUrl = site.getBaseUrl() + "/api/v1/video/vtoken/" + getName() + "?username=" + guestUsername;
|
||||||
Request req = new Request.Builder()
|
return tokenInfoUrl;
|
||||||
.url(url)
|
}
|
||||||
|
|
||||||
|
private String constructStreamUrl(String edgeServer, String streamName, String token) {
|
||||||
|
StringBuilder url = new StringBuilder("https://");
|
||||||
|
url.append(edgeServer).append('/');
|
||||||
|
if (streamName.contains("-flu")) {
|
||||||
|
url.append(streamName);
|
||||||
|
url.append("_h264_aac");
|
||||||
|
url.append(streamName.contains("-flu-hd") ? "_720p" : "_480p");
|
||||||
|
url.append("/index.m3u8");
|
||||||
|
if (!isPublic(streamName)) {
|
||||||
|
url.append("?token=").append(token);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// https://vide7-ord.camsoda.com/cam/mp4:maxandtokio-enc10-ord_h264_aac_480p/playlist.m3u8
|
||||||
|
url.append("cam/mp4:");
|
||||||
|
url.append(streamName);
|
||||||
|
url.append("_h264_aac_480p/playlist.m3u8");
|
||||||
|
}
|
||||||
|
LOG.trace("Stream URL: {}", url);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Request createJsonRequest(String tokenInfoUrl) {
|
||||||
|
return new Request.Builder()
|
||||||
|
.url(tokenInfoUrl)
|
||||||
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
||||||
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
|
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
|
||||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
.build();
|
.build();
|
||||||
try (Response response = site.getHttpClient().execute(req)) {
|
}
|
||||||
|
|
||||||
|
private JSONObject executeJsonRequest(Request request) throws IOException {
|
||||||
|
try (Response response = site.getHttpClient().execute(request)) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
JSONObject jsonResponse = new JSONObject(response.body().string());
|
JSONObject jsonResponse = new JSONObject(response.body().string());
|
||||||
if (jsonResponse.optInt(STATUS) == 1) {
|
return jsonResponse;
|
||||||
String edgeServer = jsonResponse.getJSONArray(EDGE_SERVERS).getString(0);
|
|
||||||
String streamName = jsonResponse.getString(STREAM_NAME);
|
|
||||||
String token = jsonResponse.getString("token");
|
|
||||||
streamUrl = "https://" + edgeServer + '/' + streamName + "_h264_aac_480p/index.m3u8?token=" + token;
|
|
||||||
} else {
|
|
||||||
throw new JSONException("Response does not contain a token");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new HttpException(response.code(), response.message());
|
throw new HttpException(response.code(), response.message());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return streamUrl;
|
}
|
||||||
|
|
||||||
|
private boolean isPublic(String streamName) {
|
||||||
|
return Optional.ofNullable(streamName).orElse("").contains("_public");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||||
|
try {
|
||||||
String playlistUrl = getStreamUrl();
|
String playlistUrl = getStreamUrl();
|
||||||
if (playlistUrl == null) {
|
if (playlistUrl == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
@ -112,13 +139,9 @@ public class CamsodaModel extends AbstractModel {
|
||||||
MasterPlaylist master = playlist.getMasterPlaylist();
|
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||||
PlaylistData playlistData = master.getPlaylists().get(0);
|
PlaylistData playlistData = master.getPlaylists().get(0);
|
||||||
StreamSource streamsource = new StreamSource();
|
StreamSource streamsource = new StreamSource();
|
||||||
if (oldStreamUrl) {
|
int cutOffAt = Math.max(playlistUrl.indexOf("index.m3u8"), playlistUrl.indexOf("playlist.m3u8"));
|
||||||
streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", playlistData.getUri());
|
|
||||||
} else {
|
|
||||||
int cutOffAt = playlistUrl.indexOf("index.m3u8");
|
|
||||||
String segmentPlaylistUrl = playlistUrl.substring(0, cutOffAt) + playlistData.getUri();
|
String segmentPlaylistUrl = playlistUrl.substring(0, cutOffAt) + playlistData.getUri();
|
||||||
streamsource.mediaPlaylistUrl = segmentPlaylistUrl;
|
streamsource.mediaPlaylistUrl = segmentPlaylistUrl;
|
||||||
}
|
|
||||||
if (playlistData.hasStreamInfo()) {
|
if (playlistData.hasStreamInfo()) {
|
||||||
StreamInfo info = playlistData.getStreamInfo();
|
StreamInfo info = playlistData.getStreamInfo();
|
||||||
streamsource.bandwidth = info.getBandwidth();
|
streamsource.bandwidth = info.getBandwidth();
|
||||||
|
@ -133,10 +156,13 @@ public class CamsodaModel extends AbstractModel {
|
||||||
streamSources.add(streamsource);
|
streamSources.add(streamsource);
|
||||||
} else {
|
} else {
|
||||||
LOG.trace("Response: {}", response.body().string());
|
LOG.trace("Response: {}", response.body().string());
|
||||||
throw new HttpException(response.code(), response.message());
|
throw new HttpException(playlistUrl, response.code(), response.message());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return streamSources;
|
return streamSources;
|
||||||
|
} catch (JSONException e) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadModel() throws IOException {
|
private void loadModel() throws IOException {
|
||||||
|
@ -151,15 +177,9 @@ public class CamsodaModel extends AbstractModel {
|
||||||
try (Response response = site.getHttpClient().execute(req)) {
|
try (Response response = site.getHttpClient().execute(req)) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
JSONObject result = new JSONObject(response.body().string());
|
JSONObject result = new JSONObject(response.body().string());
|
||||||
if (result.getBoolean(STATUS)) {
|
if (result.optBoolean(STATUS)) {
|
||||||
JSONObject chat = result.getJSONObject("user").getJSONObject("chat");
|
JSONObject chat = result.getJSONObject("user").getJSONObject("chat");
|
||||||
String status = chat.getString(STATUS);
|
String status = chat.getString(STATUS);
|
||||||
oldStreamUrl = !chat.getString(STREAM_NAME).contains("/");
|
|
||||||
if (oldStreamUrl && chat.has(EDGE_SERVERS)) {
|
|
||||||
String edgeServer = chat.getJSONArray(EDGE_SERVERS).getString(0);
|
|
||||||
String streamName = chat.getString(STREAM_NAME);
|
|
||||||
streamUrl = "https://" + edgeServer + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8";
|
|
||||||
}
|
|
||||||
setOnlineStateByStatus(status);
|
setOnlineStateByStatus(status);
|
||||||
} else {
|
} else {
|
||||||
throw new IOException("Result was not ok");
|
throw new IOException("Result was not ok");
|
||||||
|
@ -222,11 +242,11 @@ public class CamsodaModel extends AbstractModel {
|
||||||
return resolution;
|
return resolution;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
List<StreamSource> streamSources = getStreamSources();
|
List<StreamSource> sources = getStreamSources();
|
||||||
if (streamSources.isEmpty()) {
|
if (sources.isEmpty()) {
|
||||||
return new int[] { 0, 0 };
|
return new int[] { 0, 0 };
|
||||||
} else {
|
} else {
|
||||||
StreamSource src = streamSources.get(0);
|
StreamSource src = sources.get(0);
|
||||||
resolution = new int[] { src.width, src.height };
|
resolution = new int[] { src.width, src.height };
|
||||||
return resolution;
|
return resolution;
|
||||||
}
|
}
|
||||||
|
@ -309,10 +329,6 @@ public class CamsodaModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStreamUrl(String streamUrl) {
|
|
||||||
this.streamUrl = streamUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getSortOrder() {
|
public float getSortOrder() {
|
||||||
return sortOrder;
|
return sortOrder;
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,12 +86,11 @@ public class Flirt4FreeModel extends AbstractModel {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
JSONObject json = new JSONObject(body);
|
JSONObject json = new JSONObject(body);
|
||||||
//LOG.debug("check model status: {}", json.toString(2));
|
online = Objects.equals(json.optString("status"), "online"); // online is true, even if the model is in private or away
|
||||||
online = Objects.equals(json.optString("status"), "online");
|
updateModelId(json);
|
||||||
id = String.valueOf(json.get("model_id"));
|
|
||||||
if (online) {
|
if (online) {
|
||||||
try {
|
try {
|
||||||
loadStreamUrl();
|
loadModelInfo();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
online = false;
|
online = false;
|
||||||
onlineState = Model.State.OFFLINE;
|
onlineState = Model.State.OFFLINE;
|
||||||
|
@ -109,6 +108,18 @@ public class Flirt4FreeModel extends AbstractModel {
|
||||||
return online;
|
return online;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateModelId(JSONObject json) {
|
||||||
|
if (json.has("model_id")) {
|
||||||
|
Object modelId = json.get("model_id");
|
||||||
|
if (modelId instanceof Number) {
|
||||||
|
Number n = (Number) modelId;
|
||||||
|
if (n.intValue() > 0) {
|
||||||
|
id = String.valueOf(json.get("model_id"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void loadModelInfo() throws IOException, InterruptedException {
|
private void loadModelInfo() throws IOException, InterruptedException {
|
||||||
String url = getSite().getBaseUrl() + "/webservices/chat-room-interface.php?a=login_room&model_id=" + id;
|
String url = getSite().getBaseUrl() + "/webservices/chat-room-interface.php?a=login_room&model_id=" + id;
|
||||||
LOG.trace("Loading url {}", url);
|
LOG.trace("Loading url {}", url);
|
||||||
|
@ -127,13 +138,15 @@ public class Flirt4FreeModel extends AbstractModel {
|
||||||
// LOG.debug("chat-room-interface {}", json.toString(2));
|
// LOG.debug("chat-room-interface {}", json.toString(2));
|
||||||
JSONObject config = json.getJSONObject("config");
|
JSONObject config = json.getJSONObject("config");
|
||||||
JSONObject performer = config.getJSONObject("performer");
|
JSONObject performer = config.getJSONObject("performer");
|
||||||
setName(performer.optString("name_seo", "n/a"));
|
|
||||||
setDisplayName(performer.optString("name", "n/a"));
|
|
||||||
setUrl(getSite().getBaseUrl() + "/rooms/" + getName() + '/');
|
setUrl(getSite().getBaseUrl() + "/rooms/" + getName() + '/');
|
||||||
|
setDisplayName(performer.optString("name", getName()));
|
||||||
JSONObject room = config.getJSONObject("room");
|
JSONObject room = config.getJSONObject("room");
|
||||||
chatHost = room.getString("host");
|
chatHost = room.getString("host");
|
||||||
chatPort = room.getString("port_to_be");
|
chatPort = room.getString("port_to_be");
|
||||||
chatToken = json.getString("token_enc");
|
chatToken = json.getString("token_enc");
|
||||||
|
String status = room.optString("status");
|
||||||
|
setOnlineState(mapStatus(status));
|
||||||
|
online = onlineState == State.ONLINE;
|
||||||
JSONObject user = config.getJSONObject("user");
|
JSONObject user = config.getJSONObject("user");
|
||||||
userIp = user.getString("ip");
|
userIp = user.getString("ip");
|
||||||
} else {
|
} else {
|
||||||
|
@ -147,6 +160,19 @@ public class Flirt4FreeModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private State mapStatus(String status) {
|
||||||
|
switch (status) {
|
||||||
|
case "P":
|
||||||
|
case "F":
|
||||||
|
return State.PRIVATE;
|
||||||
|
case "A":
|
||||||
|
return State.AWAY;
|
||||||
|
case "O":
|
||||||
|
default:
|
||||||
|
return State.ONLINE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||||
return getStreamSources(true);
|
return getStreamSources(true);
|
||||||
|
@ -238,10 +264,9 @@ public class Flirt4FreeModel extends AbstractModel {
|
||||||
streamHost = data.getString("stream_host"); // TODO look, if the stream_host is equal to the one encoded in base64 in some of the ajax requests (parameters)
|
streamHost = data.getString("stream_host"); // TODO look, if the stream_host is equal to the one encoded in base64 in some of the ajax requests (parameters)
|
||||||
online = true;
|
online = true;
|
||||||
isInteractiveShow = data.optString("devices").equals("1");
|
isInteractiveShow = data.optString("devices").equals("1");
|
||||||
if(data.optString("room_state").equals("P")) {
|
String roomState = data.optString("room_state");
|
||||||
onlineState = Model.State.PRIVATE;
|
onlineState = mapStatus(roomState);
|
||||||
online = false;
|
online = onlineState == State.ONLINE;
|
||||||
}
|
|
||||||
if(data.optString("room_state").equals("0") && data.optString("login_group_id").equals("14")) {
|
if(data.optString("room_state").equals("0") && data.optString("login_group_id").equals("14")) {
|
||||||
onlineState = Model.State.GROUP;
|
onlineState = Model.State.GROUP;
|
||||||
online = false;
|
online = false;
|
||||||
|
@ -262,6 +287,7 @@ public class Flirt4FreeModel extends AbstractModel {
|
||||||
synchronized (monitor) {
|
synchronized (monitor) {
|
||||||
monitor.notify();
|
monitor.notify();
|
||||||
}
|
}
|
||||||
|
response.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -455,9 +481,11 @@ public class Flirt4FreeModel extends AbstractModel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void readSiteSpecificData(JsonReader reader) throws IOException {
|
public void readSiteSpecificData(JsonReader reader) throws IOException {
|
||||||
|
if (reader.hasNext()) {
|
||||||
reader.nextName();
|
reader.nextName();
|
||||||
id = reader.nextString();
|
id = reader.nextString();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
|
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
|
||||||
|
@ -481,7 +509,7 @@ public class Flirt4FreeModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void acquireSlot() throws InterruptedException {
|
private void acquireSlot() throws InterruptedException {
|
||||||
//LOG.debug("Acquire: {}", requestThrottle.availablePermits());
|
//LOG.debug("Acquire: {} - Queue: {}", requestThrottle.availablePermits(), requestThrottle.getQueueLength());
|
||||||
requestThrottle.acquire();
|
requestThrottle.acquire();
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
long millisSinceLastRequest = now - lastRequest;
|
long millisSinceLastRequest = now - lastRequest;
|
||||||
|
@ -494,6 +522,6 @@ public class Flirt4FreeModel extends AbstractModel {
|
||||||
private void releaseSlot() {
|
private void releaseSlot() {
|
||||||
lastRequest = System.currentTimeMillis();
|
lastRequest = System.currentTimeMillis();
|
||||||
requestThrottle.release();
|
requestThrottle.release();
|
||||||
//LOG.debug("Release: {}", requestThrottle.availablePermits());
|
// LOG.debug("Release: {}", requestThrottle.availablePermits());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import static ctbrec.io.HttpConstants.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
@ -13,6 +14,8 @@ import java.util.regex.Pattern;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.jsoup.nodes.Element;
|
import org.jsoup.nodes.Element;
|
||||||
import org.jsoup.select.Elements;
|
import org.jsoup.select.Elements;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
|
@ -27,6 +30,7 @@ import okhttp3.Response;
|
||||||
|
|
||||||
public class LiveJasmin extends AbstractSite {
|
public class LiveJasmin extends AbstractSite {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(LiveJasmin.class);
|
||||||
public static String baseUrl = "";
|
public static String baseUrl = "";
|
||||||
public static String baseDomain = "";
|
public static String baseDomain = "";
|
||||||
private HttpClient httpClient;
|
private HttpClient httpClient;
|
||||||
|
@ -169,7 +173,8 @@ public class LiveJasmin extends AbstractSite {
|
||||||
}
|
}
|
||||||
return models;
|
return models;
|
||||||
} else {
|
} else {
|
||||||
throw new IOException("Response was not successful: " + url + "\n" + body);
|
LOG.debug("Response was not successful: {}\n{}", url, body);
|
||||||
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new HttpException(response.code(), response.message());
|
throw new HttpException(response.code(), response.message());
|
||||||
|
@ -198,7 +203,7 @@ public class LiveJasmin extends AbstractSite {
|
||||||
String name = m.group(1);
|
String name = m.group(1);
|
||||||
return createModel(name);
|
return createModel(name);
|
||||||
}
|
}
|
||||||
m = Pattern.compile("http.*?livejasmin\\.com.*?/chat-html5/(.*)").matcher(url);
|
m = Pattern.compile("http.*?livejasmin\\.com.*?/chat(?:-html5)?/(.*)").matcher(url);
|
||||||
if(m.find()) {
|
if(m.find()) {
|
||||||
String name = m.group(1);
|
String name = m.group(1);
|
||||||
return createModel(name);
|
return createModel(name);
|
||||||
|
|
|
@ -72,6 +72,8 @@ public class LiveJasminModel extends AbstractModel {
|
||||||
JSONObject config = data.getJSONObject("config");
|
JSONObject config = data.getJSONObject("config");
|
||||||
JSONObject chatRoom = config.getJSONObject("chatRoom");
|
JSONObject chatRoom = config.getJSONObject("chatRoom");
|
||||||
setId(chatRoom.getString("p_id"));
|
setId(chatRoom.getString("p_id"));
|
||||||
|
setName(chatRoom.getString("performer_id"));
|
||||||
|
setDisplayName(chatRoom.getString("display_name"));
|
||||||
if (chatRoom.has("profile_picture_url")) {
|
if (chatRoom.has("profile_picture_url")) {
|
||||||
setPreview(chatRoom.getString("profile_picture_url"));
|
setPreview(chatRoom.getString("profile_picture_url"));
|
||||||
}
|
}
|
||||||
|
@ -80,11 +82,14 @@ public class LiveJasminModel extends AbstractModel {
|
||||||
if (chatRoom.optInt("is_on_private", 0) == 1) {
|
if (chatRoom.optInt("is_on_private", 0) == 1) {
|
||||||
onlineState = State.PRIVATE;
|
onlineState = State.PRIVATE;
|
||||||
}
|
}
|
||||||
|
if (chatRoom.optInt("is_video_call_enabled", 0) == 1) {
|
||||||
|
onlineState = State.PRIVATE;
|
||||||
|
}
|
||||||
resolution = new int[2];
|
resolution = new int[2];
|
||||||
resolution[0] = config.optInt("streamWidth");
|
resolution[0] = config.optInt("streamWidth");
|
||||||
resolution[1] = config.optInt("streamHeight");
|
resolution[1] = config.optInt("streamHeight");
|
||||||
online = onlineState == State.ONLINE;
|
online = onlineState == State.ONLINE;
|
||||||
LOG.trace("{} - status:{} {} {} {}", getName(), online, onlineState, Arrays.toString(resolution), getUrl());
|
LOG.trace("{} - status:{} {} {} {} {}", getName(), online, onlineState, Arrays.toString(resolution), getUrl(), id);
|
||||||
} else {
|
} else {
|
||||||
throw new IOException("Response was not successful: " + body);
|
throw new IOException("Response was not successful: " + body);
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,8 +50,8 @@ public class HlsStreamSourceProvider implements StreamSourceProvider {
|
||||||
src.width = playlist.getStreamInfo().getResolution().width;
|
src.width = playlist.getStreamInfo().getResolution().width;
|
||||||
src.height = playlist.getStreamInfo().getResolution().height;
|
src.height = playlist.getStreamInfo().getResolution().height;
|
||||||
} else {
|
} else {
|
||||||
src.width = Integer.MAX_VALUE;
|
src.width = StreamSource.UNKNOWN;
|
||||||
src.height = Integer.MAX_VALUE;
|
src.height = StreamSource.UNKNOWN;
|
||||||
}
|
}
|
||||||
String masterUrl = streamUrl;
|
String masterUrl = streamUrl;
|
||||||
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||||
|
|
|
@ -40,6 +40,7 @@ import com.squareup.moshi.Moshi;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.StringUtil;
|
import ctbrec.StringUtil;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
import okhttp3.Cookie;
|
import okhttp3.Cookie;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
@ -56,6 +57,7 @@ public class MyFreeCamsClient {
|
||||||
private static MyFreeCamsClient instance;
|
private static MyFreeCamsClient instance;
|
||||||
private MyFreeCams mfc;
|
private MyFreeCams mfc;
|
||||||
private WebSocket ws;
|
private WebSocket ws;
|
||||||
|
private Thread keepAlive;
|
||||||
private Moshi moshi;
|
private Moshi moshi;
|
||||||
private volatile boolean running = false;
|
private volatile boolean running = false;
|
||||||
|
|
||||||
|
@ -92,6 +94,7 @@ public class MyFreeCamsClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() throws IOException {
|
public void start() throws IOException {
|
||||||
|
requestLandingPage(); // to get some cookies
|
||||||
running = true;
|
running = true;
|
||||||
serverConfig = new ServerConfig(mfc);
|
serverConfig = new ServerConfig(mfc);
|
||||||
List<String> websocketServers = new ArrayList<>(serverConfig.wsServers.size());
|
List<String> websocketServers = new ArrayList<>(serverConfig.wsServers.size());
|
||||||
|
@ -133,6 +136,21 @@ public class MyFreeCamsClient {
|
||||||
watchDog.start();
|
watchDog.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void requestLandingPage() throws IOException {
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(MyFreeCams.baseUrl)
|
||||||
|
.header(ACCEPT, "*/*")
|
||||||
|
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(CONNECTION, KEEP_ALIVE)
|
||||||
|
.build();
|
||||||
|
try(Response resp = mfc.getHttpClient().execute(req)) {
|
||||||
|
if(!resp.isSuccessful()) {
|
||||||
|
throw new HttpException(resp.code(), resp.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
running = false;
|
running = false;
|
||||||
ws.close(1000, "Good Bye"); // terminate normally (1000)
|
ws.close(1000, "Good Bye"); // terminate normally (1000)
|
||||||
|
@ -454,6 +472,15 @@ public class MyFreeCamsClient {
|
||||||
model.update(state, getStreamUrl(state));
|
model.update(state, getStreamUrl(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocket webSocket, ByteString bytes) {
|
||||||
|
super.onMessage(webSocket, bytes);
|
||||||
|
LOG.debug("msgb: {}", bytes.hex());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return websocket;
|
||||||
|
}
|
||||||
|
|
||||||
private Message parseMessage(StringBuilder msgBuffer) throws UnsupportedEncodingException {
|
private Message parseMessage(StringBuilder msgBuffer) throws UnsupportedEncodingException {
|
||||||
int packetLengthBytes = 6;
|
int packetLengthBytes = 6;
|
||||||
if (msgBuffer.length() < packetLengthBytes) {
|
if (msgBuffer.length() < packetLengthBytes) {
|
||||||
|
@ -502,15 +529,6 @@ public class MyFreeCamsClient {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage(WebSocket webSocket, ByteString bytes) {
|
|
||||||
super.onMessage(webSocket, bytes);
|
|
||||||
LOG.debug("msgb: {}", bytes.hex());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return websocket;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean follow(int uid) {
|
protected boolean follow(int uid) {
|
||||||
if (ws != null) {
|
if (ws != null) {
|
||||||
return ws.send(ADDFRIENDREQ + " " + sessionId + " 0 " + uid + " 1\n");
|
return ws.send(ADDFRIENDREQ + " " + sessionId + " 0 " + uid + " 1\n");
|
||||||
|
@ -562,8 +580,11 @@ public class MyFreeCamsClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startKeepAlive(WebSocket ws) {
|
private void startKeepAlive(WebSocket ws) {
|
||||||
Thread keepAlive = new Thread(() -> {
|
if (keepAlive != null) {
|
||||||
while (running) {
|
keepAlive.interrupt();
|
||||||
|
}
|
||||||
|
keepAlive = new Thread(() -> {
|
||||||
|
while (running && !Thread.currentThread().isInterrupted()) {
|
||||||
try {
|
try {
|
||||||
if (!connecting) {
|
if (!connecting) {
|
||||||
LOG.trace("--> NULL to keep the connection alive");
|
LOG.trace("--> NULL to keep the connection alive");
|
||||||
|
@ -699,4 +720,18 @@ public class MyFreeCamsClient {
|
||||||
public Collection<SessionState> getSessionStates() {
|
public Collection<SessionState> getSessionStates() {
|
||||||
return Collections.unmodifiableCollection(sessionStates.asMap().values());
|
return Collections.unmodifiableCollection(sessionStates.asMap().values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void joinChannel(MyFreeCamsModel model) {
|
||||||
|
SessionState state = getSessionState(model);
|
||||||
|
int userChannel = 100000000 + state.getUid();
|
||||||
|
LOG.debug("Joining chat channel for model {}", model.getDisplayName());
|
||||||
|
try {
|
||||||
|
search(model.getName());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
ws.send(MessageTypes.ROOMDATA + " " + sessionId + " 0 1 0\n");
|
||||||
|
ws.send(MessageTypes.UEOPT + " " + sessionId + " 0 66 1 111111\n");
|
||||||
|
ws.send(MessageTypes.JOINCHAN + " " + sessionId + " 0 " + userChannel + " 9\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
@ -74,7 +74,7 @@ public class ServerConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isOnHtml5VideoServer(SessionState state) {
|
public boolean isOnHtml5VideoServer(SessionState state) {
|
||||||
int camserv = Objects.requireNonNull(Objects.requireNonNull(state.getU()).getCamserv());
|
int camserv = getCamServ(state);
|
||||||
return isOnObsServer(state)
|
return isOnObsServer(state)
|
||||||
|| h5Servers.containsKey(Integer.toString(camserv))
|
|| h5Servers.containsKey(Integer.toString(camserv))
|
||||||
|| (camserv >= 904 && camserv <= 915
|
|| (camserv >= 904 && camserv <= 915
|
||||||
|
@ -86,12 +86,17 @@ public class ServerConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isOnWzObsVideoServer(SessionState state) {
|
public boolean isOnWzObsVideoServer(SessionState state) {
|
||||||
int camserv = Objects.requireNonNull(Objects.requireNonNull(state.getU()).getCamserv());
|
int camserv = getCamServ(state);
|
||||||
return wzobsServers.containsKey(Integer.toString(camserv));
|
return wzobsServers.containsKey(Integer.toString(camserv));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isOnNgServer(SessionState state) {
|
public boolean isOnNgServer(SessionState state) {
|
||||||
int camserv = Objects.requireNonNull(Objects.requireNonNull(state.getU()).getCamserv());
|
int camserv = getCamServ(state);
|
||||||
return ngVideoServers.containsKey(Integer.toString(camserv));
|
return ngVideoServers.containsKey(Integer.toString(camserv));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int getCamServ(SessionState state) {
|
||||||
|
int camserv = Optional.ofNullable(state).map(SessionState::getU).map(User::getCamserv).orElse(-1);
|
||||||
|
return camserv;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@ -15,7 +16,6 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.io.HttpClient;
|
import ctbrec.io.HttpClient;
|
||||||
import ctbrec.io.HttpConstants;
|
|
||||||
import ctbrec.io.HttpException;
|
import ctbrec.io.HttpException;
|
||||||
import okhttp3.Cookie;
|
import okhttp3.Cookie;
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.HttpUrl;
|
||||||
|
@ -60,8 +60,10 @@ public class StreamateHttpClient extends HttpClient {
|
||||||
private void loadXsrfToken() {
|
private void loadXsrfToken() {
|
||||||
// do a first request to get cookies and stuff
|
// do a first request to get cookies and stuff
|
||||||
Request req = new Request.Builder() //
|
Request req = new Request.Builder() //
|
||||||
.url(Streamate.BASE_URL) //
|
.url(Streamate.BASE_URL + "/initialData.js") //
|
||||||
.header(HttpConstants.USER_AGENT, Config.getInstance().getSettings().httpUserAgent) //
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) //
|
||||||
|
.header(COOKIE, "smtid="+UUID.randomUUID().toString()+"; Xld_rct=1;") //
|
||||||
|
.header(REFERER, Streamate.BASE_URL)
|
||||||
.build();
|
.build();
|
||||||
try (Response resp = execute(req)) {
|
try (Response resp = execute(req)) {
|
||||||
if (resp.code() == 200) {
|
if (resp.code() == 200) {
|
||||||
|
|
|
@ -23,9 +23,19 @@ import okhttp3.Response;
|
||||||
|
|
||||||
public class Stripchat extends AbstractSite {
|
public class Stripchat extends AbstractSite {
|
||||||
|
|
||||||
public static final String BASE_URI = "https://stripchat.com";
|
public static String domain = "stripchat.com";
|
||||||
|
public static String baseUri = "https://stripchat.com";
|
||||||
private HttpClient httpClient;
|
private HttpClient httpClient;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() throws IOException {
|
||||||
|
boolean hamster = Config.getInstance().getSettings().stripchatUseXhamster;
|
||||||
|
if (hamster) {
|
||||||
|
domain = "xhamsterlive.com";
|
||||||
|
baseUri = "https://" + domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "Stripchat";
|
return "Stripchat";
|
||||||
|
@ -33,7 +43,7 @@ public class Stripchat extends AbstractSite {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getBaseUrl() {
|
public String getBaseUrl() {
|
||||||
return BASE_URI;
|
return baseUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -61,15 +71,15 @@ public class Stripchat extends AbstractSite {
|
||||||
throw new IOException("Account settings not available");
|
throw new IOException("Account settings not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
String username = Config.getInstance().getSettings().camsodaUsername;
|
String username = Config.getInstance().getSettings().stripchatPassword;
|
||||||
String url = BASE_URI + "/api/v1/user/" + username;
|
String url = baseUri + "/api/v1/user/" + username;
|
||||||
Request request = new Request.Builder().url(url).build();
|
Request request = new Request.Builder().url(url).build();
|
||||||
try(Response response = getHttpClient().execute(request)) {
|
try (Response response = getHttpClient().execute(request)) {
|
||||||
if(response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
JSONObject json = new JSONObject(response.body().string());
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
if(json.has("user")) {
|
if (json.has("user")) {
|
||||||
JSONObject user = json.getJSONObject("user");
|
JSONObject user = json.getJSONObject("user");
|
||||||
if(user.has("tokens")) {
|
if (user.has("tokens")) {
|
||||||
return (double) user.getInt("tokens");
|
return (double) user.getInt("tokens");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,11 +103,6 @@ public class Stripchat extends AbstractSite {
|
||||||
return httpClient;
|
return httpClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init() throws IOException {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
if (httpClient != null) {
|
if (httpClient != null) {
|
||||||
|
@ -122,7 +127,7 @@ public class Stripchat extends AbstractSite {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Model> search(String q) throws IOException, InterruptedException {
|
public List<Model> search(String q) throws IOException, InterruptedException {
|
||||||
String url = BASE_URI + "/api/front/v2/models/search?limit=20&query=" + URLEncoder.encode(q, "utf-8");
|
String url = baseUri + "/api/front/v2/models/search?limit=20&query=" + URLEncoder.encode(q, "utf-8");
|
||||||
Request req = new Request.Builder()
|
Request req = new Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
@ -162,7 +167,7 @@ public class Stripchat extends AbstractSite {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Model createModelFromUrl(String url) {
|
public Model createModelFromUrl(String url) {
|
||||||
Matcher m = Pattern.compile("https?://(?:.*?\\.)?stripchat.com/([^/]*?)/?").matcher(url);
|
Matcher m = Pattern.compile("https?://(?:.*?\\.)?(?:stripchat.com|xhamsterlive.com)/([^/]*?)/?").matcher(url);
|
||||||
if (m.matches()) {
|
if (m.matches()) {
|
||||||
String modelName = m.group(1);
|
String modelName = m.group(1);
|
||||||
return createModel(modelName);
|
return createModel(modelName);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import static ctbrec.io.HttpConstants.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -31,14 +32,20 @@ public class StripchatHttpClient extends HttpClient {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean login() throws IOException {
|
public boolean login() throws IOException {
|
||||||
if(loggedIn) {
|
if (loggedIn) {
|
||||||
|
if (csrfToken == null) {
|
||||||
|
loadCsrfToken();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// persisted cookies might let us log in
|
// persisted cookies might let us log in
|
||||||
if(checkLoginSuccess()) {
|
if (checkLoginSuccess()) {
|
||||||
loggedIn = true;
|
loggedIn = true;
|
||||||
LOG.debug("Logged in with cookies");
|
LOG.debug("Logged in with cookies");
|
||||||
|
if (csrfToken == null) {
|
||||||
|
loadCsrfToken();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +53,7 @@ public class StripchatHttpClient extends HttpClient {
|
||||||
loadCsrfToken();
|
loadCsrfToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
String url = Stripchat.BASE_URI + "/api/front/auth/login";
|
String url = Stripchat.baseUri + "/api/front/auth/login";
|
||||||
JSONObject requestParams = new JSONObject();
|
JSONObject requestParams = new JSONObject();
|
||||||
requestParams.put("loginOrEmail", Config.getInstance().getSettings().stripchatUsername);
|
requestParams.put("loginOrEmail", Config.getInstance().getSettings().stripchatUsername);
|
||||||
requestParams.put("password", Config.getInstance().getSettings().stripchatPassword);
|
requestParams.put("password", Config.getInstance().getSettings().stripchatPassword);
|
||||||
|
@ -59,8 +66,8 @@ public class StripchatHttpClient extends HttpClient {
|
||||||
.url(url)
|
.url(url)
|
||||||
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
.header(ORIGIN, Stripchat.BASE_URI)
|
.header(ORIGIN, Stripchat.baseUri)
|
||||||
.header(REFERER, Stripchat.BASE_URI)
|
.header(REFERER, Stripchat.baseUri)
|
||||||
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
||||||
.post(body)
|
.post(body)
|
||||||
.build();
|
.build();
|
||||||
|
@ -75,19 +82,20 @@ public class StripchatHttpClient extends HttpClient {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new HttpException(response.code(), response.message());
|
LOG.info("Auto-Login failed: {} {} {}", response.code(), response.message(), url);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadCsrfToken() throws IOException {
|
private void loadCsrfToken() throws IOException {
|
||||||
String url = Stripchat.BASE_URI + "/api/front/v2/config/data?requestPath=%2F&timezoneOffset=0";
|
String url = Stripchat.baseUri + "/api/front/v2/config/data?requestPath=%2F&timezoneOffset=0";
|
||||||
Request request = new Request.Builder()
|
Request request = new Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
.header(ORIGIN, Stripchat.BASE_URI)
|
.header(ORIGIN, Stripchat.baseUri)
|
||||||
.header(REFERER, Stripchat.BASE_URI)
|
.header(REFERER, Stripchat.baseUri)
|
||||||
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
||||||
.build();
|
.build();
|
||||||
try (Response response = execute(request)) {
|
try (Response response = execute(request)) {
|
||||||
|
@ -107,11 +115,48 @@ public class StripchatHttpClient extends HttpClient {
|
||||||
* check, if the login worked
|
* check, if the login worked
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
public boolean checkLoginSuccess() {
|
public boolean checkLoginSuccess() throws IOException {
|
||||||
return userId > 0;
|
long userId = getUserId();
|
||||||
|
String url = Stripchat.baseUri + "/api/front/users/" + userId + "/favorites";
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ORIGIN, Stripchat.baseUri)
|
||||||
|
.header(REFERER, Stripchat.baseUri + "/favorites")
|
||||||
|
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
||||||
|
.build();
|
||||||
|
try (Response response = execute(request)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.info("Login check returned unsuccessful: {}", e.getLocalizedMessage());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getUserId() {
|
public long getUserId() throws JSONException, IOException {
|
||||||
|
if (userId == 0) {
|
||||||
|
String url = Stripchat.baseUri + "/api/front/users/username/" + Config.getInstance().getSettings().stripchatUsername;
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ORIGIN, Stripchat.baseUri)
|
||||||
|
.header(REFERER, Stripchat.baseUri)
|
||||||
|
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
||||||
|
.build();
|
||||||
|
try (Response response = execute(request)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
JSONObject resp = new JSONObject(response.body().string());
|
||||||
|
JSONObject user = resp.getJSONObject("user");
|
||||||
|
userId = user.optLong("id");
|
||||||
|
} else {
|
||||||
|
throw new HttpException(url, response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return userId;
|
return userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,9 +95,8 @@ public class StripchatModel extends AbstractModel {
|
||||||
best.width = broadcastSettings.optInt("width");
|
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);
|
sources.add(best);
|
||||||
Object resolutionObject = broadcastSettings.get("resolutions");
|
JSONObject resolutions = broadcastSettings.optJSONObject("resolutions");
|
||||||
if (resolutionObject instanceof JSONObject) {
|
if (resolutions instanceof JSONObject) {
|
||||||
JSONObject resolutions = (JSONObject) resolutionObject;
|
|
||||||
JSONArray heights = resolutions.names();
|
JSONArray heights = resolutions.names();
|
||||||
for (int i = 0; i < heights.length(); i++) {
|
for (int i = 0; i < heights.length(); i++) {
|
||||||
String h = heights.getString(i);
|
String h = heights.getString(i);
|
||||||
|
@ -145,12 +144,13 @@ public class StripchatModel extends AbstractModel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean follow() throws IOException {
|
public boolean follow() throws IOException {
|
||||||
|
getSite().getHttpClient().login();
|
||||||
JSONObject modelInfo = loadModelInfo();
|
JSONObject modelInfo = loadModelInfo();
|
||||||
JSONObject user = modelInfo.getJSONObject("user");
|
JSONObject user = modelInfo.getJSONObject("user");
|
||||||
long modelId = user.optLong("id");
|
long modelId = user.optLong("id");
|
||||||
|
|
||||||
StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient();
|
StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient();
|
||||||
String url = Stripchat.BASE_URI + "/api/front/users/" + client.getUserId() + "/favorites/" + modelId;
|
String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites/" + modelId;
|
||||||
JSONObject requestParams = new JSONObject();
|
JSONObject requestParams = new JSONObject();
|
||||||
requestParams.put("csrfToken", client.getCsrfToken());
|
requestParams.put("csrfToken", client.getCsrfToken());
|
||||||
requestParams.put("csrfTimestamp", client.getCsrfTimestamp());
|
requestParams.put("csrfTimestamp", client.getCsrfTimestamp());
|
||||||
|
@ -160,8 +160,8 @@ public class StripchatModel extends AbstractModel {
|
||||||
.url(url)
|
.url(url)
|
||||||
.header(ACCEPT, "*/*")
|
.header(ACCEPT, "*/*")
|
||||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
.header(ORIGIN, Stripchat.BASE_URI)
|
.header(ORIGIN, Stripchat.baseUri)
|
||||||
.header(REFERER, Stripchat.BASE_URI + '/' + getName())
|
.header(REFERER, Stripchat.baseUri + '/' + getName())
|
||||||
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
||||||
.put(body)
|
.put(body)
|
||||||
.build();
|
.build();
|
||||||
|
@ -176,6 +176,7 @@ public class StripchatModel extends AbstractModel {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean unfollow() throws IOException {
|
public boolean unfollow() throws IOException {
|
||||||
|
getSite().getHttpClient().login();
|
||||||
JSONObject modelInfo = loadModelInfo();
|
JSONObject modelInfo = loadModelInfo();
|
||||||
JSONObject user = modelInfo.getJSONObject("user");
|
JSONObject user = modelInfo.getJSONObject("user");
|
||||||
long modelId = user.optLong("id");
|
long modelId = user.optLong("id");
|
||||||
|
@ -183,7 +184,7 @@ public class StripchatModel extends AbstractModel {
|
||||||
favoriteIds.put(modelId);
|
favoriteIds.put(modelId);
|
||||||
|
|
||||||
StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient();
|
StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient();
|
||||||
String url = Stripchat.BASE_URI + "/api/front/users/" + client.getUserId() + "/favorites";
|
String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites";
|
||||||
JSONObject requestParams = new JSONObject();
|
JSONObject requestParams = new JSONObject();
|
||||||
requestParams.put("favoriteIds", favoriteIds);
|
requestParams.put("favoriteIds", favoriteIds);
|
||||||
requestParams.put("csrfToken", client.getCsrfToken());
|
requestParams.put("csrfToken", client.getCsrfToken());
|
||||||
|
@ -194,8 +195,8 @@ public class StripchatModel extends AbstractModel {
|
||||||
.url(url)
|
.url(url)
|
||||||
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
.header(ORIGIN, Stripchat.BASE_URI)
|
.header(ORIGIN, Stripchat.baseUri)
|
||||||
.header(REFERER, Stripchat.BASE_URI)
|
.header(REFERER, Stripchat.baseUri)
|
||||||
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
|
||||||
.delete(body)
|
.delete(body)
|
||||||
.build();
|
.build();
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package ctbrec;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class ReflectionUtil {
|
||||||
|
|
||||||
|
private ReflectionUtil () {}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> T call(Object target, String methodName, Object...args) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
|
||||||
|
List<Class<?>> argTypes = Arrays.stream(args).map(arg -> arg.getClass()).collect(Collectors.toList());
|
||||||
|
Class<?>[] argTypeArray = argTypes.toArray(new Class[0]);
|
||||||
|
Method method = target.getClass().getDeclaredMethod(methodName, argTypeArray);
|
||||||
|
method.setAccessible(true);
|
||||||
|
return (T) method.invoke(target, args);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package ctbrec.sites.mfc;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import ctbrec.ReflectionUtil;
|
||||||
|
|
||||||
|
public class MyFreeCamsClientTest {
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMessageParsing() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
|
||||||
|
MyFreeCamsClient client = MyFreeCamsClient.getInstance();
|
||||||
|
StringBuilder input = new StringBuilder("00001933 0 439895060 1 0 00001933 0 439895060 1 0 0000208 0 439895060 0 112 00038...");
|
||||||
|
Message msg = ReflectionUtil.call(client, "parseMessage", input);
|
||||||
|
assertEquals(33, msg.getType());
|
||||||
|
assertEquals(0, msg.getSender());
|
||||||
|
assertEquals(439895060, msg.getReceiver());
|
||||||
|
assertEquals(1, msg.getArg1());
|
||||||
|
assertEquals(0, msg.getArg2());
|
||||||
|
assertTrue(msg.getMessage().isBlank());
|
||||||
|
|
||||||
|
input = new StringBuilder("00207820 439461784 439895060 12 507930 %7B%22lv%22%3A4%2C%22nm%22%3A%22Nivea%22%2C%22pid%22%3A1%2C%22sid%22%3A439461784%2C%22uid%22%3A507930%2C%22vs%22%3A12%2C%22u%22%3A%7B%22age%22%3A33%2C%22avatar%22%3A1%2C%22blurb%22%3A%22I%20love%20when%20my%20nose%20touch%20your%20belly%20when%20I%20do%20you%20a%20blowjob!%20When%20I%20look%20into%20your%20horny%20eyes%20when%22%2C%22camserv%22%3A1367%2C%22chat_color%22%3A%22FF0000%22%2C%22chat_font%22%3A0%2C%22chat_opt%22%3A1%2C%22ethnic%22%3A%22Caucasian%22%2C%22photos%22%3A74%2C%22profile%22%3A1%2C%22status%22%3A%22%22%7D%2C%22m%22%3A%7B%22camscore%22%3A7505.700%2C%22continent%22%3A%22EU%22%2C%22flags%22%3A605224%2C%22hidecs%22%3Atrue%2C%22kbit%22%3A0%2C%22lastnews%22%3A0%2C%22mg%22%3A0%2C%22missmfc%22%3A2%2C%22new_model%22%3A0%2C%22rank%22%3A0%2C%22rc%22%3A20%2C%22sfw%22%3A0%2C%22topic%22%3A%22hi%253A)%255Bnone%255D-Topless%252C500-snap4life%252C50-spanks%252C180-flash%252C700-10%2520mins%2520of%2520Nora%2520fun%252C666-shot%252C27%252C270%2520%253C3%22%7D%2C%22x%22%3A%7B%22fcext%22%3A%7B%22sm%22%3A%22%22%2C%22sfw%22%3A0%7D%2C%22share%22%3A%7B%22follows%22%3A11%2C%22albums%22%3A0%2C%22clubs%22%3A0%2C%22tm_album%22%3A0%2C%22collections%22%3A0%2C%22stores%22%3A0%2C%22goals%22%3A0%2C%22polls%22%3A0%2C%22things%22%3A0%2C%22recent_album_tm%22%3A0%2C%22recent_club_tm%22%3A0%2C%22recent_collection_tm%22%3A0%2C%22recent_goal_tm%22%3A0%2C%22recent_item_tm%22%3A0%2C%22recent_poll_tm%22%3A0%2C%22recent_story_tm%22%3A0%2C%22recent_album_thumb%22%3A%22%22%2C%22recent_club_thumb%22%3A%22%22%2C%22recent_collection_thumb%22%3A%22%22%2C%22recent_goal_thumb%22%3A%22%22%2C%22recent_item_thumb%22%3A%22%22%2C%22recent_poll_thumb%22%3A%22%22%2C%22recent_story_thumb%22%3A%22%22%2C%22recent_album_title%22%3A%22%22%2C%22recent_club_title%22%3A%22%22%2C%22recent_collection_title%22%3A%22%22%2C%22recent_goal_title%22%3A%22%22%2C%22recent_item_title%22%3A%22%22%2C%22recent_poll_title%22%3A%22%22%2C%22recent_story_title%22%3A%22%22%2C%22recent_album_slug%22%3A%22%22%2C%22recent_collection_slug%22%3A%22%22%2C%22tipmenus%22%3A0%7D%7D%7D");
|
||||||
|
msg = ReflectionUtil.call(client, "parseMessage", input);
|
||||||
|
assertEquals(20, msg.getType());
|
||||||
|
assertEquals(439461784, msg.getSender());
|
||||||
|
assertEquals(439895060, msg.getReceiver());
|
||||||
|
assertEquals(12, msg.getArg1());
|
||||||
|
assertEquals(507930, msg.getArg2());
|
||||||
|
assertEquals(JSONObject.class, new JSONObject(msg.getMessage()).getClass()); // make sure we have a parsable JSON message
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<version>3.8.1</version>
|
<version>3.8.6</version>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>../common</module>
|
<module>../common</module>
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<version.javafx>14-ea+4</version.javafx>
|
<version.javafx>14.0.2.1</version.javafx>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>3.8.1</version>
|
<version>3.8.6</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|
|
@ -106,7 +106,7 @@ public class HttpServer {
|
||||||
safeLogin(site);
|
safeLogin(site);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onlineMonitor = new OnlineMonitor(recorder);
|
onlineMonitor = new OnlineMonitor(recorder, config);
|
||||||
onlineMonitor.start();
|
onlineMonitor.start();
|
||||||
startHttpServer();
|
startHttpServer();
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,17 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
||||||
response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
|
response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
|
||||||
resp.getWriter().write(response);
|
resp.getWriter().write(response);
|
||||||
break;
|
break;
|
||||||
|
case "stopAt":
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
recorder.stopRecordingAt(request.model);
|
||||||
|
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
|
||||||
|
LOG.error("Couldn't stop recording for model {}", request.model, e);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
|
||||||
|
resp.getWriter().write(response);
|
||||||
|
break;
|
||||||
case "list":
|
case "list":
|
||||||
resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of models\", \"models\": [");
|
resp.getWriter().write("{\"status\": \"success\", \"msg\": \"List of models\", \"models\": [");
|
||||||
JsonAdapter<Model> modelAdapter = new ModelJsonAdapter();
|
JsonAdapter<Model> modelAdapter = new ModelJsonAdapter();
|
||||||
|
|
|
@ -58,6 +58,10 @@ th a:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkmark-green {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
color: #dc4444;
|
||||||
|
}
|
|
@ -96,19 +96,22 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th><a href="#" data-bind="orderable: {collection: 'models', field: 'ko_name'}">Model</a></th>
|
<th><a href="#" data-bind="orderable: {collection: 'models', field: 'ko_name'}">Model</a></th>
|
||||||
<th><a href="#" data-bind="orderable: {collection: 'models', field: 'ko_suspended'}">Paused</a></th>
|
|
||||||
<th><a href="#" data-bind="orderable: {collection: 'models', field: 'ko_online'}">Online</a></th>
|
<th><a href="#" data-bind="orderable: {collection: 'models', field: 'ko_online'}">Online</a></th>
|
||||||
<th><a href="#" data-bind="orderable: {collection: 'models', field: 'ko_recording'}">Recording</a></th>
|
<th><a href="#" data-bind="orderable: {collection: 'models', field: 'ko_recording'}">Recording</a></th>
|
||||||
<th>Actions</th>
|
<th></th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody data-bind="foreach: models">
|
<tbody data-bind="foreach: models">
|
||||||
<tr>
|
<tr>
|
||||||
<td><a data-bind="attr: { href: ko_url, title: ko_name }, text: ko_name"></a></td>
|
<td><a data-bind="attr: { href: ko_url, title: ko_name }, text: ko_name"></a></td>
|
||||||
<td><input type="checkbox" data-bind="checked: ko_suspended" /></td>
|
<td><span data-bind="checked: ko_online, class: ko_online() ? `fa fa-check-square checkmark-green` : ``" style="font-size: 2em"></span></td>
|
||||||
<td><input type="checkbox" disabled data-bind="checked: ko_online" /></td>
|
<td><span data-bind="checked: ko_recording, class: ko_recording() ? `fa fa-circle red` : ``" style="font-size: 2em"></span></td>
|
||||||
<td><input type="checkbox" disabled data-bind="checked: ko_recording" /></td>
|
<td>
|
||||||
<td><button class="btn btn-secondary fa fa-minus-circle" title="Stop recording" data-bind="click: ctbrec.stop"></button></td>
|
<button class="btn btn-secondary fa fa-play" title="Resume recording" data-bind="click: ctbrec.resume, visible: ko_suspended"></button>
|
||||||
|
<button class="btn btn-secondary fa fa-pause" title="Pause recording" data-bind="click: ctbrec.suspend, hidden: ko_suspended"></button>
|
||||||
|
</td>
|
||||||
|
<td><button class="btn btn-secondary fa fa-trash" title="Remove model" data-bind="click: ctbrec.stop"></button></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -69,6 +69,7 @@ function syncModels(models) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
model.ko_recording = ko.observable(model.online && !model.suspended);
|
model.ko_recording = ko.observable(model.online && !model.suspended);
|
||||||
|
//model.ko_recording_class = ko.observable( (model.online && !model.suspended) ? 'fa fa-circle red' : '' );
|
||||||
model.ko_suspended = ko.observable(model.suspended);
|
model.ko_suspended = ko.observable(model.suspended);
|
||||||
model.swallowEvents = false;
|
model.swallowEvents = false;
|
||||||
model.ko_suspended.subscribe(function(checked) {
|
model.ko_suspended.subscribe(function(checked) {
|
||||||
|
@ -102,6 +103,7 @@ function syncModels(models) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.ko_online(onlineState);
|
m.ko_online(onlineState);
|
||||||
|
//m.ko_recording_class( (model.online && !model.suspended) ? 'fa fa-circle red' : '');
|
||||||
m.swallowEvents = true;
|
m.swallowEvents = true;
|
||||||
m.ko_suspended(model.suspended);
|
m.ko_suspended(model.suspended);
|
||||||
m.swallowEvents = false;
|
m.swallowEvents = false;
|
||||||
|
|
|
@ -195,7 +195,6 @@ function updateDiskSpace() {
|
||||||
throughput.bytes(data.throughput);
|
throughput.bytes(data.throughput);
|
||||||
throughput.timeframe(data.throughputTimeframe);
|
throughput.timeframe(data.throughputTimeframe);
|
||||||
let bytesPerSecond = data.throughput / data.throughputTimeframe;
|
let bytesPerSecond = data.throughput / data.throughputTimeframe;
|
||||||
console.log(data.throughput, data.throughputTimeframe, bytesPerSecond, calculateSize(bytesPerSecond) + '/s');
|
|
||||||
throughput.text(calculateSize(bytesPerSecond) + '/s');
|
throughput.text(calculateSize(bytesPerSecond) + '/s');
|
||||||
} else {
|
} else {
|
||||||
if (console)
|
if (console)
|
||||||
|
|
Loading…
Reference in New Issue