Merge branch 'dev'
This commit is contained in:
commit
d84771d2f9
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,3 +1,17 @@
|
||||||
|
1.16.0
|
||||||
|
========================
|
||||||
|
* Thumbnails can show a live preview. Can be switched on in the settings.
|
||||||
|
* Live preview is experimental for now, because I noticed some funky behavior
|
||||||
|
of the the internal media player. You can use it on your own risk.
|
||||||
|
* Added Streamate (metcams, xhamstercams, pornhublive)
|
||||||
|
* Maximum resolution can be an arbitrary value now
|
||||||
|
* Added setting for minimal recording length. Recordings, which are shorter
|
||||||
|
than this value, get deleted automatically.
|
||||||
|
* Double-click in Recording tab starts the player
|
||||||
|
* Fix: BongaCams friends tab not working
|
||||||
|
* Fix: BongaCams search fails with JSON exception
|
||||||
|
* Fix: In some cases MFC models got confused
|
||||||
|
|
||||||
1.15.0
|
1.15.0
|
||||||
========================
|
========================
|
||||||
* Fix: BongaCams overview didn't work anymore
|
* Fix: BongaCams overview didn't work anymore
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>1.15.0</version>
|
<version>1.16.0</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ import ctbrec.sites.cam4.Cam4;
|
||||||
import ctbrec.sites.camsoda.Camsoda;
|
import ctbrec.sites.camsoda.Camsoda;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
import ctbrec.sites.chaturbate.Chaturbate;
|
||||||
import ctbrec.sites.mfc.MyFreeCams;
|
import ctbrec.sites.mfc.MyFreeCams;
|
||||||
|
import ctbrec.sites.streamate.Streamate;
|
||||||
import ctbrec.ui.settings.SettingsTab;
|
import ctbrec.ui.settings.SettingsTab;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.application.HostServices;
|
import javafx.application.HostServices;
|
||||||
|
@ -76,6 +77,7 @@ public class CamrecApplication extends Application {
|
||||||
sites.add(new Camsoda());
|
sites.add(new Camsoda());
|
||||||
sites.add(new Chaturbate());
|
sites.add(new Chaturbate());
|
||||||
sites.add(new MyFreeCams());
|
sites.add(new MyFreeCams());
|
||||||
|
sites.add(new Streamate());
|
||||||
loadConfig();
|
loadConfig();
|
||||||
registerAlertSystem();
|
registerAlertSystem();
|
||||||
createHttpClient();
|
createHttpClient();
|
||||||
|
@ -176,9 +178,13 @@ public class CamrecApplication extends Application {
|
||||||
try {
|
try {
|
||||||
Config.getInstance().save();
|
Config.getInstance().save();
|
||||||
LOG.info("Shutdown complete. Goodbye!");
|
LOG.info("Shutdown complete. Goodbye!");
|
||||||
Platform.exit();
|
Platform.runLater(() -> {
|
||||||
// This is needed, because OkHttp?! seems to block the shutdown with its writer threads. They are not daemon threads :(
|
primaryStage.close();
|
||||||
System.exit(0);
|
shutdownInfo.close();
|
||||||
|
Platform.exit();
|
||||||
|
// This is needed, because OkHttp?! seems to block the shutdown with its writer threads. They are not daemon threads :(
|
||||||
|
System.exit(0);
|
||||||
|
});
|
||||||
} catch (IOException e1) {
|
} catch (IOException e1) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||||
|
|
|
@ -1,39 +1,23 @@
|
||||||
package ctbrec.ui;
|
package ctbrec.ui;
|
||||||
|
|
||||||
import java.io.InterruptedIOException;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.ui.controls.StreamPreview;
|
||||||
import ctbrec.io.HttpException;
|
|
||||||
import ctbrec.recorder.download.StreamSource;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Point2D;
|
import javafx.geometry.Point2D;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.ProgressIndicator;
|
|
||||||
import javafx.scene.control.TableColumn;
|
import javafx.scene.control.TableColumn;
|
||||||
import javafx.scene.control.TableRow;
|
import javafx.scene.control.TableRow;
|
||||||
import javafx.scene.control.TableView;
|
import javafx.scene.control.TableView;
|
||||||
import javafx.scene.image.Image;
|
|
||||||
import javafx.scene.image.ImageView;
|
|
||||||
import javafx.scene.input.MouseButton;
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.Region;
|
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.media.Media;
|
|
||||||
import javafx.scene.media.MediaPlayer;
|
|
||||||
import javafx.scene.media.MediaView;
|
|
||||||
import javafx.stage.Popup;
|
import javafx.stage.Popup;
|
||||||
|
|
||||||
public class PreviewPopupHandler implements EventHandler<MouseEvent> {
|
public class PreviewPopupHandler implements EventHandler<MouseEvent> {
|
||||||
|
@ -44,53 +28,24 @@ public class PreviewPopupHandler implements EventHandler<MouseEvent> {
|
||||||
private long timeForPopupClose = 400;
|
private long timeForPopupClose = 400;
|
||||||
private Popup popup = new Popup();
|
private Popup popup = new Popup();
|
||||||
private Node parent;
|
private Node parent;
|
||||||
private ImageView preview = new ImageView();
|
private StreamPreview streamPreview;
|
||||||
private MediaView videoPreview;
|
|
||||||
private MediaPlayer videoPlayer;
|
|
||||||
private Media video;
|
|
||||||
private JavaFxModel model;
|
private JavaFxModel model;
|
||||||
private volatile long openCountdown = -1;
|
private volatile long openCountdown = -1;
|
||||||
private volatile long closeCountdown = -1;
|
private volatile long closeCountdown = -1;
|
||||||
private volatile long lastModelChange = -1;
|
private volatile long lastModelChange = -1;
|
||||||
private volatile boolean changeModel = false;
|
private volatile boolean changeModel = false;
|
||||||
private ExecutorService executor = Executors.newSingleThreadExecutor();
|
|
||||||
private Future<?> future;
|
|
||||||
private ProgressIndicator progressIndicator;
|
|
||||||
private StackPane pane;
|
|
||||||
|
|
||||||
public PreviewPopupHandler(Node parent) {
|
public PreviewPopupHandler(Node parent) {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
|
|
||||||
videoPreview = new MediaView();
|
streamPreview = new StreamPreview();
|
||||||
videoPreview.setFitWidth(Config.getInstance().getSettings().thumbWidth);
|
streamPreview.setStyle("-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-base;"+
|
||||||
videoPreview.setFitHeight(videoPreview.getFitWidth() * 9 / 16);
|
|
||||||
videoPreview.setPreserveRatio(true);
|
|
||||||
StackPane.setMargin(videoPreview, new Insets(5));
|
|
||||||
|
|
||||||
preview.setFitWidth(Config.getInstance().getSettings().thumbWidth);
|
|
||||||
preview.setPreserveRatio(true);
|
|
||||||
preview.setSmooth(true);
|
|
||||||
preview.setStyle("-fx-background-radius: 10px, 10px, 10px, 10px;");
|
|
||||||
preview.visibleProperty().bind(videoPreview.visibleProperty().not());
|
|
||||||
StackPane.setMargin(preview, new Insets(5));
|
|
||||||
|
|
||||||
progressIndicator = new ProgressIndicator();
|
|
||||||
progressIndicator.setVisible(false);
|
|
||||||
progressIndicator.prefWidthProperty().bind(videoPreview.fitWidthProperty());
|
|
||||||
|
|
||||||
Region veil = new Region();
|
|
||||||
veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.8)");
|
|
||||||
veil.visibleProperty().bind(progressIndicator.visibleProperty());
|
|
||||||
StackPane.setMargin(veil, new Insets(5));
|
|
||||||
|
|
||||||
pane = new StackPane();
|
|
||||||
pane.getChildren().addAll(preview, videoPreview, veil, progressIndicator);
|
|
||||||
pane.setStyle("-fx-background-color: -fx-outer-border, -fx-inner-border, -fx-base;"+
|
|
||||||
"-fx-background-insets: 0 0 -1 0, 0, 1, 2;" +
|
"-fx-background-insets: 0 0 -1 0, 0, 1, 2;" +
|
||||||
"-fx-background-radius: 10px, 10px, 10px, 10px;" +
|
"-fx-background-radius: 10px, 10px, 10px, 10px;" +
|
||||||
"-fx-padding: 1;" +
|
"-fx-padding: 1;" +
|
||||||
"-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.8), 20, 0, 0, 0);");
|
"-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.8), 20, 0, 0, 0);");
|
||||||
popup.getContent().add(pane);
|
popup.getContent().add(streamPreview);
|
||||||
|
StackPane.setMargin(streamPreview, new Insets(5));
|
||||||
|
|
||||||
createTimerThread();
|
createTimerThread();
|
||||||
}
|
}
|
||||||
|
@ -121,8 +76,7 @@ public class PreviewPopupHandler implements EventHandler<MouseEvent> {
|
||||||
if(modelChanged) {
|
if(modelChanged) {
|
||||||
lastModelChange = System.currentTimeMillis();
|
lastModelChange = System.currentTimeMillis();
|
||||||
changeModel = true;
|
changeModel = true;
|
||||||
future.cancel(true);
|
streamPreview.stop();
|
||||||
progressIndicator.setVisible(true);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
openCountdown = timeForPopupOpen;
|
openCountdown = timeForPopupOpen;
|
||||||
|
@ -173,121 +127,19 @@ public class PreviewPopupHandler implements EventHandler<MouseEvent> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startStream(JavaFxModel model) {
|
private void startStream(JavaFxModel model) {
|
||||||
if(future != null && !future.isDone()) {
|
|
||||||
future.cancel(true);
|
|
||||||
}
|
|
||||||
future = executor.submit(() -> {
|
|
||||||
try {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
progressIndicator.setVisible(true);
|
|
||||||
popup.show(parent.getScene().getWindow());
|
|
||||||
});
|
|
||||||
List<StreamSource> sources = model.getStreamSources();
|
|
||||||
Collections.sort(sources);
|
|
||||||
StreamSource best = sources.get(0);
|
|
||||||
checkInterrupt();
|
|
||||||
LOG.debug("Preview url for {} is {}", model.getName(), best.getMediaPlaylistUrl());
|
|
||||||
video = new Media(best.getMediaPlaylistUrl());
|
|
||||||
if(videoPlayer != null) {
|
|
||||||
videoPlayer.dispose();
|
|
||||||
}
|
|
||||||
videoPlayer = new MediaPlayer(video);
|
|
||||||
videoPlayer.setMute(true);
|
|
||||||
checkInterrupt();
|
|
||||||
videoPlayer.setOnReady(() -> {
|
|
||||||
if(!future.isCancelled()) {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
double aspect = (double)video.getWidth() / video.getHeight();
|
|
||||||
double w = Config.getInstance().getSettings().thumbWidth;
|
|
||||||
double h = w / aspect;
|
|
||||||
resize(w, h);
|
|
||||||
progressIndicator.setVisible(false);
|
|
||||||
videoPreview.setVisible(true);
|
|
||||||
videoPreview.setMediaPlayer(videoPlayer);
|
|
||||||
videoPlayer.play();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
videoPlayer.setOnError(() -> onError(videoPlayer));
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
if(e.getMessage().equals("Stream url unknown")) {
|
|
||||||
// fine hls url for mfc not known yet
|
|
||||||
} else {
|
|
||||||
LOG.warn("Couldn't start preview video: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
showTestImage();
|
|
||||||
} catch (HttpException e) {
|
|
||||||
if(e.getResponseCode() != 404) {
|
|
||||||
LOG.warn("Couldn't start preview video: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
showTestImage();
|
|
||||||
} catch (InterruptedException | InterruptedIOException e) {
|
|
||||||
// future has been canceled, that's fine
|
|
||||||
} catch (ExecutionException e) {
|
|
||||||
if(e.getCause() instanceof InterruptedException || e.getCause() instanceof InterruptedIOException) {
|
|
||||||
// future has been canceled, that's fine
|
|
||||||
} else {
|
|
||||||
LOG.warn("Couldn't start preview video: {}", e.getMessage());
|
|
||||||
showTestImage();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.warn("Couldn't start preview video: {}", e.getMessage());
|
|
||||||
showTestImage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onError(MediaPlayer videoPlayer) {
|
|
||||||
LOG.error("Error while starting preview stream", videoPlayer.getError());
|
|
||||||
if(videoPlayer.getError().getCause() != null) {
|
|
||||||
LOG.error("Error while starting preview stream root cause:", videoPlayer.getError().getCause());
|
|
||||||
}
|
|
||||||
videoPlayer.dispose();
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
showTestImage();
|
streamPreview.startStream(model);
|
||||||
|
popup.show(parent.getScene().getWindow());
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
private void resize(double w, double h) {
|
|
||||||
preview.setFitWidth(w);
|
|
||||||
preview.setFitHeight(h);
|
|
||||||
videoPreview.setFitWidth(w);
|
|
||||||
videoPreview.setFitHeight(h);
|
|
||||||
pane.setPrefSize(w, h);
|
|
||||||
popup.setWidth(w);
|
|
||||||
popup.setHeight(h);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkInterrupt() throws InterruptedException {
|
|
||||||
if(Thread.interrupted()) {
|
|
||||||
throw new InterruptedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showTestImage() {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
videoPreview.setVisible(false);
|
|
||||||
Image img = new Image(getClass().getResource("/image_not_found.png").toString(), true);
|
|
||||||
preview.setImage(img);
|
|
||||||
double aspect = img.getWidth() / img.getHeight();
|
|
||||||
double w = Config.getInstance().getSettings().thumbWidth;
|
|
||||||
double h = w / aspect;
|
|
||||||
resize(w, h);
|
|
||||||
progressIndicator.setVisible(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hidePopup() {
|
private void hidePopup() {
|
||||||
if(future != null && !future.isDone()) {
|
|
||||||
future.cancel(true);
|
|
||||||
}
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
popup.setX(-1000);
|
popup.setX(-1000);
|
||||||
popup.setY(-1000);
|
popup.setY(-1000);
|
||||||
popup.hide();
|
popup.hide();
|
||||||
if(videoPlayer != null) {
|
streamPreview.stop();
|
||||||
videoPlayer.dispose();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ import javafx.scene.input.ClipboardContent;
|
||||||
import javafx.scene.input.ContextMenuEvent;
|
import javafx.scene.input.ContextMenuEvent;
|
||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.input.KeyEvent;
|
import javafx.scene.input.KeyEvent;
|
||||||
|
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;
|
||||||
|
@ -117,6 +118,9 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
preview.setCellValueFactory(cdf -> new SimpleStringProperty(" ▶ "));
|
preview.setCellValueFactory(cdf -> new SimpleStringProperty(" ▶ "));
|
||||||
preview.setEditable(false);
|
preview.setEditable(false);
|
||||||
preview.setId("preview");
|
preview.setId("preview");
|
||||||
|
if(!Config.getInstance().getSettings().livePreviews) {
|
||||||
|
preview.setVisible(false);
|
||||||
|
}
|
||||||
TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
|
TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
|
||||||
name.setPrefWidth(200);
|
name.setPrefWidth(200);
|
||||||
name.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("displayName"));
|
name.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("displayName"));
|
||||||
|
@ -149,6 +153,14 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
||||||
}
|
}
|
||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
|
table.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||||
|
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
|
||||||
|
JavaFxModel model = table.getSelectionModel().getSelectedItem();
|
||||||
|
if(model != null) {
|
||||||
|
new PlayAction(table, model).execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
|
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
|
||||||
if (popup != null) {
|
if (popup != null) {
|
||||||
popup.hide();
|
popup.hide();
|
||||||
|
|
|
@ -6,11 +6,13 @@ import ctbrec.sites.cam4.Cam4;
|
||||||
import ctbrec.sites.camsoda.Camsoda;
|
import ctbrec.sites.camsoda.Camsoda;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
import ctbrec.sites.chaturbate.Chaturbate;
|
||||||
import ctbrec.sites.mfc.MyFreeCams;
|
import ctbrec.sites.mfc.MyFreeCams;
|
||||||
|
import ctbrec.sites.streamate.Streamate;
|
||||||
import ctbrec.ui.sites.bonga.BongaCamsSiteUi;
|
import ctbrec.ui.sites.bonga.BongaCamsSiteUi;
|
||||||
import ctbrec.ui.sites.cam4.Cam4SiteUi;
|
import ctbrec.ui.sites.cam4.Cam4SiteUi;
|
||||||
import ctbrec.ui.sites.camsoda.CamsodaSiteUi;
|
import ctbrec.ui.sites.camsoda.CamsodaSiteUi;
|
||||||
import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi;
|
import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi;
|
||||||
import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi;
|
import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi;
|
||||||
|
import ctbrec.ui.sites.streamate.StreamateSiteUi;
|
||||||
|
|
||||||
public class SiteUiFactory {
|
public class SiteUiFactory {
|
||||||
|
|
||||||
|
@ -19,6 +21,7 @@ public class SiteUiFactory {
|
||||||
private static CamsodaSiteUi camsodaSiteUi;
|
private static CamsodaSiteUi camsodaSiteUi;
|
||||||
private static ChaturbateSiteUi ctbSiteUi;
|
private static ChaturbateSiteUi ctbSiteUi;
|
||||||
private static MyFreeCamsSiteUi mfcSiteUi;
|
private static MyFreeCamsSiteUi mfcSiteUi;
|
||||||
|
private static StreamateSiteUi streamateSiteUi;
|
||||||
|
|
||||||
public static synchronized SiteUI getUi(Site site) {
|
public static synchronized SiteUI getUi(Site site) {
|
||||||
if (site instanceof BongaCams) {
|
if (site instanceof BongaCams) {
|
||||||
|
@ -46,6 +49,11 @@ public class SiteUiFactory {
|
||||||
mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site);
|
mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site);
|
||||||
}
|
}
|
||||||
return mfcSiteUi;
|
return mfcSiteUi;
|
||||||
|
} else if (site instanceof Streamate) {
|
||||||
|
if (streamateSiteUi == null) {
|
||||||
|
streamateSiteUi = new StreamateSiteUi((Streamate) site);
|
||||||
|
}
|
||||||
|
return streamateSiteUi;
|
||||||
}
|
}
|
||||||
throw new RuntimeException("Unknown site " + site.getName());
|
throw new RuntimeException("Unknown site " + site.getName());
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import ctbrec.Model;
|
||||||
import ctbrec.io.HttpException;
|
import ctbrec.io.HttpException;
|
||||||
import ctbrec.recorder.Recorder;
|
import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.ui.action.PlayAction;
|
import ctbrec.ui.action.PlayAction;
|
||||||
|
import ctbrec.ui.controls.StreamPreview;
|
||||||
import javafx.animation.FadeTransition;
|
import javafx.animation.FadeTransition;
|
||||||
import javafx.animation.FillTransition;
|
import javafx.animation.FillTransition;
|
||||||
import javafx.animation.ParallelTransition;
|
import javafx.animation.ParallelTransition;
|
||||||
|
@ -43,6 +44,7 @@ import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.scene.paint.Paint;
|
import javafx.scene.paint.Paint;
|
||||||
import javafx.scene.shape.Circle;
|
import javafx.scene.shape.Circle;
|
||||||
|
import javafx.scene.shape.Polygon;
|
||||||
import javafx.scene.shape.Rectangle;
|
import javafx.scene.shape.Rectangle;
|
||||||
import javafx.scene.shape.Shape;
|
import javafx.scene.shape.Shape;
|
||||||
import javafx.scene.text.Font;
|
import javafx.scene.text.Font;
|
||||||
|
@ -58,6 +60,7 @@ public class ThumbCell extends StackPane {
|
||||||
private static final Duration ANIMATION_DURATION = new Duration(250);
|
private static final Duration ANIMATION_DURATION = new Duration(250);
|
||||||
|
|
||||||
private Model model;
|
private Model model;
|
||||||
|
private StreamPreview streamPreview;
|
||||||
private ImageView iv;
|
private ImageView iv;
|
||||||
private Rectangle resolutionBackground;
|
private Rectangle resolutionBackground;
|
||||||
private final Paint resolutionOnlineColor = new Color(0.22, 0.8, 0.29, 1);
|
private final Paint resolutionOnlineColor = new Color(0.22, 0.8, 0.29, 1);
|
||||||
|
@ -87,8 +90,10 @@ public class ThumbCell extends StackPane {
|
||||||
.expireAfterAccess(4, TimeUnit.HOURS)
|
.expireAfterAccess(4, TimeUnit.HOURS)
|
||||||
.maximumSize(1000)
|
.maximumSize(1000)
|
||||||
.build();
|
.build();
|
||||||
|
private ThumbOverviewTab parent;
|
||||||
|
|
||||||
public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder) {
|
public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder) {
|
||||||
|
this.parent = parent;
|
||||||
this.thumbCellList = parent.grid.getChildren();
|
this.thumbCellList = parent.grid.getChildren();
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.recorder = recorder;
|
this.recorder = recorder;
|
||||||
|
@ -96,6 +101,11 @@ public class ThumbCell extends StackPane {
|
||||||
model.setSuspended(recorder.isSuspended(model));
|
model.setSuspended(recorder.isSuspended(model));
|
||||||
this.setStyle("-fx-background-color: -fx-base");
|
this.setStyle("-fx-background-color: -fx-base");
|
||||||
|
|
||||||
|
streamPreview = new StreamPreview();
|
||||||
|
streamPreview.prefWidthProperty().bind(widthProperty());
|
||||||
|
streamPreview.prefHeightProperty().bind(heightProperty());
|
||||||
|
getChildren().add(streamPreview);
|
||||||
|
|
||||||
iv = new ImageView();
|
iv = new ImageView();
|
||||||
iv.setSmooth(true);
|
iv.setSmooth(true);
|
||||||
iv.setPreserveRatio(true);
|
iv.setPreserveRatio(true);
|
||||||
|
@ -164,8 +174,12 @@ public class ThumbCell extends StackPane {
|
||||||
StackPane.setAlignment(pausedIndicator, Pos.TOP_LEFT);
|
StackPane.setAlignment(pausedIndicator, Pos.TOP_LEFT);
|
||||||
getChildren().add(pausedIndicator);
|
getChildren().add(pausedIndicator);
|
||||||
|
|
||||||
|
if(Config.getInstance().getSettings().livePreviews) {
|
||||||
|
getChildren().add(createPreviewTrigger());
|
||||||
|
}
|
||||||
|
|
||||||
selectionOverlay = new Rectangle();
|
selectionOverlay = new Rectangle();
|
||||||
selectionOverlay.setOpacity(0);
|
selectionOverlay.visibleProperty().bind(selectionProperty);
|
||||||
selectionOverlay.widthProperty().bind(widthProperty());
|
selectionOverlay.widthProperty().bind(widthProperty());
|
||||||
selectionOverlay.heightProperty().bind(heightProperty());
|
selectionOverlay.heightProperty().bind(heightProperty());
|
||||||
StackPane.setAlignment(selectionOverlay, Pos.TOP_LEFT);
|
StackPane.setAlignment(selectionOverlay, Pos.TOP_LEFT);
|
||||||
|
@ -197,6 +211,50 @@ public class ThumbCell extends StackPane {
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Node createPreviewTrigger() {
|
||||||
|
int s = 32;
|
||||||
|
StackPane previewTrigger = new StackPane();
|
||||||
|
previewTrigger.setStyle("-fx-background-color: white;");
|
||||||
|
previewTrigger.setOpacity(.8);
|
||||||
|
previewTrigger.setMaxSize(s, s);
|
||||||
|
|
||||||
|
Polygon play = new Polygon(new double[] {
|
||||||
|
16, 8,
|
||||||
|
26, 15,
|
||||||
|
16, 22
|
||||||
|
});
|
||||||
|
StackPane.setMargin(play, new Insets(0, 0, 0, 3));
|
||||||
|
play.setStyle("-fx-background-color: black;");
|
||||||
|
previewTrigger.getChildren().add(play);
|
||||||
|
|
||||||
|
Circle clip = new Circle(s / 2);
|
||||||
|
clip.setTranslateX(clip.getRadius());
|
||||||
|
clip.setTranslateY(clip.getRadius());
|
||||||
|
previewTrigger.setClip(clip);
|
||||||
|
StackPane.setAlignment(previewTrigger, Pos.BOTTOM_LEFT);
|
||||||
|
StackPane.setMargin(previewTrigger, new Insets(0, 0, 24, 4));
|
||||||
|
previewTrigger.setOnMouseEntered(evt -> setPreviewVisible(previewTrigger, true));
|
||||||
|
previewTrigger.setOnMouseExited(evt -> setPreviewVisible(previewTrigger, false));
|
||||||
|
return previewTrigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPreviewVisible(Node previewTrigger, boolean visible) {
|
||||||
|
parent.suspendUpdates(visible);
|
||||||
|
iv.setVisible(!visible);
|
||||||
|
topic.setVisible(!visible);
|
||||||
|
topicBackground.setVisible(!visible);
|
||||||
|
name.setVisible(!visible);
|
||||||
|
nameBackground.setVisible(!visible);
|
||||||
|
streamPreview.setVisible(visible);
|
||||||
|
streamPreview.startStream(model);
|
||||||
|
recordingIndicator.setVisible(!visible);
|
||||||
|
pausedIndicator.setVisible(!visible);
|
||||||
|
if(!visible) {
|
||||||
|
updateRecordingIndicator();
|
||||||
|
}
|
||||||
|
previewTrigger.setCursor(visible ? Cursor.HAND : Cursor.DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
public void setSelected(boolean selected) {
|
public void setSelected(boolean selected) {
|
||||||
selectionProperty.set(selected);
|
selectionProperty.set(selected);
|
||||||
selectionOverlay.getStyleClass().add("selection-background");
|
selectionOverlay.getStyleClass().add("selection-background");
|
||||||
|
@ -356,6 +414,10 @@ public class ThumbCell extends StackPane {
|
||||||
nameBackground.setFill(c);
|
nameBackground.setFill(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateRecordingIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateRecordingIndicator() {
|
||||||
if(recording) {
|
if(recording) {
|
||||||
recordingIndicator.setVisible(!model.isSuspended());
|
recordingIndicator.setVisible(!model.isSuspended());
|
||||||
pausedIndicator.setVisible(model.isSuspended());
|
pausedIndicator.setVisible(model.isSuspended());
|
||||||
|
@ -574,13 +636,15 @@ public class ThumbCell extends StackPane {
|
||||||
nameBackground.setWidth(w);
|
nameBackground.setWidth(w);
|
||||||
nameBackground.setHeight(20);
|
nameBackground.setHeight(20);
|
||||||
topicBackground.setWidth(w);
|
topicBackground.setWidth(w);
|
||||||
topicBackground.setHeight(getHeight()-nameBackground.getHeight());
|
topicBackground.setHeight(h - nameBackground.getHeight());
|
||||||
topic.prefHeight(getHeight()-25);
|
topic.prefHeight(getHeight()-25);
|
||||||
topic.maxHeight(getHeight()-25);
|
topic.maxHeight(getHeight()-25);
|
||||||
int margin = 4;
|
int margin = 4;
|
||||||
topic.maxWidth(w-margin*2);
|
topic.maxWidth(w-margin*2);
|
||||||
topic.setWrappingWidth(w-margin*2);
|
topic.setWrappingWidth(w-margin*2);
|
||||||
|
|
||||||
|
streamPreview.resizeTo(w, h);
|
||||||
|
|
||||||
Rectangle clip = new Rectangle(w, h);
|
Rectangle clip = new Rectangle(w, h);
|
||||||
clip.setArcWidth(10);
|
clip.setArcWidth(10);
|
||||||
clip.arcHeightProperty().bind(clip.arcWidthProperty());
|
clip.arcHeightProperty().bind(clip.arcWidthProperty());
|
||||||
|
|
|
@ -479,6 +479,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||||
event.put("amount", tokens);
|
event.put("amount", tokens);
|
||||||
EventBusHolder.BUS.post(event);
|
EventBusHolder.BUS.post(event);
|
||||||
} catch (Exception e1) {
|
} catch (Exception e1) {
|
||||||
|
LOG.error("An error occured while sending tip", e1);
|
||||||
showError("Couldn't send tip", "An error occured while sending tip:", e1);
|
showError("Couldn't send tip", "An error occured while sending tip:", e1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
|
@ -48,7 +47,7 @@ public class TipDialog extends TextInputDialog {
|
||||||
int tokens = get();
|
int tokens = get();
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
if (tokens <= 0) {
|
if (tokens <= 0) {
|
||||||
String msg = "Do you want to buy tokens now?\n\nIf you agree, Chaturbate will open in a browser. "
|
String msg = "Do you want to buy tokens now?\n\nIf you agree, "+site.getName()+" will open in a browser. "
|
||||||
+ "The used address is an affiliate link, which supports me, but doesn't cost you anything more.";
|
+ "The used address is an affiliate link, which supports me, but doesn't cost you anything more.";
|
||||||
Alert buyTokens = new AutosizeAlert(Alert.AlertType.CONFIRMATION, msg, ButtonType.NO, ButtonType.YES);
|
Alert buyTokens = new AutosizeAlert(Alert.AlertType.CONFIRMATION, msg, ButtonType.NO, ButtonType.YES);
|
||||||
buyTokens.setTitle("No tokens");
|
buyTokens.setTitle("No tokens");
|
||||||
|
@ -56,7 +55,7 @@ public class TipDialog extends TextInputDialog {
|
||||||
buyTokens.showAndWait();
|
buyTokens.showAndWait();
|
||||||
TipDialog.this.close();
|
TipDialog.this.close();
|
||||||
if(buyTokens.getResult() == ButtonType.YES) {
|
if(buyTokens.getResult() == ButtonType.YES) {
|
||||||
DesktopIntegration.open(Chaturbate.AFFILIATE_LINK);
|
DesktopIntegration.open(site.getAffiliateLink());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
getEditor().setDisable(false);
|
getEditor().setDisable(false);
|
||||||
|
|
|
@ -1,15 +1,31 @@
|
||||||
package ctbrec.ui;
|
package ctbrec.ui;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.OS;
|
||||||
|
import ctbrec.ui.controls.Dialogs;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
import javafx.scene.web.WebEngine;
|
import javafx.scene.web.WebEngine;
|
||||||
import javafx.scene.web.WebView;
|
import javafx.scene.web.WebView;
|
||||||
|
|
||||||
public class WebbrowserTab extends Tab {
|
public class WebbrowserTab extends Tab {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(WebbrowserTab.class);
|
||||||
|
|
||||||
public WebbrowserTab(String uri) {
|
public WebbrowserTab(String uri) {
|
||||||
WebView browser = new WebView();
|
WebView browser = new WebView();
|
||||||
WebEngine webEngine = browser.getEngine();
|
WebEngine webEngine = browser.getEngine();
|
||||||
|
webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine"));
|
||||||
|
webEngine.setJavaScriptEnabled(true);
|
||||||
webEngine.load(uri);
|
webEngine.load(uri);
|
||||||
setContent(browser);
|
setContent(browser);
|
||||||
|
|
||||||
|
webEngine.setOnError(evt -> {
|
||||||
|
LOG.error("Couldn't load {}", uri, evt.getException());
|
||||||
|
Dialogs.showError("Error", "Couldn't load " + uri, evt.getException());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,10 @@ import java.io.IOException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.StringUtil;
|
||||||
import ctbrec.ui.AutosizeAlert;
|
import ctbrec.ui.AutosizeAlert;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.property.ObjectPropertyBase;
|
import javafx.beans.property.StringProperty;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.geometry.Point2D;
|
import javafx.geometry.Point2D;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
@ -29,18 +30,20 @@ import javafx.stage.FileChooser;
|
||||||
public abstract class AbstractFileSelectionBox extends HBox {
|
public abstract class AbstractFileSelectionBox extends HBox {
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(AbstractFileSelectionBox.class);
|
private static final transient Logger LOG = LoggerFactory.getLogger(AbstractFileSelectionBox.class);
|
||||||
|
|
||||||
private ObjectProperty<File> fileProperty = new ObjectPropertyBase<File>() {
|
// private ObjectProperty<File> fileProperty = new ObjectPropertyBase<File>() {
|
||||||
@Override
|
// @Override
|
||||||
public Object getBean() {
|
// public Object getBean() {
|
||||||
return null;
|
// return null;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public String getName() {
|
// public String getName() {
|
||||||
return "file";
|
// return "file";
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
private StringProperty fileProperty = new SimpleStringProperty();
|
||||||
protected TextField fileInput;
|
protected TextField fileInput;
|
||||||
|
protected boolean allowEmptyValue = false;
|
||||||
private Tooltip validationError = new Tooltip();
|
private Tooltip validationError = new Tooltip();
|
||||||
|
|
||||||
public AbstractFileSelectionBox() {
|
public AbstractFileSelectionBox() {
|
||||||
|
@ -67,8 +70,14 @@ public abstract class AbstractFileSelectionBox extends HBox {
|
||||||
private ChangeListener<? super String> textListener() {
|
private ChangeListener<? super String> textListener() {
|
||||||
return (obs, o, n) -> {
|
return (obs, o, n) -> {
|
||||||
String input = fileInput.getText();
|
String input = fileInput.getText();
|
||||||
File program = new File(input);
|
if(StringUtil.isBlank(input) && allowEmptyValue) {
|
||||||
setFile(program);
|
fileProperty.set("");
|
||||||
|
hideValidationHints();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
File program = new File(input);
|
||||||
|
setFile(program);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,13 +92,17 @@ public abstract class AbstractFileSelectionBox extends HBox {
|
||||||
validationError.show(getScene().getWindow(), p.getX(), p.getY() + fileInput.getHeight() + 4);
|
validationError.show(getScene().getWindow(), p.getX(), p.getY() + fileInput.getHeight() + 4);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fileInput.setBorder(Border.EMPTY);
|
fileProperty.set(file.getAbsolutePath());
|
||||||
fileInput.setTooltip(null);
|
hideValidationHints();
|
||||||
fileProperty.set(file);
|
|
||||||
validationError.hide();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void hideValidationHints() {
|
||||||
|
fileInput.setBorder(Border.EMPTY);
|
||||||
|
fileInput.setTooltip(null);
|
||||||
|
validationError.hide();
|
||||||
|
}
|
||||||
|
|
||||||
protected String validate(File file) {
|
protected String validate(File file) {
|
||||||
if (file == null || !file.exists()) {
|
if (file == null || !file.exists()) {
|
||||||
return "File does not exist";
|
return "File does not exist";
|
||||||
|
@ -98,6 +111,10 @@ public abstract class AbstractFileSelectionBox extends HBox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void allowEmptyValue() {
|
||||||
|
this.allowEmptyValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
private Button createBrowseButton() {
|
private Button createBrowseButton() {
|
||||||
Button button = new Button("Select");
|
Button button = new Button("Select");
|
||||||
button.setOnAction((e) -> {
|
button.setOnAction((e) -> {
|
||||||
|
@ -123,7 +140,7 @@ public abstract class AbstractFileSelectionBox extends HBox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectProperty<File> fileProperty() {
|
public StringProperty fileProperty() {
|
||||||
return fileProperty;
|
return fileProperty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,14 @@ import javafx.application.Platform;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
|
|
||||||
public class Dialogs {
|
public class Dialogs {
|
||||||
public static void showError(String header, String text, Exception e) {
|
public static void showError(String header, String text, Throwable t) {
|
||||||
Runnable r = () -> {
|
Runnable r = () -> {
|
||||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||||
alert.setTitle("Error");
|
alert.setTitle("Error");
|
||||||
alert.setHeaderText(header);
|
alert.setHeaderText(header);
|
||||||
String content = text;
|
String content = text;
|
||||||
if(e != null) {
|
if(t != null) {
|
||||||
content += " " + e.getLocalizedMessage();
|
content += " " + t.getLocalizedMessage();
|
||||||
}
|
}
|
||||||
alert.setContentText(content);
|
alert.setContentText(content);
|
||||||
alert.showAndWait();
|
alert.showAndWait();
|
||||||
|
|
|
@ -12,7 +12,7 @@ public class DirectorySelectionBox extends AbstractFileSelectionBox {
|
||||||
@Override
|
@Override
|
||||||
protected void choose() {
|
protected void choose() {
|
||||||
DirectoryChooser chooser = new DirectoryChooser();
|
DirectoryChooser chooser = new DirectoryChooser();
|
||||||
File currentDir = fileProperty().get();
|
File currentDir = new File(fileProperty().get());
|
||||||
if (currentDir.exists() && currentDir.isDirectory()) {
|
if (currentDir.exists() && currentDir.isDirectory()) {
|
||||||
chooser.setInitialDirectory(currentDir);
|
chooser.setInitialDirectory(currentDir);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
package ctbrec.ui.controls;
|
||||||
|
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.control.ProgressIndicator;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.media.Media;
|
||||||
|
import javafx.scene.media.MediaPlayer;
|
||||||
|
import javafx.scene.media.MediaView;
|
||||||
|
|
||||||
|
public class StreamPreview extends StackPane {
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(StreamPreview.class);
|
||||||
|
|
||||||
|
private ImageView preview = new ImageView();
|
||||||
|
private MediaView videoPreview;
|
||||||
|
private MediaPlayer videoPlayer;
|
||||||
|
private Media video;
|
||||||
|
private ProgressIndicator progressIndicator;
|
||||||
|
private static ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
private static Future<?> future;
|
||||||
|
|
||||||
|
public StreamPreview() {
|
||||||
|
videoPreview = new MediaView();
|
||||||
|
videoPreview.setFitWidth(Config.getInstance().getSettings().thumbWidth);
|
||||||
|
videoPreview.setFitHeight(videoPreview.getFitWidth() * 9 / 16);
|
||||||
|
videoPreview.setPreserveRatio(true);
|
||||||
|
StackPane.setMargin(videoPreview, new Insets(5));
|
||||||
|
|
||||||
|
preview.setFitWidth(Config.getInstance().getSettings().thumbWidth);
|
||||||
|
preview.setPreserveRatio(true);
|
||||||
|
preview.setSmooth(true);
|
||||||
|
preview.setStyle("-fx-background-radius: 10px, 10px, 10px, 10px;");
|
||||||
|
preview.visibleProperty().bind(videoPreview.visibleProperty().not());
|
||||||
|
StackPane.setMargin(preview, new Insets(5));
|
||||||
|
|
||||||
|
progressIndicator = new ProgressIndicator();
|
||||||
|
progressIndicator.setVisible(false);
|
||||||
|
|
||||||
|
Region veil = new Region();
|
||||||
|
veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.8)");
|
||||||
|
veil.visibleProperty().bind(progressIndicator.visibleProperty());
|
||||||
|
StackPane.setMargin(veil, new Insets(5));
|
||||||
|
|
||||||
|
getChildren().addAll(preview, videoPreview, veil, progressIndicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startStream(Model model) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
progressIndicator.setVisible(true);
|
||||||
|
if(model.getPreview() != null) {
|
||||||
|
try {
|
||||||
|
videoPreview.setVisible(false);
|
||||||
|
Image img = new Image(model.getPreview(), true);
|
||||||
|
preview.setImage(img);
|
||||||
|
double aspect = img.getWidth() / img.getHeight();
|
||||||
|
double w = Config.getInstance().getSettings().thumbWidth;
|
||||||
|
double h = w / aspect;
|
||||||
|
resizeTo(w, h);
|
||||||
|
} catch (Exception e) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(future != null && !future.isDone()) {
|
||||||
|
future.cancel(true);
|
||||||
|
}
|
||||||
|
future = executor.submit(() -> {
|
||||||
|
try {
|
||||||
|
List<StreamSource> sources = model.getStreamSources();
|
||||||
|
Collections.sort(sources);
|
||||||
|
StreamSource best = sources.get(0);
|
||||||
|
checkInterrupt();
|
||||||
|
LOG.debug("Preview url for {} is {}", model.getName(), best.getMediaPlaylistUrl());
|
||||||
|
video = new Media(best.getMediaPlaylistUrl());
|
||||||
|
video.setOnError(() -> onError(videoPlayer));
|
||||||
|
if(videoPlayer != null) {
|
||||||
|
videoPlayer.dispose();
|
||||||
|
}
|
||||||
|
videoPlayer = new MediaPlayer(video);
|
||||||
|
videoPlayer.setMute(true);
|
||||||
|
checkInterrupt();
|
||||||
|
videoPlayer.setOnReady(() -> {
|
||||||
|
if(!future.isCancelled()) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
double aspect = (double)video.getWidth() / video.getHeight();
|
||||||
|
double w = Config.getInstance().getSettings().thumbWidth;
|
||||||
|
double h = w / aspect;
|
||||||
|
resizeTo(w, h);
|
||||||
|
progressIndicator.setVisible(false);
|
||||||
|
videoPreview.setVisible(true);
|
||||||
|
videoPreview.setMediaPlayer(videoPlayer);
|
||||||
|
videoPlayer.play();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
videoPlayer.setOnError(() -> onError(videoPlayer));
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
if(e.getMessage().equals("Stream url unknown")) {
|
||||||
|
// fine hls url for mfc not known yet
|
||||||
|
} else {
|
||||||
|
LOG.warn("Couldn't start preview video: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
showTestImage();
|
||||||
|
} catch (HttpException e) {
|
||||||
|
if(e.getResponseCode() != 404) {
|
||||||
|
LOG.warn("Couldn't start preview video: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
showTestImage();
|
||||||
|
} catch (InterruptedException | InterruptedIOException e) {
|
||||||
|
// future has been canceled, that's fine
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
if(e.getCause() instanceof InterruptedException || e.getCause() instanceof InterruptedIOException) {
|
||||||
|
// future has been canceled, that's fine
|
||||||
|
} else {
|
||||||
|
LOG.warn("Couldn't start preview video: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
showTestImage();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.warn("Couldn't start preview video: {}", e.getMessage());
|
||||||
|
showTestImage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resizeTo(double w, double h) {
|
||||||
|
preview.setFitWidth(w);
|
||||||
|
preview.setFitHeight(h);
|
||||||
|
videoPreview.setFitWidth(w);
|
||||||
|
videoPreview.setFitHeight(h);
|
||||||
|
progressIndicator.setPrefSize(w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
MediaPlayer old = videoPlayer;
|
||||||
|
Future<?> oldFuture = future;
|
||||||
|
new Thread(() -> {
|
||||||
|
if(oldFuture != null && !oldFuture.isDone()) {
|
||||||
|
oldFuture.cancel(true);
|
||||||
|
}
|
||||||
|
if(old != null) {
|
||||||
|
old.dispose();
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onError(MediaPlayer videoPlayer) {
|
||||||
|
LOG.error("Error while starting preview stream", videoPlayer.getError());
|
||||||
|
Optional<Throwable> cause = Optional.ofNullable(videoPlayer).map(v -> v.getError()).map(e -> e.getCause());
|
||||||
|
if(cause.isPresent()) {
|
||||||
|
LOG.error("Error while starting preview stream root cause:", cause.get());
|
||||||
|
}
|
||||||
|
showTestImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showTestImage() {
|
||||||
|
stop();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
videoPreview.setVisible(false);
|
||||||
|
Image img = new Image(getClass().getResource("/image_not_found.png").toString(), true);
|
||||||
|
preview.setImage(img);
|
||||||
|
double aspect = img.getWidth() / img.getHeight();
|
||||||
|
double w = Config.getInstance().getSettings().thumbWidth;
|
||||||
|
double h = w / aspect;
|
||||||
|
resizeTo(w, h);
|
||||||
|
progressIndicator.setVisible(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkInterrupt() throws InterruptedException {
|
||||||
|
if(Thread.interrupted()) {
|
||||||
|
throw new InterruptedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -173,7 +173,7 @@ public class ActionSettingsPanel extends TitledPane {
|
||||||
if(playSound.isSelected()) {
|
if(playSound.isSelected()) {
|
||||||
ActionConfiguration ac = new ActionConfiguration();
|
ActionConfiguration ac = new ActionConfiguration();
|
||||||
ac.setType(PlaySound.class.getName());
|
ac.setType(PlaySound.class.getName());
|
||||||
File file = sound.fileProperty().get();
|
File file = new File(sound.fileProperty().get());
|
||||||
ac.getConfiguration().put("file", file.getAbsolutePath());
|
ac.getConfiguration().put("file", file.getAbsolutePath());
|
||||||
ac.setName("play " + file.getName());
|
ac.setName("play " + file.getName());
|
||||||
config.getActions().add(ac);
|
config.getActions().add(ac);
|
||||||
|
@ -181,7 +181,7 @@ public class ActionSettingsPanel extends TitledPane {
|
||||||
if(executeProgram.isSelected()) {
|
if(executeProgram.isSelected()) {
|
||||||
ActionConfiguration ac = new ActionConfiguration();
|
ActionConfiguration ac = new ActionConfiguration();
|
||||||
ac.setType(ExecuteProgram.class.getName());
|
ac.setType(ExecuteProgram.class.getName());
|
||||||
File file = program.fileProperty().get();
|
File file = new File(program.fileProperty().get());
|
||||||
ac.getConfiguration().put("file", file.getAbsolutePath());
|
ac.getConfiguration().put("file", file.getAbsolutePath());
|
||||||
ac.setName("execute " + file.getName());
|
ac.setName("execute " + file.getName());
|
||||||
config.getActions().add(ac);
|
config.getActions().add(ac);
|
||||||
|
|
|
@ -58,17 +58,19 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
private TextField port;
|
private TextField port;
|
||||||
private TextField onlineCheckIntervalInSecs;
|
private TextField onlineCheckIntervalInSecs;
|
||||||
private TextField leaveSpaceOnDevice;
|
private TextField leaveSpaceOnDevice;
|
||||||
|
private TextField minimumLengthInSecs;
|
||||||
private CheckBox loadResolution;
|
private CheckBox loadResolution;
|
||||||
private CheckBox secureCommunication = new CheckBox();
|
private CheckBox secureCommunication = new CheckBox();
|
||||||
private CheckBox chooseStreamQuality = new CheckBox();
|
private CheckBox chooseStreamQuality = new CheckBox();
|
||||||
private CheckBox multiplePlayers = new CheckBox();
|
private CheckBox multiplePlayers = new CheckBox();
|
||||||
private CheckBox updateThumbnails = new CheckBox();
|
private CheckBox updateThumbnails = new CheckBox();
|
||||||
|
private CheckBox livePreviews = new CheckBox();
|
||||||
private CheckBox showPlayerStarting = new CheckBox();
|
private CheckBox showPlayerStarting = new CheckBox();
|
||||||
private RadioButton recordLocal;
|
private RadioButton recordLocal;
|
||||||
private RadioButton recordRemote;
|
private RadioButton recordRemote;
|
||||||
private ToggleGroup recordLocation;
|
private ToggleGroup recordLocation;
|
||||||
private ProxySettingsPane proxySettingsPane;
|
private ProxySettingsPane proxySettingsPane;
|
||||||
private ComboBox<Integer> maxResolution;
|
private TextField maxResolution;
|
||||||
private ComboBox<SplitAfterOption> splitAfter;
|
private ComboBox<SplitAfterOption> splitAfter;
|
||||||
private ComboBox<DirectoryStructure> directoryStructure;
|
private ComboBox<DirectoryStructure> directoryStructure;
|
||||||
private ComboBox<String> startTab;
|
private ComboBox<String> startTab;
|
||||||
|
@ -236,7 +238,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
recordingsDirectory = new DirectorySelectionBox(Config.getInstance().getSettings().recordingsDir);
|
recordingsDirectory = new DirectorySelectionBox(Config.getInstance().getSettings().recordingsDir);
|
||||||
recordingsDirectory.prefWidth(400);
|
recordingsDirectory.prefWidth(400);
|
||||||
recordingsDirectory.fileProperty().addListener((obs, o, n) -> {
|
recordingsDirectory.fileProperty().addListener((obs, o, n) -> {
|
||||||
String path = n.getAbsolutePath();
|
String path = n;
|
||||||
if(!Objects.equals(path, Config.getInstance().getSettings().recordingsDir)) {
|
if(!Objects.equals(path, Config.getInstance().getSettings().recordingsDir)) {
|
||||||
Config.getInstance().getSettings().recordingsDir = path;
|
Config.getInstance().getSettings().recordingsDir = path;
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
@ -262,26 +264,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
layout.add(directoryStructure, 1, row++);
|
layout.add(directoryStructure, 1, row++);
|
||||||
recordingsDirectory.prefWidthProperty().bind(directoryStructure.widthProperty());
|
recordingsDirectory.prefWidthProperty().bind(directoryStructure.widthProperty());
|
||||||
|
|
||||||
Label l = new Label("Maximum resolution (0 = unlimited)");
|
Label l = new Label("Split recordings after (minutes)");
|
||||||
layout.add(l, 0, row);
|
|
||||||
List<Integer> resolutionOptions = new ArrayList<>();
|
|
||||||
resolutionOptions.add(1080);
|
|
||||||
resolutionOptions.add(720);
|
|
||||||
resolutionOptions.add(600);
|
|
||||||
resolutionOptions.add(480);
|
|
||||||
resolutionOptions.add(0);
|
|
||||||
maxResolution = new ComboBox<>(FXCollections.observableList(resolutionOptions));
|
|
||||||
setMaxResolutionValue();
|
|
||||||
maxResolution.setOnAction((e) -> {
|
|
||||||
Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem();
|
|
||||||
saveConfig();
|
|
||||||
});
|
|
||||||
maxResolution.prefWidthProperty().bind(directoryStructure.widthProperty());
|
|
||||||
layout.add(maxResolution, 1, row++);
|
|
||||||
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
|
|
||||||
GridPane.setMargin(maxResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
|
||||||
|
|
||||||
l = new Label("Split recordings after (minutes)");
|
|
||||||
layout.add(l, 0, row);
|
layout.add(l, 0, row);
|
||||||
List<SplitAfterOption> splitOptions = new ArrayList<>();
|
List<SplitAfterOption> splitOptions = new ArrayList<>();
|
||||||
splitOptions.add(new SplitAfterOption("disabled", 0));
|
splitOptions.add(new SplitAfterOption("disabled", 0));
|
||||||
|
@ -306,11 +289,31 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
|
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
|
||||||
GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||||
|
|
||||||
|
l = new Label("Maximum resolution (0 = unlimited)");
|
||||||
|
layout.add(l, 0, row);
|
||||||
|
maxResolution = new TextField(Integer.toString(Config.getInstance().getSettings().maximumResolution));
|
||||||
|
maxResolution.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if (!newValue.matches("\\d*")) {
|
||||||
|
maxResolution.setText(newValue.replaceAll("[^\\d]", ""));
|
||||||
|
}
|
||||||
|
if (!maxResolution.getText().isEmpty()) {
|
||||||
|
int newRes = Integer.parseInt(maxResolution.getText());
|
||||||
|
if (newRes != Config.getInstance().getSettings().maximumResolution) {
|
||||||
|
Config.getInstance().getSettings().maximumResolution = newRes;
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
maxResolution.prefWidthProperty().bind(directoryStructure.widthProperty());
|
||||||
|
layout.add(maxResolution, 1, row++);
|
||||||
|
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
|
||||||
|
GridPane.setMargin(maxResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||||
|
|
||||||
layout.add(new Label("Post-Processing"), 0, row);
|
layout.add(new Label("Post-Processing"), 0, row);
|
||||||
// TODO allow empty strings to remove post-processing scripts
|
|
||||||
postProcessing = new ProgramSelectionBox(Config.getInstance().getSettings().postProcessing);
|
postProcessing = new ProgramSelectionBox(Config.getInstance().getSettings().postProcessing);
|
||||||
|
postProcessing.allowEmptyValue();
|
||||||
postProcessing.fileProperty().addListener((obs, o, n) -> {
|
postProcessing.fileProperty().addListener((obs, o, n) -> {
|
||||||
String path = n.getAbsolutePath();
|
String path = n;
|
||||||
if(!Objects.equals(path, Config.getInstance().getSettings().postProcessing)) {
|
if(!Objects.equals(path, Config.getInstance().getSettings().postProcessing)) {
|
||||||
Config.getInstance().getSettings().postProcessing = path;
|
Config.getInstance().getSettings().postProcessing = path;
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
@ -360,6 +363,26 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
GridPane.setMargin(leaveSpaceOnDevice, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
GridPane.setMargin(leaveSpaceOnDevice, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||||
layout.add(leaveSpaceOnDevice, 1, row++);
|
layout.add(leaveSpaceOnDevice, 1, row++);
|
||||||
|
|
||||||
|
tt = new Tooltip("Delete recordings, which are shorter than x seconds. 0 to disable.");
|
||||||
|
l = new Label("Delete recordings shorter than (secs)");
|
||||||
|
l.setTooltip(tt);
|
||||||
|
layout.add(l, 0, row);
|
||||||
|
int minimumLengthInSeconds = Config.getInstance().getSettings().minimumLengthInSeconds;
|
||||||
|
minimumLengthInSecs = new TextField(Integer.toString(minimumLengthInSeconds));
|
||||||
|
minimumLengthInSecs.setTooltip(tt);
|
||||||
|
minimumLengthInSecs.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
if (!newValue.matches("\\d*")) {
|
||||||
|
minimumLengthInSecs.setText(newValue.replaceAll("[^\\d]", ""));
|
||||||
|
}
|
||||||
|
if(!minimumLengthInSecs.getText().isEmpty()) {
|
||||||
|
int minimumLength = Integer.parseInt(minimumLengthInSecs.getText());
|
||||||
|
Config.getInstance().getSettings().minimumLengthInSeconds = minimumLength;
|
||||||
|
saveConfig();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
GridPane.setMargin(minimumLengthInSecs, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||||
|
layout.add(minimumLengthInSecs, 1, row++);
|
||||||
|
|
||||||
TitledPane locations = new TitledPane("Recorder", layout);
|
TitledPane locations = new TitledPane("Recorder", layout);
|
||||||
locations.setCollapsible(false);
|
locations.setCollapsible(false);
|
||||||
return locations;
|
return locations;
|
||||||
|
@ -372,7 +395,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
layout.add(new Label("Player"), 0, row);
|
layout.add(new Label("Player"), 0, row);
|
||||||
mediaPlayer = new ProgramSelectionBox(Config.getInstance().getSettings().mediaPlayer);
|
mediaPlayer = new ProgramSelectionBox(Config.getInstance().getSettings().mediaPlayer);
|
||||||
mediaPlayer.fileProperty().addListener((obs, o, n) -> {
|
mediaPlayer.fileProperty().addListener((obs, o, n) -> {
|
||||||
String path = n.getAbsolutePath();
|
String path = n;
|
||||||
if (!Objects.equals(path, Config.getInstance().getSettings().mediaPlayer)) {
|
if (!Objects.equals(path, Config.getInstance().getSettings().mediaPlayer)) {
|
||||||
Config.getInstance().getSettings().mediaPlayer = path;
|
Config.getInstance().getSettings().mediaPlayer = path;
|
||||||
saveConfig();
|
saveConfig();
|
||||||
|
@ -401,7 +424,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
Config.getInstance().getSettings().showPlayerStarting = showPlayerStarting.isSelected();
|
Config.getInstance().getSettings().showPlayerStarting = showPlayerStarting.isSelected();
|
||||||
saveConfig();
|
saveConfig();
|
||||||
});
|
});
|
||||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
|
||||||
GridPane.setMargin(showPlayerStarting, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
GridPane.setMargin(showPlayerStarting, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||||
layout.add(showPlayerStarting, 1, row++);
|
layout.add(showPlayerStarting, 1, row++);
|
||||||
|
|
||||||
|
@ -413,7 +436,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
Config.getInstance().getSettings().determineResolution = loadResolution.isSelected();
|
Config.getInstance().getSettings().determineResolution = loadResolution.isSelected();
|
||||||
saveConfig();
|
saveConfig();
|
||||||
});
|
});
|
||||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
|
||||||
GridPane.setMargin(loadResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
GridPane.setMargin(loadResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||||
layout.add(loadResolution, 1, row++);
|
layout.add(loadResolution, 1, row++);
|
||||||
|
|
||||||
|
@ -424,7 +447,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected();
|
Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected();
|
||||||
saveConfig();
|
saveConfig();
|
||||||
});
|
});
|
||||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
|
||||||
GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||||
layout.add(chooseStreamQuality, 1, row++);
|
layout.add(chooseStreamQuality, 1, row++);
|
||||||
|
|
||||||
|
@ -435,10 +458,22 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
Config.getInstance().getSettings().updateThumbnails = updateThumbnails.isSelected();
|
Config.getInstance().getSettings().updateThumbnails = updateThumbnails.isSelected();
|
||||||
saveConfig();
|
saveConfig();
|
||||||
});
|
});
|
||||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
|
||||||
GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, CHECKBOX_MARGIN, CHECKBOX_MARGIN));
|
GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||||
layout.add(updateThumbnails, 1, row++);
|
layout.add(updateThumbnails, 1, row++);
|
||||||
|
|
||||||
|
l = new Label("Enable live previews (experimental)");
|
||||||
|
layout.add(l, 0, row);
|
||||||
|
livePreviews.setSelected(Config.getInstance().getSettings().livePreviews);
|
||||||
|
livePreviews.setOnAction((e) -> {
|
||||||
|
Config.getInstance().getSettings().livePreviews = livePreviews.isSelected();
|
||||||
|
saveConfig();
|
||||||
|
showRestartRequired();
|
||||||
|
});
|
||||||
|
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
|
||||||
|
GridPane.setMargin(livePreviews, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||||
|
layout.add(livePreviews, 1, row++);
|
||||||
|
|
||||||
l = new Label("Start Tab");
|
l = new Label("Start Tab");
|
||||||
layout.add(l, 0, row);
|
layout.add(l, 0, row);
|
||||||
startTab = new ComboBox<>();
|
startTab = new ComboBox<>();
|
||||||
|
@ -447,8 +482,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
saveConfig();
|
saveConfig();
|
||||||
});
|
});
|
||||||
layout.add(startTab, 1, row++);
|
layout.add(startTab, 1, row++);
|
||||||
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
|
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
|
||||||
GridPane.setMargin(startTab, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
GridPane.setMargin(startTab, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||||
|
|
||||||
l = new Label("Colors (Base / Accent)");
|
l = new Label("Colors (Base / Accent)");
|
||||||
layout.add(l, 0, row);
|
layout.add(l, 0, row);
|
||||||
|
@ -471,15 +506,6 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setMaxResolutionValue() {
|
|
||||||
int value = Config.getInstance().getSettings().maximumResolution;
|
|
||||||
for (Integer option : maxResolution.getItems()) {
|
|
||||||
if(option == value) {
|
|
||||||
maxResolution.getSelectionModel().select(option);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void showRestartRequired() {
|
void showRestartRequired() {
|
||||||
restartLabel.setVisible(true);
|
restartLabel.setVisible(true);
|
||||||
}
|
}
|
||||||
|
@ -503,6 +529,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
onlineCheckIntervalInSecs.setDisable(!local);
|
onlineCheckIntervalInSecs.setDisable(!local);
|
||||||
leaveSpaceOnDevice.setDisable(!local);
|
leaveSpaceOnDevice.setDisable(!local);
|
||||||
postProcessing.setDisable(!local);
|
postProcessing.setDisable(!local);
|
||||||
|
minimumLengthInSecs.setDisable(!local);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -56,7 +56,10 @@ public class BongaCamsUpdateService extends PaginatedScheduledService {
|
||||||
JSONArray _models = json.getJSONArray("models");
|
JSONArray _models = json.getJSONArray("models");
|
||||||
for (int i = 0; i < _models.length(); i++) {
|
for (int i = 0; i < _models.length(); i++) {
|
||||||
JSONObject m = _models.getJSONObject(i);
|
JSONObject m = _models.getJSONObject(i);
|
||||||
String name = m.getString("username");
|
String name = m.optString("username");
|
||||||
|
if(name.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
BongaCamsModel model = (BongaCamsModel) bongaCams.createModel(name);
|
BongaCamsModel model = (BongaCamsModel) bongaCams.createModel(name);
|
||||||
boolean away = m.optBoolean("is_away");
|
boolean away = m.optBoolean("is_away");
|
||||||
boolean online = m.optBoolean("online");
|
boolean online = m.optBoolean("online");
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
package ctbrec.ui.sites.streamate;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Settings;
|
||||||
|
import ctbrec.sites.streamate.Streamate;
|
||||||
|
import ctbrec.ui.DesktopIntegration;
|
||||||
|
import ctbrec.ui.settings.SettingsTab;
|
||||||
|
import ctbrec.ui.sites.AbstractConfigUI;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.PasswordField;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
|
||||||
|
public class StreamateConfigUI extends AbstractConfigUI {
|
||||||
|
private Streamate streamate;
|
||||||
|
|
||||||
|
public StreamateConfigUI(Streamate streamate) {
|
||||||
|
this.streamate = streamate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Parent createConfigPanel() {
|
||||||
|
GridPane layout = SettingsTab.createGridLayout();
|
||||||
|
Settings settings = Config.getInstance().getSettings();
|
||||||
|
|
||||||
|
int row = 0;
|
||||||
|
Label l = new Label("Active");
|
||||||
|
layout.add(l, 0, row);
|
||||||
|
CheckBox enabled = new CheckBox();
|
||||||
|
enabled.setSelected(!settings.disabledSites.contains(streamate.getName()));
|
||||||
|
enabled.setOnAction((e) -> {
|
||||||
|
if(enabled.isSelected()) {
|
||||||
|
settings.disabledSites.remove(streamate.getName());
|
||||||
|
} else {
|
||||||
|
settings.disabledSites.add(streamate.getName());
|
||||||
|
}
|
||||||
|
save();
|
||||||
|
});
|
||||||
|
GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
layout.add(enabled, 1, row++);
|
||||||
|
|
||||||
|
layout.add(new Label("Streamate User"), 0, row);
|
||||||
|
TextField username = new TextField(settings.streamateUsername);
|
||||||
|
username.textProperty().addListener((ob, o, n) -> {
|
||||||
|
if(!n.equals(Config.getInstance().getSettings().streamateUsername)) {
|
||||||
|
Config.getInstance().getSettings().streamateUsername = username.getText();
|
||||||
|
streamate.getHttpClient().logout();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
GridPane.setFillWidth(username, true);
|
||||||
|
GridPane.setHgrow(username, Priority.ALWAYS);
|
||||||
|
GridPane.setColumnSpan(username, 2);
|
||||||
|
layout.add(username, 1, row++);
|
||||||
|
|
||||||
|
layout.add(new Label("Streamate Password"), 0, row);
|
||||||
|
PasswordField password = new PasswordField();
|
||||||
|
password.setText(settings.streamatePassword);
|
||||||
|
password.textProperty().addListener((ob, o, n) -> {
|
||||||
|
if(!n.equals(Config.getInstance().getSettings().streamatePassword)) {
|
||||||
|
Config.getInstance().getSettings().streamatePassword = password.getText();
|
||||||
|
streamate.getHttpClient().logout();
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
GridPane.setFillWidth(password, true);
|
||||||
|
GridPane.setHgrow(password, Priority.ALWAYS);
|
||||||
|
GridPane.setColumnSpan(password, 2);
|
||||||
|
layout.add(password, 1, row++);
|
||||||
|
|
||||||
|
Button createAccount = new Button("Create new Account");
|
||||||
|
createAccount.setOnAction((e) -> DesktopIntegration.open(streamate.getAffiliateLink()));
|
||||||
|
layout.add(createAccount, 1, row++);
|
||||||
|
GridPane.setColumnSpan(createAccount, 2);
|
||||||
|
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package ctbrec.ui.sites.streamate;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import javax.xml.xpath.XPathExpressionException;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.sites.streamate.Streamate;
|
||||||
|
import ctbrec.sites.streamate.StreamateHttpClient;
|
||||||
|
import ctbrec.sites.streamate.StreamateModel;
|
||||||
|
import ctbrec.ui.PaginatedScheduledService;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class StreamateFollowedService extends PaginatedScheduledService {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(StreamateFollowedService.class);
|
||||||
|
|
||||||
|
private static final int MODELS_PER_PAGE = 48;
|
||||||
|
private Streamate streamate;
|
||||||
|
private StreamateHttpClient httpClient;
|
||||||
|
private String url;
|
||||||
|
private boolean showOnline = true;
|
||||||
|
|
||||||
|
public StreamateFollowedService(Streamate streamate) {
|
||||||
|
this.streamate = streamate;
|
||||||
|
this.httpClient = (StreamateHttpClient) streamate.getHttpClient();
|
||||||
|
this.url = streamate.getBaseUrl() + "/api/search/v1/favorites?host=streamate.com&domain=streamate.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<List<Model>> createTask() {
|
||||||
|
return new Task<List<Model>>() {
|
||||||
|
@Override
|
||||||
|
public List<Model> call() throws IOException, SAXException, ParserConfigurationException, XPathExpressionException {
|
||||||
|
httpClient.login();
|
||||||
|
String saKey = httpClient.getSaKey();
|
||||||
|
String _url = url + "&page_number=" + page + "&results_per_page=" + MODELS_PER_PAGE + "&sakey=" + saKey;
|
||||||
|
LOG.debug("Fetching page {}", _url);
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(_url)
|
||||||
|
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.addHeader("Accept", "application/json, */*")
|
||||||
|
.addHeader("Accept-Language", "en")
|
||||||
|
.addHeader("Referer", streamate.getBaseUrl())
|
||||||
|
.build();
|
||||||
|
try(Response response = streamate.getHttpClient().execute(request)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
List<Model> models = new ArrayList<>();
|
||||||
|
String content = response.body().string();
|
||||||
|
JSONObject json = new JSONObject(content);
|
||||||
|
if(json.optString("status").equals("SM_OK")) {
|
||||||
|
JSONArray performers = json.getJSONArray("Results");
|
||||||
|
for (int i = 0; i < performers.length(); i++) {
|
||||||
|
JSONObject p = performers.getJSONObject(i);
|
||||||
|
String nickname = p.getString("Nickname");
|
||||||
|
StreamateModel model = (StreamateModel) streamate.createModel(nickname);
|
||||||
|
model.setId(p.getLong("PerformerId"));
|
||||||
|
model.setPreview("https://m1.nsimg.net/biopic/320x240/" + model.getId());
|
||||||
|
boolean online = p.optString("LiveStatus").equals("live");
|
||||||
|
model.setOnline(online);
|
||||||
|
if(online == showOnline) {
|
||||||
|
models.add(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IOException("Status: " + json.optString("status"));
|
||||||
|
}
|
||||||
|
return models;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnline(boolean online) {
|
||||||
|
this.showOnline = online;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package ctbrec.ui.sites.streamate;
|
||||||
|
|
||||||
|
import ctbrec.sites.streamate.Streamate;
|
||||||
|
import ctbrec.ui.FollowedTab;
|
||||||
|
import ctbrec.ui.ThumbOverviewTab;
|
||||||
|
import javafx.concurrent.WorkerStateEvent;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.RadioButton;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
public class StreamateFollowedTab extends ThumbOverviewTab implements FollowedTab {
|
||||||
|
private Label status;
|
||||||
|
|
||||||
|
public StreamateFollowedTab(Streamate streamate) {
|
||||||
|
super("Favorites", new StreamateFollowedService(streamate), streamate);
|
||||||
|
|
||||||
|
status = new Label("Logging in...");
|
||||||
|
grid.getChildren().add(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void 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) -> {
|
||||||
|
((StreamateFollowedService)updateService).setOnline(online.isSelected());
|
||||||
|
queue.clear();
|
||||||
|
updateService.restart();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSuccess() {
|
||||||
|
grid.getChildren().remove(status);
|
||||||
|
super.onSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onFail(WorkerStateEvent event) {
|
||||||
|
status.setText("Login failed");
|
||||||
|
super.onFail(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void selected() {
|
||||||
|
status.setText("Logging in...");
|
||||||
|
super.selected();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScene(Scene scene) {
|
||||||
|
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||||
|
if(this.isSelected()) {
|
||||||
|
if(event.getCode() == KeyCode.DELETE) {
|
||||||
|
follow(selectedThumbCells, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package ctbrec.ui.sites.streamate;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import ctbrec.sites.ConfigUI;
|
||||||
|
import ctbrec.sites.streamate.Streamate;
|
||||||
|
import ctbrec.ui.SiteUI;
|
||||||
|
import ctbrec.ui.TabProvider;
|
||||||
|
|
||||||
|
public class StreamateSiteUi implements SiteUI {
|
||||||
|
|
||||||
|
private StreamateTabProvider tabProvider;
|
||||||
|
private StreamateConfigUI configUi;
|
||||||
|
private Streamate streamate;
|
||||||
|
|
||||||
|
public StreamateSiteUi(Streamate streamate) {
|
||||||
|
this.streamate = streamate;
|
||||||
|
tabProvider = new StreamateTabProvider(streamate);
|
||||||
|
configUi = new StreamateConfigUI(streamate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TabProvider getTabProvider() {
|
||||||
|
return tabProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConfigUI getConfigUI() {
|
||||||
|
return configUi;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean login() throws IOException {
|
||||||
|
return streamate.login();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package ctbrec.ui.sites.streamate;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.recorder.Recorder;
|
||||||
|
import ctbrec.sites.streamate.Streamate;
|
||||||
|
import ctbrec.ui.TabProvider;
|
||||||
|
import ctbrec.ui.ThumbOverviewTab;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Tab;
|
||||||
|
|
||||||
|
public class StreamateTabProvider extends TabProvider {
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(StreamateTabProvider.class);
|
||||||
|
private Streamate streamate;
|
||||||
|
private Recorder recorder;
|
||||||
|
private ThumbOverviewTab followedTab;
|
||||||
|
|
||||||
|
public StreamateTabProvider(Streamate streamate) {
|
||||||
|
this.streamate = streamate;
|
||||||
|
this.recorder = streamate.getRecorder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Tab> getTabs(Scene scene) {
|
||||||
|
List<Tab> tabs = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
tabs.add(createTab("Girls", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:f"));
|
||||||
|
tabs.add(createTab("Guys", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:m"));
|
||||||
|
tabs.add(createTab("Couples", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:mf"));
|
||||||
|
tabs.add(createTab("Lesbian", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:ff"));
|
||||||
|
tabs.add(createTab("Gay", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:mm"));
|
||||||
|
tabs.add(createTab("Groups", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:g"));
|
||||||
|
tabs.add(createTab("Trans female", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:tm2f"));
|
||||||
|
tabs.add(createTab("Trans male", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=gender:tf2m"));
|
||||||
|
tabs.add(createTab("New", Streamate.BASE_URL + "/api/search/list?domain=streamate.com&index=availperf&filters=new:true"));
|
||||||
|
|
||||||
|
followedTab = new StreamateFollowedTab(streamate);
|
||||||
|
followedTab.setRecorder(recorder);
|
||||||
|
tabs.add(followedTab);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Couldn't create streamate tab", e);
|
||||||
|
}
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Tab getFollowedTab() {
|
||||||
|
return followedTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tab createTab(String title, String url) throws IOException {
|
||||||
|
StreamateUpdateService updateService = new StreamateUpdateService(streamate, url);
|
||||||
|
ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, streamate);
|
||||||
|
tab.setRecorder(recorder);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package ctbrec.ui.sites.streamate;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import javax.xml.xpath.XPathExpressionException;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.sites.streamate.Streamate;
|
||||||
|
import ctbrec.sites.streamate.StreamateModel;
|
||||||
|
import ctbrec.ui.PaginatedScheduledService;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class StreamateUpdateService extends PaginatedScheduledService {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(StreamateUpdateService.class);
|
||||||
|
|
||||||
|
private static final int MODELS_PER_PAGE = 48;
|
||||||
|
private Streamate streamate;
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
public StreamateUpdateService(Streamate streamate, String url) {
|
||||||
|
this.streamate = streamate;
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<List<Model>> createTask() {
|
||||||
|
return new Task<List<Model>>() {
|
||||||
|
@Override
|
||||||
|
public List<Model> call() throws IOException, SAXException, ParserConfigurationException, XPathExpressionException {
|
||||||
|
int from = (page - 1) * MODELS_PER_PAGE;
|
||||||
|
String _url = url + "&from=" + from + "&size=" + MODELS_PER_PAGE;
|
||||||
|
LOG.debug("Fetching page {}", _url);
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(_url)
|
||||||
|
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.addHeader("Accept", "application/json, */*")
|
||||||
|
.addHeader("Accept-Language", "en")
|
||||||
|
.addHeader("Referer", streamate.getBaseUrl())
|
||||||
|
.build();
|
||||||
|
try(Response response = streamate.getHttpClient().execute(request)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
List<Model> models = new ArrayList<>();
|
||||||
|
String content = response.body().string();
|
||||||
|
JSONObject json = new JSONObject(content);
|
||||||
|
JSONArray performers = json.getJSONArray("performers");
|
||||||
|
for (int i = 0; i < performers.length(); i++) {
|
||||||
|
JSONObject p = performers.getJSONObject(i);
|
||||||
|
String nickname = p.getString("nickname");
|
||||||
|
StreamateModel model = (StreamateModel) streamate.createModel(nickname);
|
||||||
|
model.setId(p.getLong("id"));
|
||||||
|
model.setPreview(p.getString("thumbnail"));
|
||||||
|
model.setOnline(p.optBoolean("online"));
|
||||||
|
// TODO figure out, what all the states mean
|
||||||
|
// liveState {…}
|
||||||
|
// exclusiveShow false
|
||||||
|
// goldShow true
|
||||||
|
// onBreak false
|
||||||
|
// partyChat true
|
||||||
|
// preGoldShow true
|
||||||
|
// privateChat false
|
||||||
|
// specialShow false
|
||||||
|
models.add(model);
|
||||||
|
}
|
||||||
|
return models;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>1.15.0</version>
|
<version>1.16.0</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
package ctbrec;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.channels.ReadableByteChannel;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.jcodec.common.Demuxer;
|
||||||
|
import org.jcodec.common.DemuxerTrack;
|
||||||
|
import org.jcodec.common.TrackType;
|
||||||
|
import org.jcodec.common.Tuple;
|
||||||
|
import org.jcodec.common.Tuple._2;
|
||||||
|
import org.jcodec.common.io.FileChannelWrapper;
|
||||||
|
import org.jcodec.common.io.NIOUtils;
|
||||||
|
import org.jcodec.common.model.Packet;
|
||||||
|
import org.jcodec.containers.mps.MPSDemuxer;
|
||||||
|
import org.jcodec.containers.mps.MTSDemuxer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class MpegUtil {
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(MpegUtil.class);
|
||||||
|
|
||||||
|
public static void main(String[] args) throws IOException {
|
||||||
|
readFile(new File("../../test-recs/ff.ts"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void readFile(File file) throws IOException {
|
||||||
|
System.out.println(file.getCanonicalPath());
|
||||||
|
double duration = MpegUtil.getFileDuration(file);
|
||||||
|
System.out.println(Duration.ofSeconds((long) duration));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double getFileDuration(File file) throws IOException {
|
||||||
|
try(FileChannelWrapper ch = NIOUtils.readableChannel(file)) {
|
||||||
|
_2<Integer,Demuxer> m2tsDemuxer = createM2TSDemuxer(ch, TrackType.VIDEO);
|
||||||
|
Demuxer demuxer = m2tsDemuxer.v1;
|
||||||
|
DemuxerTrack videoDemux = demuxer.getTracks().get(0);
|
||||||
|
Packet videoFrame = null;
|
||||||
|
double totalDuration = 0;
|
||||||
|
while( (videoFrame = videoDemux.nextFrame()) != null) {
|
||||||
|
totalDuration += videoFrame.getDurationD();
|
||||||
|
}
|
||||||
|
return totalDuration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static _2<Integer, Demuxer> createM2TSDemuxer(FileChannelWrapper ch, TrackType targetTrack) throws IOException {
|
||||||
|
MTSDemuxer mts = new MTSDemuxer(ch);
|
||||||
|
Set<Integer> programs = mts.getPrograms();
|
||||||
|
if (programs.size() == 0) {
|
||||||
|
LOG.error("The MPEG TS stream contains no programs");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Tuple._2<Integer, Demuxer> found = null;
|
||||||
|
for (Integer pid : programs) {
|
||||||
|
ReadableByteChannel program = mts.getProgram(pid);
|
||||||
|
if (found != null) {
|
||||||
|
program.close();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
MPSDemuxer demuxer = new MPSDemuxer(program);
|
||||||
|
if (targetTrack == TrackType.AUDIO && demuxer.getAudioTracks().size() > 0
|
||||||
|
|| targetTrack == TrackType.VIDEO && demuxer.getVideoTracks().size() > 0) {
|
||||||
|
found = org.jcodec.common.Tuple._2(pid, (Demuxer) demuxer);
|
||||||
|
} else {
|
||||||
|
program.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
|
@ -93,7 +93,6 @@ public class OS {
|
||||||
} else if(OS.getOsType() == OS.TYPE.WINDOWS) {
|
} else if(OS.getOsType() == OS.TYPE.WINDOWS) {
|
||||||
notifyWindows(title, header, msg);
|
notifyWindows(title, header, msg);
|
||||||
} else if(OS.getOsType() == OS.TYPE.MAC) {
|
} else if(OS.getOsType() == OS.TYPE.MAC) {
|
||||||
// TODO find out, if it makes a sound or if we have to play a sound
|
|
||||||
notifyMac(title, header, msg);
|
notifyMac(title, header, msg);
|
||||||
} else {
|
} else {
|
||||||
// unknown system, try systemtray notification anyways
|
// unknown system, try systemtray notification anyways
|
||||||
|
|
|
@ -41,6 +41,7 @@ public class Settings {
|
||||||
public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
|
public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
|
||||||
public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
|
public DirectoryStructure recordingsDirStructure = DirectoryStructure.FLAT;
|
||||||
public long minimumSpaceLeftInBytes = 0;
|
public long minimumSpaceLeftInBytes = 0;
|
||||||
|
public int minimumLengthInSeconds = 0;
|
||||||
public String mediaPlayer = "/usr/bin/mpv";
|
public String mediaPlayer = "/usr/bin/mpv";
|
||||||
public String postProcessing = "";
|
public String postProcessing = "";
|
||||||
public String username = ""; // chaturbate username TODO maybe rename this onetime
|
public String username = ""; // chaturbate username TODO maybe rename this onetime
|
||||||
|
@ -59,13 +60,16 @@ public class Settings {
|
||||||
public boolean mfcIgnoreUpscaled = false;
|
public boolean mfcIgnoreUpscaled = false;
|
||||||
public String camsodaUsername = "";
|
public String camsodaUsername = "";
|
||||||
public String camsodaPassword = "";
|
public String camsodaPassword = "";
|
||||||
public String cam4Username;
|
public String cam4Username = "";
|
||||||
public String cam4Password;
|
public String cam4Password = "";
|
||||||
|
public String streamateUsername = "";
|
||||||
|
public String streamatePassword = "";
|
||||||
public String lastDownloadDir = "";
|
public String lastDownloadDir = "";
|
||||||
|
|
||||||
public List<Model> models = new ArrayList<>();
|
public List<Model> models = new ArrayList<>();
|
||||||
public List<EventHandlerConfiguration> eventHandlers = new ArrayList<>();
|
public List<EventHandlerConfiguration> eventHandlers = new ArrayList<>();
|
||||||
public boolean determineResolution = false;
|
public boolean determineResolution = false;
|
||||||
|
public boolean livePreviews = false;
|
||||||
public boolean requireAuthentication = false;
|
public boolean requireAuthentication = false;
|
||||||
public boolean chooseStreamQuality = false;
|
public boolean chooseStreamQuality = false;
|
||||||
public int maximumResolution = 0;
|
public int maximumResolution = 0;
|
||||||
|
|
|
@ -48,9 +48,9 @@ public class ExecuteProgram extends Action {
|
||||||
err.start();
|
err.start();
|
||||||
|
|
||||||
process.waitFor();
|
process.waitFor();
|
||||||
LOG.debug("executing {} finished", executable);
|
LOG.debug("Executing {} finished", executable);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.error("Error while processing {}", e);
|
LOG.error("Error while executing {}", executable, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,8 @@ import okhttp3.OkHttpClient.Builder;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
import okhttp3.Route;
|
import okhttp3.Route;
|
||||||
|
import okhttp3.WebSocket;
|
||||||
|
import okhttp3.WebSocketListener;
|
||||||
|
|
||||||
public abstract class HttpClient {
|
public abstract class HttpClient {
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(HttpClient.class);
|
private static final transient Logger LOG = LoggerFactory.getLogger(HttpClient.class);
|
||||||
|
@ -219,4 +221,9 @@ public abstract class HttpClient {
|
||||||
getCookieJar().clear();
|
getCookieJar().clear();
|
||||||
loggedIn = false;
|
loggedIn = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WebSocket newWebSocket(String url, WebSocketListener l) {
|
||||||
|
Request request = new Request.Builder().url(url).build();
|
||||||
|
return client.newWebSocket(request, l);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,9 @@ import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.squareup.moshi.JsonAdapter;
|
import com.squareup.moshi.JsonAdapter;
|
||||||
import com.squareup.moshi.JsonReader;
|
import com.squareup.moshi.JsonReader;
|
||||||
import com.squareup.moshi.JsonReader.Token;
|
import com.squareup.moshi.JsonReader.Token;
|
||||||
|
@ -16,6 +19,8 @@ import ctbrec.sites.chaturbate.ChaturbateModel;
|
||||||
|
|
||||||
public class ModelJsonAdapter extends JsonAdapter<Model> {
|
public class ModelJsonAdapter extends JsonAdapter<Model> {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(ModelJsonAdapter.class);
|
||||||
|
|
||||||
private List<Site> sites;
|
private List<Site> sites;
|
||||||
|
|
||||||
public ModelJsonAdapter() {
|
public ModelJsonAdapter() {
|
||||||
|
@ -62,7 +67,12 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
||||||
model.setSuspended(suspended);
|
model.setSuspended(suspended);
|
||||||
} else if(key.equals("siteSpecific")) {
|
} else if(key.equals("siteSpecific")) {
|
||||||
reader.beginObject();
|
reader.beginObject();
|
||||||
model.readSiteSpecificData(reader);
|
try {
|
||||||
|
model.readSiteSpecificData(reader);
|
||||||
|
} catch(Exception e) {
|
||||||
|
LOG.error("Couldn't read site specific data for model {}", model.getName());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
reader.endObject();
|
reader.endObject();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
package ctbrec.io;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringReader;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import javax.xml.xpath.XPath;
|
||||||
|
import javax.xml.xpath.XPathConstants;
|
||||||
|
import javax.xml.xpath.XPathExpressionException;
|
||||||
|
import javax.xml.xpath.XPathFactory;
|
||||||
|
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
import org.xml.sax.InputSource;
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
|
||||||
|
public class XmlParserUtils {
|
||||||
|
|
||||||
|
public static Document parse(String xml) throws ParserConfigurationException, SAXException, IOException {
|
||||||
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
|
return builder.parse(new InputSource(new StringReader(xml)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node getFirstElementByTagName(Document doc, String tagName) {
|
||||||
|
NodeList list = doc.getElementsByTagName(tagName);
|
||||||
|
if (list.getLength() > 0) {
|
||||||
|
return list.item(0);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTextContent(Document doc, String tagName) {
|
||||||
|
Node node = getFirstElementByTagName(doc, tagName);
|
||||||
|
if (node != null) {
|
||||||
|
return node.getTextContent();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTextContent(Node parent, String tagName) {
|
||||||
|
Node node = findChildWithTagName(parent, tagName);
|
||||||
|
if (node != null) {
|
||||||
|
return node.getTextContent();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node findChildWithTagName(Node parent, String tagName) {
|
||||||
|
if (parent == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeList childs = parent.getChildNodes();
|
||||||
|
for (int i = 0; i < childs.getLength(); i++) {
|
||||||
|
Node child = childs.item(i);
|
||||||
|
if (child.getNodeName().equals(tagName)) {
|
||||||
|
return child;
|
||||||
|
} else if (child.hasChildNodes()) {
|
||||||
|
Node result = findChildWithTagName(child, tagName);
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void getElementsByTagName(Node parent, String tagName, List<Node> result) {
|
||||||
|
if (parent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeList childs = parent.getChildNodes();
|
||||||
|
for (int i = 0; i < childs.getLength(); i++) {
|
||||||
|
Node child = childs.item(i);
|
||||||
|
if (child.getNodeName().equals(tagName)) {
|
||||||
|
result.add(child);
|
||||||
|
} else if (child.hasChildNodes()) {
|
||||||
|
getElementsByTagName(child, tagName, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getStringWithXpath(String xml, String xpath) throws XPathExpressionException {
|
||||||
|
XPath xp = XPathFactory.newInstance().newXPath();
|
||||||
|
return xp.evaluate(xpath, new InputSource(new StringReader(xml)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getStringWithXpath(Node node, String xpath) throws XPathExpressionException {
|
||||||
|
XPath xp = XPathFactory.newInstance().newXPath();
|
||||||
|
return xp.evaluate(xpath, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node getNodeWithXpath(String xml, String xpath) throws XPathExpressionException {
|
||||||
|
XPath xp = XPathFactory.newInstance().newXPath();
|
||||||
|
return (Node) xp.evaluate(xpath, new InputSource(new StringReader(xml)), XPathConstants.NODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node getNodeWithXpath(Node node, String xpath) throws XPathExpressionException {
|
||||||
|
XPath xp = XPathFactory.newInstance().newXPath();
|
||||||
|
return (Node) xp.evaluate(xpath, node, XPathConstants.NODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NodeList getNodeListWithXpath(String xml, String xpath) throws XPathExpressionException {
|
||||||
|
XPath xp = XPathFactory.newInstance().newXPath();
|
||||||
|
return (NodeList) xp.evaluate(xpath, new InputSource(new StringReader(xml)), XPathConstants.NODESET);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ import static ctbrec.Recording.State.*;
|
||||||
import static ctbrec.event.Event.Type.*;
|
import static ctbrec.event.Event.Type.*;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FilenameFilter;
|
import java.io.FilenameFilter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.FileStore;
|
import java.nio.file.FileStore;
|
||||||
|
@ -11,6 +13,7 @@ import java.nio.file.Files;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -34,11 +37,19 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.iheartradio.m3u8.Encoding;
|
||||||
|
import com.iheartradio.m3u8.Format;
|
||||||
import com.iheartradio.m3u8.ParseException;
|
import com.iheartradio.m3u8.ParseException;
|
||||||
|
import com.iheartradio.m3u8.ParsingMode;
|
||||||
import com.iheartradio.m3u8.PlaylistException;
|
import com.iheartradio.m3u8.PlaylistException;
|
||||||
|
import com.iheartradio.m3u8.PlaylistParser;
|
||||||
|
import com.iheartradio.m3u8.data.MediaPlaylist;
|
||||||
|
import com.iheartradio.m3u8.data.Playlist;
|
||||||
|
import com.iheartradio.m3u8.data.TrackData;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
|
import ctbrec.MpegUtil;
|
||||||
import ctbrec.OS;
|
import ctbrec.OS;
|
||||||
import ctbrec.Recording;
|
import ctbrec.Recording;
|
||||||
import ctbrec.Recording.State;
|
import ctbrec.Recording.State;
|
||||||
|
@ -205,10 +216,14 @@ public class LocalRecorder implements Recorder {
|
||||||
|
|
||||||
private void stopRecordingProcess(Model model) {
|
private void stopRecordingProcess(Model model) {
|
||||||
Download download = recordingProcesses.get(model);
|
Download download = recordingProcesses.get(model);
|
||||||
download.stop();
|
|
||||||
recordingProcesses.remove(model);
|
recordingProcesses.remove(model);
|
||||||
fireRecordingStateChanged(download.getTarget(), STOPPED, model, download.getStartTime());
|
fireRecordingStateChanged(download.getTarget(), STOPPED, model, download.getStartTime());
|
||||||
ppThreadPool.submit(createPostProcessor(download));
|
|
||||||
|
Runnable stopAndThePostProcess = () -> {
|
||||||
|
download.stop();
|
||||||
|
createPostProcessor(download).run();
|
||||||
|
};
|
||||||
|
ppThreadPool.submit(stopAndThePostProcess);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void postprocess(Download download) {
|
private void postprocess(Download download) {
|
||||||
|
@ -539,6 +554,10 @@ public class LocalRecorder implements Recorder {
|
||||||
if (rec.listFiles().length == 0) {
|
if (rec.listFiles().length == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// don't list recordings, which currently get deleted
|
||||||
|
if (deleteInProgress.contains(rec)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Date startDate = sdf.parse(rec.getName());
|
Date startDate = sdf.parse(rec.getName());
|
||||||
Recording recording = new Recording();
|
Recording recording = new Recording();
|
||||||
|
@ -740,9 +759,71 @@ public class LocalRecorder implements Recorder {
|
||||||
fireRecordingStateChanged(download.getTarget(), GENERATING_PLAYLIST, download.getModel(), download.getStartTime());
|
fireRecordingStateChanged(download.getTarget(), GENERATING_PLAYLIST, download.getModel(), download.getStartTime());
|
||||||
generatePlaylist(download.getTarget());
|
generatePlaylist(download.getTarget());
|
||||||
}
|
}
|
||||||
|
boolean deleted = deleteIfTooShort(download);
|
||||||
|
if(deleted) {
|
||||||
|
// recording was too short. stop here and don't do post-processing
|
||||||
|
return;
|
||||||
|
}
|
||||||
fireRecordingStateChanged(download.getTarget(), POST_PROCESSING, download.getModel(), download.getStartTime());
|
fireRecordingStateChanged(download.getTarget(), POST_PROCESSING, download.getModel(), download.getStartTime());
|
||||||
postprocess(download);
|
postprocess(download);
|
||||||
fireRecordingStateChanged(download.getTarget(), FINISHED, download.getModel(), download.getStartTime());
|
fireRecordingStateChanged(download.getTarget(), FINISHED, download.getModel(), download.getStartTime());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO maybe get file size and bitrate and check, if the values are plausible
|
||||||
|
// we could also compare the length with the time elapsed since starting the recording
|
||||||
|
private boolean deleteIfTooShort(Download download) {
|
||||||
|
long minimumLengthInSeconds = Config.getInstance().getSettings().minimumLengthInSeconds;
|
||||||
|
if(minimumLengthInSeconds <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
LOG.debug("Determining video length for {}", download.getTarget());
|
||||||
|
File target = download.getTarget();
|
||||||
|
double duration = 0;
|
||||||
|
if(target.isDirectory()) {
|
||||||
|
File playlist = new File(target, "playlist.m3u8");
|
||||||
|
duration = getPlaylistLength(playlist);
|
||||||
|
} else {
|
||||||
|
duration = MpegUtil.getFileDuration(target);
|
||||||
|
}
|
||||||
|
Duration minLength = Duration.ofSeconds(minimumLengthInSeconds);
|
||||||
|
Duration videoLength = Duration.ofSeconds((long) duration);
|
||||||
|
LOG.debug("Recording started at:{}. Video length is {}", download.getStartTime(), videoLength);
|
||||||
|
if(videoLength.minus(minLength).isNegative()) {
|
||||||
|
LOG.debug("Video too short {} {}", videoLength, download.getTarget());
|
||||||
|
LOG.debug("Deleting {}", target);
|
||||||
|
if(target.isDirectory()) {
|
||||||
|
deleteDirectory(target);
|
||||||
|
deleteEmptyParents(target);
|
||||||
|
} else {
|
||||||
|
Files.delete(target.toPath());
|
||||||
|
deleteEmptyParents(target.getParentFile());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Couldn't check video length", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getPlaylistLength(File playlist) throws IOException, ParseException, PlaylistException {
|
||||||
|
if(playlist.exists()) {
|
||||||
|
PlaylistParser playlistParser = new PlaylistParser(new FileInputStream(playlist), Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||||
|
Playlist m3u = playlistParser.parse();
|
||||||
|
MediaPlaylist mediaPlaylist = m3u.getMediaPlaylist();
|
||||||
|
double length = 0;
|
||||||
|
for (TrackData trackData : mediaPlaylist.getTracks()) {
|
||||||
|
length += trackData.getTrackInfo().duration;
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
} else {
|
||||||
|
throw new FileNotFoundException(playlist.getAbsolutePath() + " does not exist");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,24 +6,10 @@ import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.FilenameFilter;
|
import java.io.FilenameFilter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.channels.ReadableByteChannel;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.jcodec.common.Demuxer;
|
|
||||||
import org.jcodec.common.DemuxerTrack;
|
|
||||||
import org.jcodec.common.TrackType;
|
|
||||||
import org.jcodec.common.Tuple;
|
|
||||||
import org.jcodec.common.Tuple._2;
|
|
||||||
import org.jcodec.common.io.FileChannelWrapper;
|
|
||||||
import org.jcodec.common.io.NIOUtils;
|
|
||||||
import org.jcodec.common.model.Packet;
|
|
||||||
import org.jcodec.containers.mps.MPSDemuxer;
|
|
||||||
import org.jcodec.containers.mps.MTSDemuxer;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -40,6 +26,8 @@ import com.iheartradio.m3u8.data.PlaylistType;
|
||||||
import com.iheartradio.m3u8.data.TrackData;
|
import com.iheartradio.m3u8.data.TrackData;
|
||||||
import com.iheartradio.m3u8.data.TrackInfo;
|
import com.iheartradio.m3u8.data.TrackInfo;
|
||||||
|
|
||||||
|
import ctbrec.MpegUtil;
|
||||||
|
|
||||||
|
|
||||||
public class PlaylistGenerator {
|
public class PlaylistGenerator {
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(PlaylistGenerator.class);
|
private static final transient Logger LOG = LoggerFactory.getLogger(PlaylistGenerator.class);
|
||||||
|
@ -58,10 +46,8 @@ public class PlaylistGenerator {
|
||||||
|
|
||||||
Arrays.sort(files, (f1, f2) -> {
|
Arrays.sort(files, (f1, f2) -> {
|
||||||
String n1 = f1.getName();
|
String n1 = f1.getName();
|
||||||
int seq1 = getSequence(n1);
|
|
||||||
String n2 = f2.getName();
|
String n2 = f2.getName();
|
||||||
int seq2 = getSequence(n2);
|
return n1.compareTo(n2);
|
||||||
return seq1 - seq2;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// create a track containing all files
|
// create a track containing all files
|
||||||
|
@ -72,7 +58,7 @@ public class PlaylistGenerator {
|
||||||
try {
|
try {
|
||||||
track.add(new TrackData.Builder()
|
track.add(new TrackData.Builder()
|
||||||
.withUri(file.getName())
|
.withUri(file.getName())
|
||||||
.withTrackInfo(new TrackInfo((float) getFileDuration(file), file.getName()))
|
.withTrackInfo(new TrackInfo((float) MpegUtil.getFileDuration(file), file.getName()))
|
||||||
.build());
|
.build());
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
LOG.warn("Couldn't determine duration for {}. Skipping this file.", file.getName());
|
LOG.warn("Couldn't determine duration for {}. Skipping this file.", file.getName());
|
||||||
|
@ -112,16 +98,6 @@ public class PlaylistGenerator {
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getSequence(String filename) {
|
|
||||||
filename = filename.substring(0, filename.lastIndexOf('.')); // cut off file suffix
|
|
||||||
Matcher matcher = Pattern.compile(".*?(\\d+)").matcher(filename);
|
|
||||||
if(matcher.matches()) {
|
|
||||||
return Integer.parseInt(matcher.group(1));
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateProgressListeners(double percentage) {
|
private void updateProgressListeners(double percentage) {
|
||||||
int p = (int) (percentage*100);
|
int p = (int) (percentage*100);
|
||||||
if(p > lastPercentage) {
|
if(p > lastPercentage) {
|
||||||
|
@ -141,45 +117,6 @@ public class PlaylistGenerator {
|
||||||
return targetDuration;
|
return targetDuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double getFileDuration(File file) throws IOException {
|
|
||||||
try(FileChannelWrapper ch = NIOUtils.readableChannel(file)) {
|
|
||||||
_2<Integer,Demuxer> m2tsDemuxer = createM2TSDemuxer(ch, TrackType.VIDEO);
|
|
||||||
Demuxer demuxer = m2tsDemuxer.v1;
|
|
||||||
DemuxerTrack videoDemux = demuxer.getTracks().get(0);
|
|
||||||
Packet videoFrame = null;
|
|
||||||
double totalDuration = 0;
|
|
||||||
while( (videoFrame = videoDemux.nextFrame()) != null) {
|
|
||||||
totalDuration += videoFrame.getDurationD();
|
|
||||||
}
|
|
||||||
return totalDuration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static _2<Integer, Demuxer> createM2TSDemuxer(FileChannelWrapper ch, TrackType targetTrack) throws IOException {
|
|
||||||
MTSDemuxer mts = new MTSDemuxer(ch);
|
|
||||||
Set<Integer> programs = mts.getPrograms();
|
|
||||||
if (programs.size() == 0) {
|
|
||||||
LOG.error("The MPEG TS stream contains no programs");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Tuple._2<Integer, Demuxer> found = null;
|
|
||||||
for (Integer pid : programs) {
|
|
||||||
ReadableByteChannel program = mts.getProgram(pid);
|
|
||||||
if (found != null) {
|
|
||||||
program.close();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
MPSDemuxer demuxer = new MPSDemuxer(program);
|
|
||||||
if (targetTrack == TrackType.AUDIO && demuxer.getAudioTracks().size() > 0
|
|
||||||
|| targetTrack == TrackType.VIDEO && demuxer.getVideoTracks().size() > 0) {
|
|
||||||
found = org.jcodec.common.Tuple._2(pid, (Demuxer) demuxer);
|
|
||||||
} else {
|
|
||||||
program.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addProgressListener(ProgressListener l) {
|
public void addProgressListener(ProgressListener l) {
|
||||||
listeners.add(l);
|
listeners.add(l);
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,9 @@ public abstract class AbstractHlsDownload implements Download {
|
||||||
Request request = new Request.Builder().url(segmentsUrl).addHeader("connection", "keep-alive").build();
|
Request request = new Request.Builder().url(segmentsUrl).addHeader("connection", "keep-alive").build();
|
||||||
try(Response response = client.execute(request)) {
|
try(Response response = client.execute(request)) {
|
||||||
if(response.isSuccessful()) {
|
if(response.isSuccessful()) {
|
||||||
|
// String body = response.body().string();
|
||||||
|
// InputStream inputStream = new ByteArrayInputStream(body.getBytes("utf-8"));
|
||||||
|
// LOG.debug("Segments {}", body);
|
||||||
InputStream inputStream = response.body().byteStream();
|
InputStream inputStream = response.body().byteStream();
|
||||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||||
Playlist playlist = parser.parse();
|
Playlist playlist = parser.parse();
|
||||||
|
|
|
@ -13,10 +13,13 @@ import java.nio.file.FileSystems;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.LinkOption;
|
import java.nio.file.LinkOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.NumberFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -39,6 +42,10 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
|
|
||||||
protected Path downloadDir;
|
protected Path downloadDir;
|
||||||
|
|
||||||
|
private int segmentCounter = 1;
|
||||||
|
private NumberFormat nf = new DecimalFormat("000000");
|
||||||
|
private Object downloadFinished = new Object();
|
||||||
|
|
||||||
public HlsDownload(HttpClient client) {
|
public HlsDownload(HttpClient client) {
|
||||||
super(client);
|
super(client);
|
||||||
}
|
}
|
||||||
|
@ -69,18 +76,13 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
}
|
}
|
||||||
int lastSegment = 0;
|
int lastSegment = 0;
|
||||||
int nextSegment = 0;
|
int nextSegment = 0;
|
||||||
|
int waitFactor = 1;
|
||||||
while(running) {
|
while(running) {
|
||||||
SegmentPlaylist lsp = getNextSegments(segments);
|
SegmentPlaylist lsp = getNextSegments(segments);
|
||||||
if(nextSegment > 0 && lsp.seq > nextSegment) {
|
if(nextSegment > 0 && lsp.seq > nextSegment) {
|
||||||
LOG.warn("Missed segments {} < {} in download for {}", nextSegment, lsp.seq, model);
|
|
||||||
String first = lsp.segments.get(0);
|
|
||||||
int seq = lsp.seq;
|
|
||||||
for (int i = nextSegment; i < lsp.seq; i++) {
|
|
||||||
URL segmentUrl = new URL(first.replaceAll(Integer.toString(seq), Integer.toString(i)));
|
|
||||||
LOG.debug("Reloading segment {} for model {}", i, model.getName());
|
|
||||||
downloadThreadPool.submit(new SegmentDownload(segmentUrl, downloadDir, client));
|
|
||||||
}
|
|
||||||
// TODO switch to a lower bitrate/resolution ?!?
|
// TODO switch to a lower bitrate/resolution ?!?
|
||||||
|
waitFactor *= 2;
|
||||||
|
LOG.warn("Missed segments {} < {} in download for {} - setting wait factor to 1/{}", nextSegment, lsp.seq, model, waitFactor);
|
||||||
}
|
}
|
||||||
int skip = nextSegment - lsp.seq;
|
int skip = nextSegment - lsp.seq;
|
||||||
for (String segment : lsp.segments) {
|
for (String segment : lsp.segments) {
|
||||||
|
@ -88,7 +90,8 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
skip--;
|
skip--;
|
||||||
} else {
|
} else {
|
||||||
URL segmentUrl = new URL(segment);
|
URL segmentUrl = new URL(segment);
|
||||||
downloadThreadPool.submit(new SegmentDownload(segmentUrl, downloadDir, client));
|
String prefix = nf.format(segmentCounter++);
|
||||||
|
downloadThreadPool.submit(new SegmentDownload(segmentUrl, downloadDir, client, prefix));
|
||||||
//new SegmentDownload(segment, downloadDir).call();
|
//new SegmentDownload(segment, downloadDir).call();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,7 +99,7 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
long wait = 0;
|
long wait = 0;
|
||||||
if(lastSegment == lsp.seq) {
|
if(lastSegment == lsp.seq) {
|
||||||
// playlist didn't change -> wait for at least half the target duration
|
// playlist didn't change -> wait for at least half the target duration
|
||||||
wait = (long) lsp.targetDuration * 1000 / 2;
|
wait = (long) lsp.targetDuration * 1000 / waitFactor;
|
||||||
LOG.trace("Playlist didn't change... waiting for {}ms", wait);
|
LOG.trace("Playlist didn't change... waiting for {}ms", wait);
|
||||||
} else {
|
} else {
|
||||||
// playlist did change -> wait for at least last segment duration
|
// playlist did change -> wait for at least last segment duration
|
||||||
|
@ -112,8 +115,12 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this if check makes sure, that we don't decrease nextSegment. for some reason
|
||||||
|
// streamate playlists sometimes jump back. e.g. max sequence = 79 -> 80 -> 79
|
||||||
lastSegment = lsp.seq;
|
lastSegment = lsp.seq;
|
||||||
nextSegment = lastSegment + lsp.segments.size();
|
if(lastSegment + lsp.segments.size() > nextSegment) {
|
||||||
|
nextSegment = lastSegment + lsp.segments.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new IOException("Couldn't determine segments uri");
|
throw new IOException("Couldn't determine segments uri");
|
||||||
|
@ -134,7 +141,15 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
throw new IOException("Couldn't download segment", e);
|
throw new IOException("Couldn't download segment", e);
|
||||||
} finally {
|
} finally {
|
||||||
|
downloadThreadPool.shutdown();
|
||||||
|
try {
|
||||||
|
LOG.debug("Waiting for last segments for {}", model);
|
||||||
|
downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
alive = false;
|
alive = false;
|
||||||
|
synchronized (downloadFinished) {
|
||||||
|
downloadFinished.notifyAll();
|
||||||
|
}
|
||||||
LOG.debug("Download for {} terminated", model);
|
LOG.debug("Download for {} terminated", model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,7 +157,13 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {
|
||||||
running = false;
|
running = false;
|
||||||
alive = false;
|
try {
|
||||||
|
synchronized (downloadFinished) {
|
||||||
|
downloadFinished.wait();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOG.error("Couldn't wait for download to finish", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class SegmentDownload implements Callable<Boolean> {
|
private static class SegmentDownload implements Callable<Boolean> {
|
||||||
|
@ -150,11 +171,11 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
private Path file;
|
private Path file;
|
||||||
private HttpClient client;
|
private HttpClient client;
|
||||||
|
|
||||||
public SegmentDownload(URL url, Path dir, HttpClient client) {
|
public SegmentDownload(URL url, Path dir, HttpClient client, String prefix) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
File path = new File(url.getPath());
|
File path = new File(url.getPath());
|
||||||
file = FileSystems.getDefault().getPath(dir.toString(), path.getName());
|
file = FileSystems.getDefault().getPath(dir.toString(), prefix + '_' + path.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -66,6 +66,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
private BlockingQueue<Runnable> downloadQueue = new LinkedBlockingQueue<>(50);
|
private BlockingQueue<Runnable> downloadQueue = new LinkedBlockingQueue<>(50);
|
||||||
private ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue);
|
private ExecutorService downloadThreadPool = new ThreadPoolExecutor(5, 5, 2, TimeUnit.MINUTES, downloadQueue);
|
||||||
private FileChannel fileChannel = null;
|
private FileChannel fileChannel = null;
|
||||||
|
private Object downloadFinished = new Object();
|
||||||
|
|
||||||
public MergedHlsDownload(HttpClient client) {
|
public MergedHlsDownload(HttpClient client) {
|
||||||
super(client);
|
super(client);
|
||||||
|
@ -105,13 +106,20 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) {
|
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) {
|
||||||
throw new IOException("Couldn't add HMAC to playlist url", e);
|
throw new IOException("Couldn't add HMAC to playlist url", e);
|
||||||
} finally {
|
} finally {
|
||||||
alive = false;
|
|
||||||
try {
|
try {
|
||||||
streamer.stop();
|
streamer.stop();
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
LOG.error("Couldn't stop streamer", e);
|
LOG.error("Couldn't stop streamer", e);
|
||||||
}
|
}
|
||||||
downloadThreadPool.shutdown();
|
downloadThreadPool.shutdown();
|
||||||
|
try {
|
||||||
|
LOG.debug("Waiting for last segments for {}", model);
|
||||||
|
downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
alive = false;
|
||||||
|
synchronized (downloadFinished) {
|
||||||
|
downloadFinished.notifyAll();
|
||||||
|
}
|
||||||
LOG.debug("Download terminated for {}", segmentPlaylistUri);
|
LOG.debug("Download terminated for {}", segmentPlaylistUri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,7 +163,6 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
throw new IOException("Couldn't download segment", e);
|
throw new IOException("Couldn't download segment", e);
|
||||||
} finally {
|
} finally {
|
||||||
alive = false;
|
|
||||||
if(streamer != null) {
|
if(streamer != null) {
|
||||||
try {
|
try {
|
||||||
streamer.stop();
|
streamer.stop();
|
||||||
|
@ -163,6 +170,15 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
LOG.error("Couldn't stop streamer", e);
|
LOG.error("Couldn't stop streamer", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
downloadThreadPool.shutdown();
|
||||||
|
try {
|
||||||
|
LOG.debug("Waiting for last segments for {}", model);
|
||||||
|
downloadThreadPool.awaitTermination(60, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
alive = false;
|
||||||
|
synchronized (downloadFinished) {
|
||||||
|
downloadFinished.notifyAll();
|
||||||
|
}
|
||||||
LOG.debug("Download for {} terminated", model);
|
LOG.debug("Download for {} terminated", model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -353,10 +369,16 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
@Override
|
@Override
|
||||||
public void stop() {
|
public void stop() {
|
||||||
running = false;
|
running = false;
|
||||||
alive = false;
|
|
||||||
if(streamer != null) {
|
if(streamer != null) {
|
||||||
streamer.stop();
|
streamer.stop();
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
synchronized (downloadFinished) {
|
||||||
|
downloadFinished.wait();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOG.error("Couldn't wait for download to finish", e);
|
||||||
|
}
|
||||||
LOG.debug("Download stopped");
|
LOG.debug("Download stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -157,15 +157,17 @@ public class BongaCams extends AbstractSite {
|
||||||
JSONArray results = json.getJSONArray("models");
|
JSONArray results = json.getJSONArray("models");
|
||||||
for (int i = 0; i < results.length(); i++) {
|
for (int i = 0; i < results.length(); i++) {
|
||||||
JSONObject result = results.getJSONObject(i);
|
JSONObject result = results.getJSONObject(i);
|
||||||
Model model = createModel(result.getString("username"));
|
if(result.has("username")) {
|
||||||
String thumb = result.getString("thumb_image");
|
Model model = createModel(result.getString("username"));
|
||||||
if(thumb != null) {
|
String thumb = result.getString("thumb_image");
|
||||||
model.setPreview("https:" + thumb);
|
if(thumb != null) {
|
||||||
|
model.setPreview("https:" + thumb);
|
||||||
|
}
|
||||||
|
if(result.has("display_name")) {
|
||||||
|
model.setDisplayName(result.getString("display_name"));
|
||||||
|
}
|
||||||
|
models.add(model);
|
||||||
}
|
}
|
||||||
if(result.has("display_name")) {
|
|
||||||
model.setDisplayName(result.getString("display_name"));
|
|
||||||
}
|
|
||||||
models.add(model);
|
|
||||||
}
|
}
|
||||||
return models;
|
return models;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -277,7 +277,7 @@ public class MyFreeCamsClient {
|
||||||
case ROOMDATA:
|
case ROOMDATA:
|
||||||
LOG.debug("ROOMDATA: {}", message);
|
LOG.debug("ROOMDATA: {}", message);
|
||||||
case UEOPT:
|
case UEOPT:
|
||||||
LOG.debug("UEOPT: {}", message);
|
LOG.trace("UEOPT: {}", message);
|
||||||
break;
|
break;
|
||||||
case SLAVEVSHARE:
|
case SLAVEVSHARE:
|
||||||
// LOG.debug("SLAVEVSHARE {}", message);
|
// LOG.debug("SLAVEVSHARE {}", message);
|
||||||
|
@ -295,7 +295,7 @@ public class MyFreeCamsClient {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
LOG.debug("Unknown message {}", message);
|
LOG.trace("Unknown message {}", message);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -420,6 +420,11 @@ public class MyFreeCamsClient {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uid not set, we can't identify this model
|
||||||
|
if(state.getUid() == null || state.getUid() <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
MyFreeCamsModel model = models.getIfPresent(state.getUid());
|
MyFreeCamsModel model = models.getIfPresent(state.getUid());
|
||||||
if(model == null) {
|
if(model == null) {
|
||||||
model = mfc.createModel(state.getNm());
|
model = mfc.createModel(state.getNm());
|
||||||
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
package ctbrec.sites.streamate;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.StringUtil;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.sites.AbstractSite;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class Streamate extends AbstractSite {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(Streamate.class);
|
||||||
|
|
||||||
|
public static final String BASE_URL = "https://www.streamate.com";
|
||||||
|
|
||||||
|
private StreamateHttpClient httpClient;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Streamate";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return BASE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffiliateLink() {
|
||||||
|
return BASE_URL + "/landing/click/?AFNO=2-11329.1";
|
||||||
|
// return BASE_URL + "/landing/click/?AFNO=2-11330.2";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Model createModel(String name) {
|
||||||
|
StreamateModel model = new StreamateModel();
|
||||||
|
model.setName(name);
|
||||||
|
model.setUrl(BASE_URL + "/cam/" + name);
|
||||||
|
model.setDescription("");
|
||||||
|
model.setSite(this);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getTokenBalance() throws IOException {
|
||||||
|
// int userId = ((StreamateHttpClient)getHttpClient()).getUserId();
|
||||||
|
// String url = Streamate.BASE_URL + "/tools/amf.php";
|
||||||
|
// RequestBody body = new FormBody.Builder()
|
||||||
|
// .add("method", "ping")
|
||||||
|
// .add("args[]", Integer.toString(userId))
|
||||||
|
// .build();
|
||||||
|
// Request request = new Request.Builder()
|
||||||
|
// .url(url)
|
||||||
|
// .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
// .addHeader("Accept", "application/json, text/javascript, */*")
|
||||||
|
// .addHeader("Accept-Language", "en")
|
||||||
|
// .addHeader("Referer", Streamate.BASE_URL)
|
||||||
|
// .addHeader("X-Requested-With", "XMLHttpRequest")
|
||||||
|
// .post(body)
|
||||||
|
// .build();
|
||||||
|
// try(Response response = getHttpClient().execute(request)) {
|
||||||
|
// if(response.isSuccessful()) {
|
||||||
|
// JSONObject json = new JSONObject(response.body().string());
|
||||||
|
// if(json.optString("status").equals("online")) {
|
||||||
|
// JSONObject userData = json.getJSONObject("userData");
|
||||||
|
// return userData.getInt("balance");
|
||||||
|
// } else {
|
||||||
|
// throw new IOException("Request was not successful: " + json.toString(2));
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// throw new HttpException(response.code(), response.message());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBuyTokensLink() {
|
||||||
|
return getAffiliateLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean login() throws IOException {
|
||||||
|
return credentialsAvailable() && getHttpClient().login();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient getHttpClient() {
|
||||||
|
if (httpClient == null) {
|
||||||
|
httpClient = new StreamateHttpClient();
|
||||||
|
}
|
||||||
|
return httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() throws IOException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
if (httpClient != null) {
|
||||||
|
httpClient.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsTips() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsFollow() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsSearch() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean searchRequiresLogin() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Model> search(String q) throws IOException, InterruptedException {
|
||||||
|
String url = BASE_URL + "/api/search/autocomplete?exact=false&skin_search_kids=0&results_per_page=10&query=" + URLEncoder.encode(q, "utf-8");
|
||||||
|
Request req = new Request.Builder().url(url)
|
||||||
|
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.addHeader("Accept", "application/json, text/javascript, */*")
|
||||||
|
.addHeader("Accept-Language", "en")
|
||||||
|
.addHeader("Referer", Streamate.BASE_URL)
|
||||||
|
.addHeader("X-Requested-With", "XMLHttpRequest").build();
|
||||||
|
try (Response response = getHttpClient().execute(req)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
String body = response.body().string();
|
||||||
|
JSONObject json = new JSONObject(body);
|
||||||
|
if (json.optString("status").equals("SM_OK")) {
|
||||||
|
List<Model> models = new ArrayList<>();
|
||||||
|
JSONObject results = json.getJSONObject("results");
|
||||||
|
JSONArray nickname = results.getJSONArray("nickname");
|
||||||
|
for (int i = 0; i < nickname.length(); i++) {
|
||||||
|
JSONObject result = nickname.getJSONObject(i);
|
||||||
|
StreamateModel model = (StreamateModel) createModel(result.getString("nickname"));
|
||||||
|
model.setId(Long.parseLong(result.getString("performerId")));
|
||||||
|
String thumb = result.getString("thumbnail");
|
||||||
|
if (thumb != null) {
|
||||||
|
model.setPreview(thumb);
|
||||||
|
}
|
||||||
|
model.setOnline(result.optString("liveStatus").equals("live"));
|
||||||
|
models.add(model);
|
||||||
|
}
|
||||||
|
return models;
|
||||||
|
} else {
|
||||||
|
LOG.warn("Search result: " + json.toString(2));
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSiteForModel(Model m) {
|
||||||
|
return m instanceof StreamateModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean credentialsAvailable() {
|
||||||
|
String username = Config.getInstance().getSettings().username;
|
||||||
|
return StringUtil.isNotBlank(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Model createModelFromUrl(String url) {
|
||||||
|
Matcher m = Pattern.compile("https?://.*?streamate.com/cam/([^/]*?)/?").matcher(url);
|
||||||
|
if (m.matches()) {
|
||||||
|
String modelName = m.group(1);
|
||||||
|
return createModel(modelName);
|
||||||
|
} else {
|
||||||
|
return super.createModelFromUrl(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package ctbrec.sites.streamate;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import okhttp3.Cookie;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class StreamateHttpClient extends HttpClient {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(StreamateHttpClient.class);
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
private String saKey = "";
|
||||||
|
private String userNickname = "";
|
||||||
|
|
||||||
|
public StreamateHttpClient() {
|
||||||
|
super("streamate");
|
||||||
|
|
||||||
|
// this cookie is needed for the search
|
||||||
|
Cookie searchCookie = new Cookie.Builder()
|
||||||
|
.domain("streamate.com")
|
||||||
|
.name("Xld_rct")
|
||||||
|
.value("1")
|
||||||
|
.build();
|
||||||
|
getCookieJar().saveFromResponse(HttpUrl.parse(Streamate.BASE_URL), Collections.singletonList(searchCookie));
|
||||||
|
|
||||||
|
// try to load sakey from cookie
|
||||||
|
try {
|
||||||
|
Cookie cookie = getCookieJar().getCookie(HttpUrl.parse("https://www.streamate.com"), "sakey");
|
||||||
|
saKey = cookie.value();
|
||||||
|
} catch (NoSuchElementException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean login() throws IOException {
|
||||||
|
if(loggedIn) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean cookiesWorked = checkLoginSuccess();
|
||||||
|
if(cookiesWorked) {
|
||||||
|
loggedIn = true;
|
||||||
|
LOG.debug("Logged in with cookies");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
loggedIn = loginWithoutCookies();
|
||||||
|
return loggedIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized boolean loginWithoutCookies() throws IOException {
|
||||||
|
JSONObject loginRequest = new JSONObject();
|
||||||
|
loginRequest.put("email", Config.getInstance().getSettings().streamateUsername);
|
||||||
|
loginRequest.put("password", Config.getInstance().getSettings().streamatePassword);
|
||||||
|
loginRequest.put("referrerId", 0);
|
||||||
|
loginRequest.put("siteId", 1);
|
||||||
|
RequestBody body = RequestBody.create(MediaType.parse("application/json"), loginRequest.toString());
|
||||||
|
Request login = new Request.Builder()
|
||||||
|
.url(Streamate.BASE_URL + "/api/member/login")
|
||||||
|
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.addHeader("Accept", "application/json, text/javascript, */*")
|
||||||
|
.addHeader("Accept-Language", "en")
|
||||||
|
.addHeader("Referer", Streamate.BASE_URL)
|
||||||
|
.addHeader("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.post(body)
|
||||||
|
.build();
|
||||||
|
try (Response response = client.newCall(login).execute()) {
|
||||||
|
String content = response.body().string();
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(content);
|
||||||
|
LOG.debug(json.toString(2));
|
||||||
|
loggedIn = json.has("sakey");
|
||||||
|
saKey = json.optString("sakey");
|
||||||
|
JSONObject account = json.getJSONObject("account");
|
||||||
|
userId = account.getLong("userid");
|
||||||
|
userNickname = account.getString("nickname");
|
||||||
|
} else {
|
||||||
|
throw new IOException("Login failed: " + response.code() + " " + response.message());
|
||||||
|
}
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return loggedIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check, if the login worked by loading the favorites
|
||||||
|
*/
|
||||||
|
public boolean checkLoginSuccess() {
|
||||||
|
String url = Streamate.BASE_URL + "/api/search/v1/favorites?host=streamate.com&domain=streamate.com";
|
||||||
|
url = url + "&page_number=1&results_per_page=48&sakey=" + saKey + "&userid=" + userId;
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.addHeader("Accept", "application/json, */*")
|
||||||
|
.addHeader("Accept-Language", "en")
|
||||||
|
.addHeader("Referer", Streamate.BASE_URL)
|
||||||
|
.build();
|
||||||
|
try(Response response = execute(request)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
String content = response.body().string();
|
||||||
|
JSONObject json = new JSONObject(content);
|
||||||
|
if(json.optString("status").equals("SM_OK")) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSaKey() {
|
||||||
|
return saKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUserId() throws IOException {
|
||||||
|
if(userId == null) {
|
||||||
|
loginWithoutCookies();
|
||||||
|
}
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserNickname() {
|
||||||
|
return userNickname;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,342 @@
|
||||||
|
package ctbrec.sites.streamate;
|
||||||
|
|
||||||
|
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 org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.iheartradio.m3u8.ParseException;
|
||||||
|
import com.iheartradio.m3u8.PlaylistException;
|
||||||
|
import com.squareup.moshi.JsonReader;
|
||||||
|
import com.squareup.moshi.JsonWriter;
|
||||||
|
|
||||||
|
import ctbrec.AbstractModel;
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okio.Buffer;
|
||||||
|
|
||||||
|
public class StreamateModel extends AbstractModel {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(StreamateModel.class);
|
||||||
|
|
||||||
|
private boolean online = false;
|
||||||
|
private List<StreamSource> streamSources = new ArrayList<>();
|
||||||
|
private int[] resolution;
|
||||||
|
private Long id;
|
||||||
|
private String streamId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||||
|
if(ignoreCache) {
|
||||||
|
String url = "https://sea1c-ls.naiadsystems.com/sea1c-edge-ls/80/live/s:" + getName() + ".json";
|
||||||
|
Request req = new Request.Builder().url(url)
|
||||||
|
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.addHeader("Accept", "*/*")
|
||||||
|
.addHeader("Accept-Language", "en")
|
||||||
|
.addHeader("Referer", Streamate.BASE_URL + '/' + getName())
|
||||||
|
.addHeader("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.build();
|
||||||
|
try(Response response = site.getHttpClient().execute(req)) {
|
||||||
|
online = response.isSuccessful();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return online;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnline(boolean online) {
|
||||||
|
this.online = online;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||||
|
if(failFast) {
|
||||||
|
return onlineState;
|
||||||
|
} else {
|
||||||
|
if(onlineState == UNKNOWN) {
|
||||||
|
return online ? ONLINE : OFFLINE;
|
||||||
|
}
|
||||||
|
return onlineState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOnlineState(State onlineState) {
|
||||||
|
this.onlineState = onlineState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||||
|
String url = "https://sea1c-ls.naiadsystems.com/sea1c-edge-ls/80/live/s:" + getName() + ".json";
|
||||||
|
Request req = new Request.Builder().url(url)
|
||||||
|
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.addHeader("Accept", "*/*")
|
||||||
|
.addHeader("Accept-Language", "en")
|
||||||
|
.addHeader("Referer", Streamate.BASE_URL + '/' + getName())
|
||||||
|
.addHeader("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.build();
|
||||||
|
try(Response response = site.getHttpClient().execute(req)) {
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
|
JSONObject formats = json.getJSONObject("formats");
|
||||||
|
JSONObject hls = formats.getJSONObject("mp4-hls");
|
||||||
|
|
||||||
|
// add encodings
|
||||||
|
JSONArray encodings = hls.getJSONArray("encodings");
|
||||||
|
streamSources.clear();
|
||||||
|
for (int i = 0; i < encodings.length(); i++) {
|
||||||
|
JSONObject encoding = encodings.getJSONObject(i);
|
||||||
|
StreamSource src = new StreamSource();
|
||||||
|
src.mediaPlaylistUrl = encoding.getString("location");
|
||||||
|
src.width = encoding.optInt("videoWidth");
|
||||||
|
src.height = encoding.optInt("videoHeight");
|
||||||
|
src.bandwidth = (encoding.optInt("videoKbps") + encoding.optInt("audioKbps")) * 1024;
|
||||||
|
streamSources.add(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add raw source stream
|
||||||
|
if(formats.has("mp4-ws")) {
|
||||||
|
JSONObject ws = formats.getJSONObject("mp4-ws");
|
||||||
|
JSONObject origin = hls.getJSONObject("origin");
|
||||||
|
StreamSource src = new StreamSource();
|
||||||
|
src.mediaPlaylistUrl = origin.getString("location");
|
||||||
|
origin = ws.getJSONObject("origin"); // switch to web socket origin, because it has width, height and bitrates
|
||||||
|
src.width = origin.optInt("videoWidth");
|
||||||
|
src.height = origin.optInt("videoHeight");
|
||||||
|
src.bandwidth = (origin.optInt("videoKbps") + origin.optInt("audioKbps")) * 1024;
|
||||||
|
streamSources.add(src);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return streamSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateCacheEntries() {
|
||||||
|
resolution = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receiveTip(int tokens) throws IOException {
|
||||||
|
/*
|
||||||
|
Mt._giveGoldAjax = function(e, t) {
|
||||||
|
var n = _t.getState(),
|
||||||
|
a = n.nickname,
|
||||||
|
o = n.id,
|
||||||
|
i = Ds.getState(),
|
||||||
|
r = i.userStreamId,
|
||||||
|
s = i.sakey,
|
||||||
|
l = i.userId,
|
||||||
|
c = i.nickname,
|
||||||
|
u = "";
|
||||||
|
switch (Ot.getState().streamType) {
|
||||||
|
case z.STREAM_TYPE_PRIVATE:
|
||||||
|
case z.STREAM_TYPE_BLOCK:
|
||||||
|
u = "premium";
|
||||||
|
break;
|
||||||
|
case z.STREAM_TYPE_EXCLUSIVE:
|
||||||
|
case z.STREAM_TYPE_BLOCK_EXCLUSIVE:
|
||||||
|
u = "exclusive"
|
||||||
|
}
|
||||||
|
if (!l) return ae.a.reject("no userId!");
|
||||||
|
var d = {
|
||||||
|
amt: e,
|
||||||
|
isprepopulated: t,
|
||||||
|
modelname: a,
|
||||||
|
nickname: c,
|
||||||
|
performernickname: a,
|
||||||
|
sakey: s,
|
||||||
|
session: u,
|
||||||
|
smid: o,
|
||||||
|
streamid: r,
|
||||||
|
userid: l,
|
||||||
|
username: c
|
||||||
|
},
|
||||||
|
p = de.a.getBaseUrl() + "/api/v1/givegold/";
|
||||||
|
return de.a.postPromise(p, d, "json")
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
StreamateHttpClient client = (StreamateHttpClient) getSite().getHttpClient();
|
||||||
|
client.login();
|
||||||
|
String saKey = client.getSaKey();
|
||||||
|
Long userId = client.getUserId();
|
||||||
|
String nickname = client.getUserNickname();
|
||||||
|
|
||||||
|
String url = "https://hybridclient.naiadsystems.com/api/v1/givegold/"; // this returns 404 at the moment. not sure if it's the wrong server, or if this is not used anymore
|
||||||
|
RequestBody body = new FormBody.Builder()
|
||||||
|
.add("amt", Integer.toString(tokens)) // amount
|
||||||
|
.add("isprepopulated", "1") // ?
|
||||||
|
.add("modelname", getName()) // model's name
|
||||||
|
.add("nickname", nickname) // user's nickname
|
||||||
|
.add("performernickname", getName()) // model's name
|
||||||
|
.add("sakey", saKey) // sakey from login
|
||||||
|
.add("session", "") // is related to gold an private shows, for normal tips keep it empty
|
||||||
|
.add("smid", Long.toString(getId())) // model id
|
||||||
|
.add("streamid", getStreamId()) // id of the current stream
|
||||||
|
.add("userid", Long.toString(userId)) // user's id
|
||||||
|
.add("username", nickname) // user's nickname
|
||||||
|
.build();
|
||||||
|
Buffer b = new Buffer();
|
||||||
|
body.writeTo(b);
|
||||||
|
LOG.debug("tip params {}", b.readUtf8());
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.addHeader("Accept", "application/json, text/javascript, */*")
|
||||||
|
.addHeader("Accept-Language", "en")
|
||||||
|
.addHeader("Referer", Streamate.BASE_URL + '/' + getName())
|
||||||
|
.addHeader("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.post(body)
|
||||||
|
.build();
|
||||||
|
try(Response response = site.getHttpClient().execute(request)) {
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
|
LOG.debug(json.toString(2));
|
||||||
|
if(!json.optString("status").equals("success")) {
|
||||||
|
LOG.error("Sending tip failed {}", json.toString(2));
|
||||||
|
throw new IOException("Sending tip failed");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getStreamId() throws IOException {
|
||||||
|
loadModelInfo();
|
||||||
|
return streamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadModelInfo() throws IOException {
|
||||||
|
String url = "https://hybridclient.naiadsystems.com/api/v1/config/?name=" + getName()
|
||||||
|
+ "&sabasic=&sakey=&sk=www.streamate.com&userid=0&version=6.3.17&ajax=1";
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.addHeader("Accept", "application/json, text/javascript, */*")
|
||||||
|
.addHeader("Accept-Language", "en")
|
||||||
|
.addHeader("Referer", Streamate.BASE_URL + '/' + getName())
|
||||||
|
.addHeader("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.build();
|
||||||
|
try(Response response = site.getHttpClient().execute(request)) {
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
|
JSONObject stream = json.getJSONObject("stream");
|
||||||
|
streamId = stream.getString("streamId");
|
||||||
|
JSONObject performer = json.getJSONObject("performer");
|
||||||
|
id = performer.getLong("id");
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
|
||||||
|
if(resolution == null) {
|
||||||
|
if(failFast) {
|
||||||
|
return new int[2];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if(!isOnline()) {
|
||||||
|
return new int[2];
|
||||||
|
}
|
||||||
|
List<StreamSource> streamSources = getStreamSources();
|
||||||
|
Collections.sort(streamSources);
|
||||||
|
StreamSource best = streamSources.get(streamSources.size()-1);
|
||||||
|
resolution = new int[] {best.width, best.height};
|
||||||
|
} catch (ExecutionException | IOException | ParseException | PlaylistException | InterruptedException e) {
|
||||||
|
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
|
||||||
|
}
|
||||||
|
return resolution;
|
||||||
|
} else {
|
||||||
|
return resolution;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean follow() throws IOException {
|
||||||
|
return follow(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean unfollow() throws IOException {
|
||||||
|
return follow(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean follow(boolean follow) throws IOException {
|
||||||
|
StreamateHttpClient client = (StreamateHttpClient) getSite().getHttpClient();
|
||||||
|
client.login();
|
||||||
|
String saKey = client.getSaKey();
|
||||||
|
Long userId = client.getUserId();
|
||||||
|
|
||||||
|
JSONObject requestParams = new JSONObject();
|
||||||
|
requestParams.put("sakey", saKey);
|
||||||
|
requestParams.put("userid", userId);
|
||||||
|
requestParams.put("pid", id);
|
||||||
|
requestParams.put("domain", "streamate.com");
|
||||||
|
requestParams.put("fav", follow);
|
||||||
|
RequestBody body = RequestBody.create(MediaType.parse("application/json"), requestParams.toString());
|
||||||
|
|
||||||
|
String url = site.getBaseUrl() + "/ajax/fav-notify.php?userid="+userId+"&sakey="+saKey+"&pid="+id+"&fav="+follow+"&domain=streamate.com";
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.addHeader("Accept", "application/json, */*")
|
||||||
|
.addHeader("Accept-Language", "en")
|
||||||
|
.addHeader("Referer", getSite().getBaseUrl())
|
||||||
|
.post(body)
|
||||||
|
.build();
|
||||||
|
try(Response response = getSite().getHttpClient().execute(request)) {
|
||||||
|
String content = response.body().string();
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(content);
|
||||||
|
return json.optBoolean("success");
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readSiteSpecificData(JsonReader reader) throws IOException {
|
||||||
|
reader.nextName();
|
||||||
|
id = reader.nextLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
|
||||||
|
if(id == null) {
|
||||||
|
try {
|
||||||
|
loadModelInfo();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.name("id").value(id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package ctbrec.sites.streamate;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.WebSocket;
|
||||||
|
import okhttp3.WebSocketListener;
|
||||||
|
import okio.ByteString;
|
||||||
|
|
||||||
|
public class StreamateWebsocketClient {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(StreamateWebsocketClient.class);
|
||||||
|
private String url;
|
||||||
|
private HttpClient client;
|
||||||
|
|
||||||
|
public StreamateWebsocketClient(String url, HttpClient client) {
|
||||||
|
this.url = url;
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
String roomId = "";
|
||||||
|
public String getRoomId() throws InterruptedException {
|
||||||
|
LOG.debug("Connecting to {}", url);
|
||||||
|
Object monitor = new Object();
|
||||||
|
client.newWebSocket(url, new WebSocketListener() {
|
||||||
|
@Override
|
||||||
|
public void onOpen(WebSocket webSocket, Response response) {
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocket webSocket, String text) {
|
||||||
|
if(text.contains("NaiadAuthorized")) {
|
||||||
|
Matcher m = Pattern.compile("\"roomid\":\"(.*?)\"").matcher(text);
|
||||||
|
if(m.find()) {
|
||||||
|
roomId = m.group(1);
|
||||||
|
webSocket.close(1000, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocket webSocket, ByteString bytes) {
|
||||||
|
LOG.debug("ws btxt {}", bytes.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClosed(WebSocket webSocket, int code, String reason) {
|
||||||
|
synchronized (monitor) {
|
||||||
|
monitor.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
||||||
|
LOG.debug("ws failure", t);
|
||||||
|
response.close();
|
||||||
|
synchronized (monitor) {
|
||||||
|
monitor.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
synchronized (monitor) {
|
||||||
|
monitor.wait();
|
||||||
|
}
|
||||||
|
return roomId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<version>1.15.0</version>
|
<version>1.16.0</version>
|
||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>../common</module>
|
<module>../common</module>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>master</artifactId>
|
<artifactId>master</artifactId>
|
||||||
<version>1.15.0</version>
|
<version>1.16.0</version>
|
||||||
<relativePath>../master</relativePath>
|
<relativePath>../master</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ import ctbrec.sites.cam4.Cam4;
|
||||||
import ctbrec.sites.camsoda.Camsoda;
|
import ctbrec.sites.camsoda.Camsoda;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
import ctbrec.sites.chaturbate.Chaturbate;
|
||||||
import ctbrec.sites.mfc.MyFreeCams;
|
import ctbrec.sites.mfc.MyFreeCams;
|
||||||
|
import ctbrec.sites.streamate.Streamate;
|
||||||
|
|
||||||
public class HttpServer {
|
public class HttpServer {
|
||||||
|
|
||||||
|
@ -82,6 +83,7 @@ public class HttpServer {
|
||||||
sites.add(new Camsoda());
|
sites.add(new Camsoda());
|
||||||
sites.add(new Cam4());
|
sites.add(new Cam4());
|
||||||
sites.add(new BongaCams());
|
sites.add(new BongaCams());
|
||||||
|
sites.add(new Streamate());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addShutdownHook() {
|
private void addShutdownHook() {
|
||||||
|
|
Loading…
Reference in New Issue