forked from j62/ctbrec
1
0
Fork 0

Merge branch 'dev' into notify

This commit is contained in:
0xboobface 2018-12-01 20:22:02 +01:00
commit 5b8cfc02d6
36 changed files with 929 additions and 305 deletions

View File

@ -1,3 +1,12 @@
1.12.2
========================
* Fix: Player not starting when path contains spaces
* Added setting to toggle "Player Starting" message
* Added possibility to add models by their URL
* Added pause / resume all buttons
* Implemented multi-selection for Recording and Recordings tab
* Fix: Don't do space check, if minimum is set to 0
1.12.1
========================
* Fixed downloads in client / server mode

View File

@ -288,9 +288,8 @@ public class CamrecApplication extends Application {
LOG.error("Couldn't load settings", e);
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Whoopsie");
alert.setContentText("Couldn't load settings.");
alert.setContentText("Couldn't load settings. Falling back to defaults. A backup of your settings has been created.");
alert.showAndWait();
System.exit(1);
}
config = Config.getInstance();
}

View File

@ -197,4 +197,14 @@ public class JavaFxModel implements Model {
delegate.setSuspended(suspended);
pausedProperty.set(suspended);
}
@Override
public String getDisplayName() {
return delegate.getDisplayName();
}
@Override
public void setDisplayName(String name) {
delegate.setDisplayName(name);
}
}

View File

@ -120,7 +120,11 @@ public class Player {
try {
if (Config.getInstance().getSettings().localRecording && rec != null) {
File file = new File(Config.getInstance().getSettings().recordingsDir, rec.getPath());
playerProcess = rt.exec(Config.getInstance().getSettings().mediaPlayer + " " + file, OS.getEnvironment(), file.getParentFile());
String[] args = new String[] {
Config.getInstance().getSettings().mediaPlayer,
file.getName()
};
playerProcess = rt.exec(args, OS.getEnvironment(), file.getParentFile());
} else {
if(Config.getInstance().getSettings().requireAuthentication) {
URL u = new URL(url);
@ -136,10 +140,12 @@ public class Player {
// create threads, which read stdout and stderr of the player process. these are needed,
// because otherwise the internal buffer for these streams fill up and block the process
Thread std = new Thread(new StreamRedirectThread(playerProcess.getInputStream(), new DevNull()));
//Thread std = new Thread(new StreamRedirectThread(playerProcess.getInputStream(), System.out));
std.setName("Player stdout pipe");
std.setDaemon(true);
std.start();
Thread err = new Thread(new StreamRedirectThread(playerProcess.getErrorStream(), new DevNull()));
//Thread err = new Thread(new StreamRedirectThread(playerProcess.getErrorStream(), System.err));
err.setName("Player stderr pipe");
err.setDaemon(true);
err.start();

View File

@ -0,0 +1,332 @@
package ctbrec.ui;
import java.io.InterruptedIOException;
import java.util.Collections;
import java.util.List;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import javafx.application.Platform;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
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;
import javafx.stage.Popup;
public class PreviewPopupHandler implements EventHandler<MouseEvent> {
private static final transient Logger LOG = LoggerFactory.getLogger(PreviewPopupHandler.class);
private static final int offset = 10;
private long timeForPopupOpen = TimeUnit.SECONDS.toMillis(1);
private long timeForPopupClose = 400;
private Popup popup = new Popup();
private Node parent;
private ImageView preview = new ImageView();
private MediaView videoPreview;
private MediaPlayer videoPlayer;
private Media video;
private JavaFxModel model;
private volatile long openCountdown = -1;
private volatile long closeCountdown = -1;
private volatile long lastModelChange = -1;
private volatile boolean changeModel = false;
private ExecutorService executor = Executors.newSingleThreadExecutor();
private Future<?> future;
private ProgressIndicator progressIndicator;
private StackPane pane;
public PreviewPopupHandler(Node parent) {
this.parent = parent;
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);
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-radius: 10px, 10px, 10px, 10px;" +
"-fx-padding: 1;" +
"-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.8), 20, 0, 0, 0);");
popup.getContent().add(pane);
createTimerThread();
}
@Override
public void handle(MouseEvent event) {
if(!isInPreviewColumn(event)) {
closeCountdown = timeForPopupClose;
return;
}
if(event.getEventType() == MouseEvent.MOUSE_CLICKED && event.getButton() == MouseButton.PRIMARY) {
model = getModel(event);
popup.setX(event.getScreenX()+ offset);
popup.setY(event.getScreenY()+ offset);
showPopup();
openCountdown = -1;
} else if(event.getEventType() == MouseEvent.MOUSE_ENTERED) {
popup.setX(event.getScreenX()+ offset);
popup.setY(event.getScreenY()+ offset);
JavaFxModel model = getModel(event);
if(model != null) {
closeCountdown = -1;
boolean modelChanged = model != this.model;
this.model = model;
if(popup.isShowing()) {
openCountdown = -1;
if(modelChanged) {
lastModelChange = System.currentTimeMillis();
changeModel = true;
future.cancel(true);
progressIndicator.setVisible(true);
}
} else {
openCountdown = timeForPopupOpen;
}
}
} else if(event.getEventType() == MouseEvent.MOUSE_EXITED) {
openCountdown = -1;
closeCountdown = timeForPopupClose;
model = null;
} else if(event.getEventType() == MouseEvent.MOUSE_MOVED) {
popup.setX(event.getScreenX() + offset);
popup.setY(event.getScreenY() + offset);
}
}
private boolean isInPreviewColumn(MouseEvent event) {
@SuppressWarnings("unchecked")
TableRow<JavaFxModel> row = (TableRow<JavaFxModel>) event.getSource();
TableView<JavaFxModel> table = row.getTableView();
double offset = 0;
double width = 0;
for (TableColumn<JavaFxModel, ?> col : table.getColumns()) {
offset += width;
width = col.getWidth();
if(Objects.equals(col.getId(), "preview")) {
Point2D screenToLocal = table.screenToLocal(event.getScreenX(), event.getScreenY());
double x = screenToLocal.getX();
return x >= offset && x <= offset + width;
}
}
return false;
}
private JavaFxModel getModel(MouseEvent event) {
@SuppressWarnings("unchecked")
TableRow<JavaFxModel> row = (TableRow<JavaFxModel>) event.getSource();
TableView<JavaFxModel> table = row.getTableView();
int rowIndex = row.getIndex();
if(rowIndex < table.getItems().size()) {
return table.getItems().get(rowIndex);
} else {
return null;
}
}
private void showPopup() {
startStream(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());
}
Platform.runLater(() -> {
showTestImage();
});
}
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() {
Platform.runLater(() -> {
popup.setX(-1000);
popup.setY(-1000);
popup.hide();
if(videoPlayer != null) {
videoPlayer.dispose();
}
});
}
private void createTimerThread() {
Thread timerThread = new Thread(() -> {
while(true) {
openCountdown--;
if(openCountdown == 0) {
openCountdown = -1;
if(model != null) {
showPopup();
}
}
closeCountdown--;
if(closeCountdown == 0) {
hidePopup();
closeCountdown = -1;
}
openCountdown = Math.max(openCountdown, -1);
closeCountdown = Math.max(closeCountdown, -1);
long now = System.currentTimeMillis();
long diff = (now - lastModelChange);
if(changeModel && diff > 400) {
changeModel = false;
if(model != null) {
startStream(model);
}
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
LOG.error("PreviewPopupTimer interrupted");
break;
}
}
});
timerThread.setDaemon(true);
timerThread.setPriority(Thread.MIN_PRIORITY);
timerThread.setName("PreviewPopupTimer");
timerThread.start();
}
}

View File

@ -3,6 +3,7 @@ package ctbrec.ui;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
@ -14,6 +15,7 @@ import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -29,6 +31,7 @@ import ctbrec.sites.Site;
import ctbrec.ui.controls.AutoFillTextField;
import ctbrec.ui.controls.Toast;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
@ -42,9 +45,11 @@ import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Tab;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.CheckBoxTableCell;
@ -79,6 +84,8 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
Label modelLabel = new Label("Model");
AutoFillTextField model;
Button addModelButton = new Button("Record");
Button pauseAll = new Button("Pause All");
Button resumeAll = new Button("Resume All");
public RecordedModelsTab(String title, Recorder recorder, List<Site> sites) {
super(title);
@ -100,54 +107,68 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
scrollPane.setFitToWidth(true);
BorderPane.setMargin(scrollPane, new Insets(5));
table.setEditable(false);
table.setEditable(true);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
PreviewPopupHandler previewPopupHandler = new PreviewPopupHandler(table);
table.setRowFactory((tableview) -> {
TableRow<JavaFxModel> row = new TableRow<>();
row.addEventHandler(MouseEvent.ANY, previewPopupHandler);
return row;
});
TableColumn<JavaFxModel, String> preview = new TableColumn<>("🎥");
preview.setPrefWidth(35);
preview.setCellValueFactory(cdf -> new SimpleStringProperty(""));
preview.setEditable(false);
preview.setId("preview");
TableColumn<JavaFxModel, String> name = new TableColumn<>("Model");
name.setPrefWidth(200);
name.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("name"));
name.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("displayName"));
name.setEditable(false);
TableColumn<JavaFxModel, String> url = new TableColumn<>("URL");
url.setCellValueFactory(new PropertyValueFactory<JavaFxModel, String>("url"));
url.setPrefWidth(400);
url.setEditable(false);
TableColumn<JavaFxModel, Boolean> online = new TableColumn<>("Online");
online.setCellValueFactory((cdf) -> cdf.getValue().getOnlineProperty());
online.setCellValueFactory(cdf -> cdf.getValue().getOnlineProperty());
online.setCellFactory(CheckBoxTableCell.forTableColumn(online));
online.setPrefWidth(100);
online.setEditable(false);
TableColumn<JavaFxModel, Boolean> recording = new TableColumn<>("Recording");
recording.setCellValueFactory((cdf) -> cdf.getValue().getRecordingProperty());
recording.setCellValueFactory(cdf -> cdf.getValue().getRecordingProperty());
recording.setCellFactory(CheckBoxTableCell.forTableColumn(recording));
recording.setPrefWidth(100);
recording.setEditable(false);
TableColumn<JavaFxModel, Boolean> paused = new TableColumn<>("Paused");
paused.setCellValueFactory((cdf) -> cdf.getValue().getPausedProperty());
paused.setCellValueFactory(cdf -> cdf.getValue().getPausedProperty());
paused.setCellFactory(CheckBoxTableCell.forTableColumn(paused));
paused.setPrefWidth(100);
table.getColumns().addAll(name, url, online, recording, paused);
paused.setEditable(true);
table.getColumns().addAll(preview, name, url, online, recording, paused);
table.setItems(observableModels);
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
popup = createContextMenu();
if(popup != null) {
if (popup != null) {
popup.show(table, event.getScreenX(), event.getScreenY());
}
event.consume();
});
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if(popup != null) {
if (popup != null) {
popup.hide();
}
});
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if(event.getCode() == KeyCode.DELETE) {
stopAction();
table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
List<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
if (event.getCode() == KeyCode.DELETE) {
stopAction(selectedModels);
} else if (event.getCode() == KeyCode.P) {
List<JavaFxModel> pausedModels = selectedModels.stream().filter(m -> m.isSuspended()).collect(Collectors.toList());
List<JavaFxModel> runningModels = selectedModels.stream().filter(m -> !m.isSuspended()).collect(Collectors.toList());
resumeRecording(pausedModels);
pauseRecording(runningModels);
}
});
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if(event.getCode() == KeyCode.S) {
for (TableColumn<JavaFxModel, ?> col : table.getSortOrder()) {
System.out.println(col.getText());
System.out.println(col.getSortType());
System.out.println(col.getComparator());
}
}
});
scrollPane.setContent(table);
HBox addModelBox = new HBox(5);
@ -155,14 +176,17 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
ObservableList<String> suggestions = FXCollections.observableArrayList();
sites.forEach(site -> suggestions.add(site.getName()));
model = new AutoFillTextField(suggestions);
model.setPrefWidth(300);
model.setPromptText("e.g. MyFreeCams:ModelName");
model.onActionHandler(e -> addModel(e));
model.setPrefWidth(600);
model.setPromptText("e.g. MyFreeCams:ModelName or an URL like https://chaturbate.com/modelname/");
model.onActionHandler(this::addModel);
model.setTooltip(new Tooltip("To add a model enter SiteName:ModelName\n" +
"press ENTER to confirm a suggested site name"));
BorderPane.setMargin(addModelBox, new Insets(5));
addModelButton.setOnAction((e) -> addModel(e));
addModelBox.getChildren().addAll(modelLabel, model, addModelButton);
addModelButton.setOnAction(this::addModel);
addModelBox.getChildren().addAll(modelLabel, model, addModelButton, pauseAll, resumeAll);
HBox.setMargin(pauseAll, new Insets(0, 0, 0, 20));
pauseAll.setOnAction(this::pauseAll);
resumeAll.setOnAction(this::resumeAll);
BorderPane root = new BorderPane();
root.setPadding(new Insets(5));
@ -174,6 +198,43 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}
private void addModel(ActionEvent e) {
String input = model.getText();
if (StringUtil.isBlank(input)) {
return;
}
if (input.startsWith("http")) {
addModelByUrl(input);
} else {
addModelByName(input);
}
};
private void addModelByUrl(String url) {
for (Site site : sites) {
Model model = site.createModelFromUrl(url);
if (model != null) {
try {
recorder.startRecording(model);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't add model");
alert.setContentText("The model " + model.getName() + " could not be added: " + e1.getLocalizedMessage());
alert.showAndWait();
}
return;
}
}
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Unknown URL format");
alert.setHeaderText("Couldn't add model");
alert.setContentText("The URL you entered has an unknown format or the function does not support this site, yet");
alert.showAndWait();
}
private void addModelByName(String siteModelCombo) {
String[] parts = model.getText().trim().split(":");
if (parts.length != 2) {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
@ -207,15 +268,50 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
alert.setHeaderText("Couldn't add model");
alert.setContentText("The site you entered is unknown");
alert.showAndWait();
};
}
private void pauseAll(ActionEvent evt) {
List<Model> models = recorder.getModelsRecording();
Consumer<Model> action = (m) -> {
try {
recorder.suspendRecording(m);
} catch(Exception e) {
Platform.runLater(() ->
showErrorDialog(e, "Couldn't suspend recording of model", "Suspending recording of " + m.getName() + " failed"));
}
};
massEdit(models, action);
}
private void resumeAll(ActionEvent evt) {
List<Model> models = recorder.getModelsRecording();
Consumer<Model> action = (m) -> {
try {
recorder.resumeRecording(m);
} catch(Exception e) {
Platform.runLater(() ->
showErrorDialog(e, "Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed"));
}
};
massEdit(models, action);
}
private void massEdit(List<Model> models, Consumer<Model> action) {
getTabPane().setCursor(Cursor.WAIT);
threadPool.submit(() -> {
for (Model model : models) {
action.accept(model);
}
Platform.runLater(() -> getTabPane().setCursor(Cursor.DEFAULT));
});
}
void initializeUpdateService() {
updateService = createUpdateService();
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
updateService.setOnSucceeded((event) -> {
List<JavaFxModel> models = updateService.getValue();
if(models == null) {
if (models == null) {
return;
}
@ -223,6 +319,17 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
int index = observableModels.indexOf(updatedModel);
if (index == -1) {
observableModels.add(updatedModel);
updatedModel.getPausedProperty().addListener((obs, oldV, newV) -> {
if (newV) {
if(!recorder.isSuspended(updatedModel)) {
pauseRecording(Collections.singletonList(updatedModel));
}
} else {
if(recorder.isSuspended(updatedModel)) {
resumeRecording(Collections.singletonList(updatedModel));
}
}
});
} else {
// make sure to update the JavaFX online property, so that the table cell is updated
JavaFxModel oldModel = observableModels.get(index);
@ -310,16 +417,16 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}
private ContextMenu createContextMenu() {
JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem();
if(selectedModel == null) {
ObservableList<JavaFxModel> selectedModels = table.getSelectionModel().getSelectedItems();
if (selectedModels.isEmpty()) {
return null;
}
MenuItem stop = new MenuItem("Remove Model");
stop.setOnAction((e) -> stopAction());
stop.setOnAction((e) -> stopAction(selectedModels));
MenuItem copyUrl = new MenuItem("Copy URL");
copyUrl.setOnAction((e) -> {
Model selected = selectedModel;
Model selected = selectedModels.get(0);
final Clipboard clipboard = Clipboard.getSystemClipboard();
final ClipboardContent content = new ClipboardContent();
content.putString(selected.getUrl());
@ -327,19 +434,31 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
});
MenuItem pauseRecording = new MenuItem("Pause Recording");
pauseRecording.setOnAction((e) -> pauseRecording());
pauseRecording.setOnAction((e) -> pauseRecording(selectedModels));
MenuItem resumeRecording = new MenuItem("Resume Recording");
resumeRecording.setOnAction((e) -> resumeRecording());
resumeRecording.setOnAction((e) -> resumeRecording(selectedModels));
MenuItem openInBrowser = new MenuItem("Open in Browser");
openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModel.getUrl()));
openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModels.get(0).getUrl()));
MenuItem openInPlayer = new MenuItem("Open in Player");
openInPlayer.setOnAction((e) -> openInPlayer(selectedModel));
openInPlayer.setOnAction((e) -> openInPlayer(selectedModels.get(0)));
MenuItem switchStreamSource = new MenuItem("Switch resolution");
switchStreamSource.setOnAction((e) -> switchStreamSource(selectedModel));
switchStreamSource.setOnAction((e) -> switchStreamSource(selectedModels.get(0)));
ContextMenu menu = new ContextMenu(stop);
menu.getItems().add(selectedModel.isSuspended() ? resumeRecording : pauseRecording);
if (selectedModels.size() == 1) {
menu.getItems().add(selectedModels.get(0).isSuspended() ? resumeRecording : pauseRecording);
} else {
menu.getItems().addAll(resumeRecording, pauseRecording);
}
menu.getItems().addAll(copyUrl, openInPlayer, openInBrowser, switchStreamSource);
if (selectedModels.size() > 1) {
copyUrl.setDisable(true);
openInPlayer.setDisable(true);
openInBrowser.setDisable(true);
switchStreamSource.setDisable(true);
}
return menu;
}
@ -348,7 +467,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
new Thread(() -> {
boolean started = Player.play(selectedModel);
Platform.runLater(() -> {
if(started) {
if (started && Config.getInstance().getSettings().showPlayerStarting) {
Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500);
}
table.setCursor(Cursor.DEFAULT);
@ -358,7 +477,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
private void switchStreamSource(JavaFxModel fxModel) {
try {
if(!fxModel.isOnline()) {
if (!fxModel.isOnline()) {
Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION);
alert.setTitle("Switch resolution");
alert.setHeaderText("Couldn't switch stream resolution");
@ -393,98 +512,61 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}
private void showStreamSwitchErrorDialog(Throwable throwable) {
showErrorDialog(throwable, "Couldn't switch stream resolution", "Error while switching stream resolution");
}
private void showErrorDialog(Throwable throwable, String header, String msg) {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't switch stream resolution");
alert.setContentText("Error while switching stream resolution: " + throwable.getLocalizedMessage());
alert.setHeaderText(header);
alert.setContentText(msg + ": " + throwable.getLocalizedMessage());
alert.showAndWait();
}
private void stopAction() {
Model selected = table.getSelectionModel().getSelectedItem().getDelegate();
if (selected != null) {
table.setCursor(Cursor.WAIT);
new Thread() {
@Override
public void run() {
try {
recorder.stopRecording(selected);
observableModels.remove(selected);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Couldn't stop recording", e1);
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't stop recording");
alert.setContentText("Error while stopping the recording: " + e1.getLocalizedMessage());
alert.showAndWait();
});
} finally {
table.setCursor(Cursor.DEFAULT);
}
}
}.start();
}
private void stopAction(List<JavaFxModel> selectedModels) {
Consumer<Model> action = (m) -> {
try {
recorder.stopRecording(m);
observableModels.remove(m);
} catch(Exception e) {
Platform.runLater(() ->
showErrorDialog(e, "Couldn't stop recording", "Stopping recording of " + m.getName() + " failed"));
}
};
List<Model> models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList());
massEdit(models, action);
};
private void pauseRecording() {
JavaFxModel model = table.getSelectionModel().getSelectedItem();
Model delegate = table.getSelectionModel().getSelectedItem().getDelegate();
if (delegate != null) {
table.setCursor(Cursor.WAIT);
new Thread() {
@Override
public void run() {
try {
recorder.suspendRecording(delegate);
Platform.runLater(() -> model.setSuspended(true));
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Couldn't pause recording", e1);
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't pause recording");
alert.setContentText("Error while pausing the recording: " + e1.getLocalizedMessage());
alert.showAndWait();
});
} finally {
table.setCursor(Cursor.DEFAULT);
}
}
}.start();
}
private void pauseRecording(List<JavaFxModel> selectedModels) {
Consumer<Model> action = (m) -> {
try {
recorder.suspendRecording(m);
m.setSuspended(true);
} catch(Exception e) {
Platform.runLater(() ->
showErrorDialog(e, "Couldn't pause recording of model", "Pausing recording of " + m.getName() + " failed"));
}
};
List<Model> models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList());
massEdit(models, action);
};
private void resumeRecording() {
JavaFxModel model = table.getSelectionModel().getSelectedItem();
Model delegate = table.getSelectionModel().getSelectedItem().getDelegate();
if (delegate != null) {
table.setCursor(Cursor.WAIT);
new Thread() {
@Override
public void run() {
try {
recorder.resumeRecording(delegate);
Platform.runLater(() -> model.setSuspended(false));
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Couldn't resume recording", e1);
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't resume recording");
alert.setContentText("Error while resuming the recording: " + e1.getLocalizedMessage());
alert.showAndWait();
});
} finally {
table.setCursor(Cursor.DEFAULT);
}
}
}.start();
}
private void resumeRecording(List<JavaFxModel> selectedModels) {
Consumer<Model> action = (m) -> {
try {
recorder.resumeRecording(m);
m.setSuspended(false);
} catch(Exception e) {
Platform.runLater(() ->
showErrorDialog(e, "Couldn't resume recording of model", "Resuming recording of " + m.getName() + " failed"));
}
};
List<Model> models = selectedModels.stream().map(jfxm -> jfxm.getDelegate()).collect(Collectors.toList());
massEdit(models, action);
}
public void saveState() {
if(!table.getSortOrder().isEmpty()) {
if (!table.getSortOrder().isEmpty()) {
TableColumn<JavaFxModel, ?> col = table.getSortOrder().get(0);
Config.getInstance().getSettings().recordedModelsSortColumn = col.getText();
Config.getInstance().getSettings().recordedModelsSortType = col.getSortType().toString();
@ -498,9 +580,9 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
private void restoreState() {
String sortCol = Config.getInstance().getSettings().recordedModelsSortColumn;
if(StringUtil.isNotBlank(sortCol)) {
if (StringUtil.isNotBlank(sortCol)) {
for (TableColumn<JavaFxModel, ?> col : table.getColumns()) {
if(Objects.equals(sortCol, col.getText())) {
if (Objects.equals(sortCol, col.getText())) {
col.setSortType(SortType.valueOf(Config.getInstance().getSettings().recordedModelsSortType));
table.getSortOrder().clear();
table.getSortOrder().add(col);
@ -510,7 +592,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}
double[] columnWidths = Config.getInstance().getSettings().recordedModelsColumnWidths;
if(columnWidths != null && columnWidths.length == table.getColumns().size()) {
if (columnWidths != null && columnWidths.length == table.getColumns().size()) {
for (int i = 0; i < columnWidths.length; i++) {
table.getColumns().get(i).setPrefWidth(columnWidths[i]);
}

View File

@ -6,6 +6,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.nio.file.NoSuchFileException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.DecimalFormat;
@ -22,6 +23,8 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -52,6 +55,7 @@ import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Tab;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
@ -91,6 +95,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
ContextMenu popup;
ProgressBar spaceLeft;
Label spaceLabel;
Lock recordingsLock = new ReentrantLock();
public RecordingsTab(String title, Recorder recorder, Config config, List<Site> sites) {
super(title);
@ -114,6 +119,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
BorderPane.setMargin(scrollPane, new Insets(5));
table.setEditable(false);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
TableColumn<JavaFxRecording, String> name = new TableColumn<>("Model");
name.setPrefWidth(200);
name.setCellValueFactory(new PropertyValueFactory<JavaFxRecording, String>("modelName"));
@ -162,14 +168,12 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
setStyle(null);
} else {
setText(StringUtil.formatSize(sizeInByte));
setStyle("-fx-alignment: CENTER-RIGHT;");
if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
int row = this.getTableRow().getIndex();
JavaFxRecording rec = tableViewProperty().get().getItems().get(row);
if(!rec.valueChanged() && rec.getStatus() == STATUS.RECORDING) {
setStyle("-fx-alignment: CENTER-RIGHT; -fx-background-color: red");
} else {
setStyle("-fx-alignment: CENTER-RIGHT;");
//setStyle(null);
}
}
}
@ -182,9 +186,9 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
table.getColumns().addAll(name, date, status, progress, size);
table.setItems(observableRecordings);
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
Recording recording = table.getSelectionModel().getSelectedItem();
if(recording != null) {
popup = createContextMenu(recording);
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
if(recordings != null && !recordings.isEmpty()) {
popup = createContextMenu(recordings);
if(!popup.getItems().isEmpty()) {
popup.show(table, event.getScreenX(), event.getScreenY());
}
@ -205,13 +209,15 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
}
});
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
JavaFxRecording recording = table.getSelectionModel().getSelectedItem();
if (recording != null) {
List<JavaFxRecording> recordings = table.getSelectionModel().getSelectedItems();
if (recordings != null && !recordings.isEmpty()) {
if (event.getCode() == KeyCode.DELETE) {
delete(recording);
if(recordings.size() > 1 || recordings.get(0).getStatus() == STATUS.FINISHED) {
delete(recordings);
}
} else if (event.getCode() == KeyCode.ENTER) {
if(recording.getStatus() == STATUS.FINISHED) {
play(recording);
if(recordings.get(0).getStatus() == STATUS.FINISHED) {
play(recordings.get(0));
}
}
}
@ -275,23 +281,28 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
return;
}
for (Iterator<JavaFxRecording> iterator = observableRecordings.iterator(); iterator.hasNext();) {
JavaFxRecording old = iterator.next();
if (!recordings.contains(old)) {
// remove deleted recordings
iterator.remove();
recordingsLock.lock();
try {
for (Iterator<JavaFxRecording> iterator = observableRecordings.iterator(); iterator.hasNext();) {
JavaFxRecording old = iterator.next();
if (!recordings.contains(old)) {
// remove deleted recordings
iterator.remove();
}
}
}
for (JavaFxRecording recording : recordings) {
if (!observableRecordings.contains(recording)) {
// add new recordings
observableRecordings.add(recording);
} else {
// update existing ones
int index = observableRecordings.indexOf(recording);
JavaFxRecording old = observableRecordings.get(index);
old.update(recording);
for (JavaFxRecording recording : recordings) {
if (!observableRecordings.contains(recording)) {
// add new recordings
observableRecordings.add(recording);
} else {
// update existing ones
int index = observableRecordings.indexOf(recording);
JavaFxRecording old = observableRecordings.get(index);
old.update(recording);
}
}
} finally {
recordingsLock.unlock();
}
table.sort();
}
@ -316,6 +327,10 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
try {
spaceTotal = recorder.getTotalSpaceBytes();
spaceFree = recorder.getFreeSpaceBytes();
Platform.runLater(() -> spaceLeft.setTooltip(new Tooltip()));
} catch (NoSuchFileException e) {
// recordings dir does not exist
Platform.runLater(() -> spaceLeft.setTooltip(new Tooltip("Recordings directory does not exist")));
} catch (IOException e) {
LOG.error("Couldn't update free space", e);
}
@ -351,7 +366,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
}
}
private ContextMenu createContextMenu(Recording recording) {
private ContextMenu createContextMenu(List<JavaFxRecording> recordings) {
ContextMenu contextMenu = new ContextMenu();
contextMenu.setHideOnEscape(true);
contextMenu.setAutoHide(true);
@ -359,9 +374,9 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
MenuItem openInPlayer = new MenuItem("Open in Player");
openInPlayer.setOnAction((e) -> {
play(recording);
play(recordings.get(0));
});
if(recording.getStatus() == STATUS.FINISHED || Config.getInstance().getSettings().localRecording) {
if(recordings.get(0).getStatus() == STATUS.FINISHED || Config.getInstance().getSettings().localRecording) {
contextMenu.getItems().add(openInPlayer);
}
@ -381,16 +396,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
MenuItem deleteRecording = new MenuItem("Delete");
deleteRecording.setOnAction((e) -> {
delete(recording);
delete(recordings);
});
if(recording.getStatus() == STATUS.FINISHED) {
if(recordings.get(0).getStatus() == STATUS.FINISHED || recordings.size() > 1) {
contextMenu.getItems().add(deleteRecording);
}
MenuItem openDir = new MenuItem("Open directory");
openDir.setOnAction((e) -> {
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
String path = recording.getPath();
String path = recordings.get(0).getPath();
File tsFile = new File(recordingsDir, path);
new Thread(() -> {
DesktopIntegration.open(tsFile.getParent());
@ -403,16 +418,22 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
MenuItem downloadRecording = new MenuItem("Download");
downloadRecording.setOnAction((e) -> {
try {
download(recording);
download(recordings.get(0));
} catch (IOException | ParseException | PlaylistException e1) {
showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e1);
LOG.error("Error while downloading recording", e1);
}
});
if (!Config.getInstance().getSettings().localRecording && recording.getStatus() == STATUS.FINISHED) {
if (!Config.getInstance().getSettings().localRecording && recordings.get(0).getStatus() == STATUS.FINISHED) {
contextMenu.getItems().add(downloadRecording);
}
if(recordings.size() > 1) {
openInPlayer.setDisable(true);
openDir.setDisable(true);
downloadRecording.setDisable(true);
}
return contextMenu;
}
@ -477,80 +498,6 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
}
}
// private void download(Recording recording) throws IOException, ParseException, PlaylistException {
// String filename = recording.getPath().replaceAll("/", "-") + ".ts";
// FileChooser chooser = new FileChooser();
// chooser.setInitialFileName(filename);
// if(config.getSettings().lastDownloadDir != null && !config.getSettings().lastDownloadDir.equals("")) {
// File dir = new File(config.getSettings().lastDownloadDir);
// while(!dir.exists()) {
// dir = dir.getParentFile();
// }
// chooser.setInitialDirectory(dir);
// }
// File target = chooser.showSaveDialog(null);
// if(target != null) {
// config.getSettings().lastDownloadDir = target.getParent();
// String hlsBase = "http://" + config.getSettings().httpServer + ":" + config.getSettings().httpPort + "/hls";
// URL url = new URL(hlsBase + "/" + recording.getPath() + "/playlist.m3u8");
// LOG.info("Downloading {}", recording.getPath());
//
// PlaylistParser parser = new PlaylistParser(url.openStream(), Format.EXT_M3U, Encoding.UTF_8);
// Playlist playlist = parser.parse();
// MediaPlaylist mediaPlaylist = playlist.getMediaPlaylist();
// List<TrackData> tracks = mediaPlaylist.getTracks();
// List<String> segmentUris = new ArrayList<>();
// for (TrackData trackData : tracks) {
// String segmentUri = hlsBase + "/" + recording.getPath() + "/" + trackData.getUri();
// segmentUris.add(segmentUri);
// }
//
// Thread t = new Thread() {
// @Override
// public void run() {
// try(FileOutputStream fos = new FileOutputStream(target)) {
// for (int i = 0; i < segmentUris.size(); i++) {
// URL segment = new URL(segmentUris.get(i));
// InputStream in = segment.openStream();
// byte[] b = new byte[1024];
// int length = -1;
// while( (length = in.read(b)) >= 0 ) {
// fos.write(b, 0, length);
// }
// in.close();
// int progress = (int) (i * 100.0 / segmentUris.size());
// Platform.runLater(new Runnable() {
// @Override
// public void run() {
// recording.setStatus(STATUS.DOWNLOADING);
// recording.setProgress(progress);
// }
// });
// }
//
// } catch (FileNotFoundException e) {
// showErrorDialog("Error while downloading recording", "The target file couldn't be created", e);
// LOG.error("Error while downloading recording", e);
// } catch (IOException e) {
// showErrorDialog("Error while downloading recording", "The recording could not be downloaded", e);
// LOG.error("Error while downloading recording", e);
// } finally {
// Platform.runLater(new Runnable() {
// @Override
// public void run() {
// recording.setStatus(STATUS.FINISHED);
// recording.setProgress(-1);
// }
// });
// }
// }
// };
// t.setDaemon(true);
// t.setName("Download Thread " + recording.getPath());
// t.start();
// }
// }
private void showErrorDialog(final String title, final String msg, final Exception e) {
Platform.runLater(new Runnable() {
@Override
@ -571,7 +518,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
@Override
public void run() {
boolean started = Player.play(recording);
if(started) {
if(started && Config.getInstance().getSettings().showPlayerStarting) {
Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500));
}
}
@ -583,7 +530,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
@Override
public void run() {
boolean started = Player.play(url);
if(started) {
if(started && Config.getInstance().getSettings().showPlayerStarting) {
Platform.runLater(() -> Toast.makeText(getTabPane().getScene(), "Starting Player", 2000, 500, 500));
}
}
@ -592,12 +539,16 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
}
private void delete(Recording r) {
if(r.getStatus() != STATUS.FINISHED) {
return;
}
private void delete(List<JavaFxRecording> recordings) {
table.setCursor(Cursor.WAIT);
String msg = "Delete " + r.getModelName() + "/" + r.getStartDate() + " for good?";
String msg;
if(recordings.size() > 1) {
msg = "Delete " + recordings.size() + " recordings for good?";
} else {
Recording r = recordings.get(0);
msg = "Delete " + r.getModelName() + "/" + r.getStartDate() + " for good?";
}
AutosizeAlert confirm = new AutosizeAlert(AlertType.CONFIRMATION, msg, YES, NO);
confirm.setTitle("Delete recording?");
confirm.setHeaderText(msg);
@ -607,14 +558,26 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
Thread deleteThread = new Thread() {
@Override
public void run() {
recordingsLock.lock();
try {
recorder.delete(r);
Platform.runLater(() -> observableRecordings.remove(r));
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Error while deleting recording", e1);
showErrorDialog("Error while deleting recording", "Recording not deleted", e1);
List<Recording> deleted = new ArrayList<>();
for (Iterator<JavaFxRecording> iterator = recordings.iterator(); iterator.hasNext();) {
JavaFxRecording r = iterator.next();
if(r.getStatus() != STATUS.FINISHED) {
continue;
}
try {
recorder.delete(r);
deleted.add(r);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Error while deleting recording", e1);
showErrorDialog("Error while deleting recording", "Recording not deleted", e1);
}
}
observableRecordings.removeAll(deleted);
} finally {
table.setCursor(Cursor.DEFAULT);
recordingsLock.unlock();
Platform.runLater(() -> table.setCursor(Cursor.DEFAULT));
}
}
};

View File

@ -72,6 +72,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private CheckBox chooseStreamQuality = new CheckBox();
private CheckBox multiplePlayers = new CheckBox();
private CheckBox updateThumbnails = new CheckBox();
private CheckBox showPlayerStarting = new CheckBox();
private RadioButton recordLocal;
private RadioButton recordRemote;
private ToggleGroup recordLocation;
@ -409,6 +410,18 @@ public class SettingsTab extends Tab implements TabSelectionListener {
GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
layout.add(multiplePlayers, 1, row++);
l = new Label("Show \"Player Starting\" Message");
layout.add(l, 0, row);
showPlayerStarting.setSelected(Config.getInstance().getSettings().showPlayerStarting);
showPlayerStarting.setOnAction((e) -> {
Config.getInstance().getSettings().showPlayerStarting = showPlayerStarting.isSelected();
saveConfig();
});
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
GridPane.setMargin(showPlayerStarting, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
layout.add(showPlayerStarting, 1, row++);
l = new Label("Display stream resolution in overview");
layout.add(l, 0, row);
loadResolution = new CheckBox();

View File

@ -1,5 +1,6 @@
package ctbrec.ui;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@ -15,7 +16,9 @@ public class StreamSourceSelectionDialog {
Task<List<StreamSource>> selectStreamSource = new Task<List<StreamSource>>() {
@Override
protected List<StreamSource> call() throws Exception {
return model.getStreamSources();
List<StreamSource> sources = model.getStreamSources();
Collections.sort(sources);
return sources;
}
};
selectStreamSource.setOnSucceeded((e) -> {

View File

@ -5,6 +5,8 @@ import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
import org.slf4j.Logger;
@ -14,6 +16,7 @@ import com.iheartradio.m3u8.ParseException;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.io.HttpException;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Toast;
import javafx.animation.FadeTransition;
@ -43,6 +46,8 @@ import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.util.Duration;
import okhttp3.Request;
import okhttp3.Response;
public class ThumbCell extends StackPane {
@ -74,6 +79,7 @@ public class ThumbCell extends StackPane {
private ObservableList<Node> thumbCellList;
private boolean mouseHovering = false;
private boolean recording = false;
private static ExecutorService imageLoadingThreadPool = Executors.newFixedThreadPool(30);
public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder) {
this.thumbCellList = parent.grid.getChildren();
@ -110,7 +116,7 @@ public class ThumbCell extends StackPane {
StackPane.setMargin(resolutionBackground, new Insets(2));
getChildren().add(resolutionBackground);
name = new Text(model.getName());
name = new Text(model.getDisplayName());
name.setFill(Color.WHITE);
name.setFont(new Font("Sansserif", 16));
name.setTextAlignment(TextAlignment.CENTER);
@ -267,18 +273,35 @@ public class ThumbCell extends StackPane {
if(!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
boolean updateThumbs = Config.getInstance().getSettings().updateThumbnails;
if(updateThumbs || iv.getImage() == null) {
Image img = new Image(url, true);
// wait for the image to load, otherwise the ImageView replaces the current image with an "empty" image,
// which causes to show the grey background until the image is loaded
img.progressProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if(newValue.doubleValue() == 1.0) {
//imgAspectRatio = img.getHeight() / img.getWidth();
iv.setImage(img);
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
imageLoadingThreadPool.submit(() -> {
Request req = new Request.Builder()
.url(url)
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.build();
try(Response resp = CamrecApplication.httpClient.execute(req)) {
if(resp.isSuccessful()) {
Image img = new Image(resp.body().byteStream());
if(img.progressProperty().get() == 1.0) {
Platform.runLater(() -> {
iv.setImage(img);
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
});
} else {
img.progressProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if(newValue.doubleValue() == 1.0) {
iv.setImage(img);
setThumbWidth(Config.getInstance().getSettings().thumbWidth);
}
}
});
}
} else {
throw new HttpException(resp.code(), resp.message());
}
} catch (IOException e) {
LOG.error("Error loading image", e);
}
});
}
@ -308,7 +331,7 @@ public class ThumbCell extends StackPane {
boolean started = Player.play(model);
Platform.runLater(() -> {
setCursor(Cursor.DEFAULT);
if (started) {
if (started && Config.getInstance().getSettings().showPlayerStarting) {
Toast.makeText(getScene(), "Starting Player", 2000, 500, 500);
}
});

View File

@ -753,6 +753,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
String[] tokens = filter.split(" ");
StringBuilder searchTextBuilder = new StringBuilder(m.getName());
searchTextBuilder.append(' ');
searchTextBuilder.append(m.getDisplayName());
searchTextBuilder.append(' ');
for (String tag : m.getTags()) {
searchTextBuilder.append(tag).append(' ');
}

View File

@ -38,6 +38,7 @@ import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.Player;
@ -86,7 +87,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
new Thread(() -> {
Platform.runLater(() -> {
boolean started = Player.play(model);
if(started) {
if(started && Config.getInstance().getSettings().showPlayerStarting) {
Toast.makeText(getScene(), "Starting Player", 2000, 500, 500);
}
setCursor(Cursor.DEFAULT);
@ -230,7 +231,7 @@ public class SearchPopoverTreeList extends PopoverTreeList<Model> implements Pop
} else {
follow.setVisible(model.getSite().supportsFollow());
title.setVisible(true);
title.setText(model.getName());
title.setText(model.getDisplayName());
this.model = model;
URL anonymousPng = getClass().getResource("/anonymous.png");
String previewUrl = Optional.ofNullable(model.getPreview()).orElse(anonymousPng.toString());

View File

@ -70,6 +70,9 @@ public class BongaCamsUpdateService extends PaginatedScheduledService {
model.setOnlineState("offline");
}
model.setPreview("https:" + m.getString("thumb_image"));
if(m.has("display_name")) {
model.setDisplayName(m.getString("display_name"));
}
models.add(model);
}
}

View File

@ -62,6 +62,7 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
if(result.has("tpl")) {
JSONArray tpl = result.getJSONArray("tpl");
String name = tpl.getString(0);
String displayName = tpl.getString(1);
// int connections = tpl.getInt(2);
String streamName = tpl.getString(5);
String tsize = tpl.getString(6);
@ -77,11 +78,11 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
JSONArray edgeServers = result.getJSONArray("edge_servers");
model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8");
}
model.setDisplayName(displayName);
models.add(model);
} else {
String name = result.getString("username");
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
if(result.has("server_prefix")) {
String serverPrefix = result.getString("server_prefix");
String streamName = result.getString("stream_name");
@ -91,6 +92,10 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
model.setOnlineState(result.getString("status"));
}
if(result.has("display_name")) {
model.setDisplayName(result.getString("display_name"));
}
if(result.has("edge_servers")) {
JSONArray edgeServers = result.getJSONArray("edge_servers");
model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8");

View File

@ -42,14 +42,29 @@ public class ChaturbateConfigUi extends AbstractConfigUI {
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, 1);
layout.add(new Label("Chaturbate Base URL"), 0, 2);
TextField baseUrl = new TextField();
baseUrl.setText(Config.getInstance().getSettings().chaturbateBaseUrl);
baseUrl.textProperty().addListener((ob, o, n) -> {
Config.getInstance().getSettings().chaturbateBaseUrl = baseUrl.getText();
save();
});
GridPane.setFillWidth(baseUrl, true);
GridPane.setHgrow(baseUrl, Priority.ALWAYS);
GridPane.setColumnSpan(baseUrl, 2);
layout.add(baseUrl, 1, 2);
Button createAccount = new Button("Create new Account");
createAccount.setOnAction((e) -> DesktopIntegration.open(Chaturbate.REGISTRATION_LINK));
layout.add(createAccount, 1, 2);
layout.add(createAccount, 1, 3);
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(baseUrl, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
username.setPrefWidth(300);
return layout;
}
}

View File

@ -1,7 +1,5 @@
package ctbrec.ui.sites.chaturbate;
import static ctbrec.sites.chaturbate.Chaturbate.*;
import java.util.ArrayList;
import java.util.List;
@ -21,17 +19,17 @@ public class ChaturbateTabProvider extends TabProvider {
public ChaturbateTabProvider(Chaturbate chaturbate) {
this.chaturbate = chaturbate;
this.recorder = chaturbate.getRecorder();
this.followedTab = new ChaturbateFollowedTab("Followed", BASE_URI + "/followed-cams/", chaturbate);
this.followedTab = new ChaturbateFollowedTab("Followed", chaturbate.getBaseUrl() + "/followed-cams/", chaturbate);
}
@Override
public List<Tab> getTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>();
tabs.add(createTab("Featured", BASE_URI + "/"));
tabs.add(createTab("Female", BASE_URI + "/female-cams/"));
tabs.add(createTab("Male", BASE_URI + "/male-cams/"));
tabs.add(createTab("Couples", BASE_URI + "/couple-cams/"));
tabs.add(createTab("Trans", BASE_URI + "/trans-cams/"));
tabs.add(createTab("Featured", chaturbate.getBaseUrl() + "/"));
tabs.add(createTab("Female", chaturbate.getBaseUrl() + "/female-cams/"));
tabs.add(createTab("Male", chaturbate.getBaseUrl() + "/male-cams/"));
tabs.add(createTab("Couples", chaturbate.getBaseUrl() + "/couple-cams/"));
tabs.add(createTab("Trans", chaturbate.getBaseUrl() + "/trans-cams/"));
followedTab.setScene(scene);
followedTab.setRecorder(recorder);
tabs.add(followedTab);

View File

@ -14,6 +14,7 @@ public abstract class AbstractModel implements Model {
private String url;
private String name;
private String displayName;
private String preview;
private String description;
private List<String> tags = new ArrayList<>();
@ -46,6 +47,20 @@ public abstract class AbstractModel implements Model {
this.name = name;
}
@Override
public String getDisplayName() {
if(displayName != null) {
return displayName;
} else {
return getName();
}
}
@Override
public void setDisplayName(String name) {
this.displayName = name;
}
@Override
public String getPreview() {
return preview;

View File

@ -7,6 +7,7 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@ -46,7 +47,6 @@ public class Config {
} else {
filename = "settings.json";
}
load();
}
private void load() throws FileNotFoundException, IOException {
@ -61,6 +61,13 @@ public class Config {
BufferedSource source = buffer.readFrom(fin);
settings = adapter.fromJson(source);
settings.httpTimeout = Math.max(settings.httpTimeout, 10_000);
} catch(Throwable e) {
settings = OS.getDefaultSettings();
for (Site site : sites) {
site.setEnabled(!settings.disabledSites.contains(site.getName()));
}
makeBackup(configFile);
throw e;
}
} else {
LOG.error("Config file does not exist. Falling back to default values.");
@ -71,9 +78,22 @@ public class Config {
}
}
private void makeBackup(File source) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
String timestamp = sdf.format(new Date());
String backup = source.getName() + '.' + timestamp;
File target = new File(source.getParentFile(), backup);
Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch(Throwable e) {
LOG.error("Couldn't create backup of settings file", e);
}
}
public static synchronized void init(List<Site> sites) throws FileNotFoundException, IOException {
if(instance == null) {
instance = new Config(sites);
instance.load();
}
}

View File

@ -15,6 +15,8 @@ import ctbrec.sites.Site;
public interface Model {
public String getUrl();
public void setUrl(String url);
public String getDisplayName();
public void setDisplayName(String name);
public String getName();
public void setName(String name);
public String getPreview();

View File

@ -30,6 +30,7 @@ public class Settings {
}
public boolean singlePlayer = true;
public boolean showPlayerStarting = false;
public boolean localRecording = true;
public int httpPort = 8080;
public int httpTimeout = 10000;
@ -42,6 +43,7 @@ public class Settings {
public String postProcessing = "";
public String username = ""; // chaturbate username TODO maybe rename this onetime
public String password = ""; // chaturbate password TODO maybe rename this onetime
public String chaturbateBaseUrl = "https://chaturbate.com";
public String bongaUsername = "";
public String bongaPassword = "";
public String mfcUsername = "";

View File

@ -431,7 +431,8 @@ public class LocalRecorder implements Recorder {
running = true;
while (running) {
Instant begin = Instant.now();
for (Model model : getModelsRecording()) {
List<Model> models = getModelsRecording();
for (Model model : models) {
try {
boolean isOnline = model.isOnline(IGNORE_CACHE);
LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline"));
@ -450,6 +451,7 @@ public class LocalRecorder implements Recorder {
}
Instant end = Instant.now();
Duration timeCheckTook = Duration.between(begin, end);
LOG.trace("Online check for {} models took {} seconds", models.size(), timeCheckTook.getSeconds());
long sleepTime = Config.getInstance().getSettings().onlineCheckIntervalInSecs;
if(timeCheckTook.getSeconds() < sleepTime) {
@ -710,13 +712,20 @@ public class LocalRecorder implements Recorder {
@Override
public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
LOG.debug("Switching stream source to index {} for model {}", model.getStreamUrlIndex(), model.getName());
Download download = recordingProcesses.get(model);
if(download != null) {
stopRecordingProcess(model);
if (models.contains(model)) {
int index = models.indexOf(model);
models.get(index).setStreamUrlIndex(model.getStreamUrlIndex());
config.save();
LOG.debug("Switching stream source to index {} for model {}", model.getStreamUrlIndex(), model.getName());
Download download = recordingProcesses.get(model);
if(download != null) {
stopRecordingProcess(model);
}
tryRestartRecording(model);
} else {
LOG.warn("Couldn't switch stream source for model {}. Not found in list", model.getName());
return;
}
tryRestartRecording(model);
config.save();
}
@Override
@ -752,13 +761,17 @@ public class LocalRecorder implements Recorder {
int index = models.indexOf(model);
Model m = models.get(index);
m.setSuspended(false);
startRecordingProcess(m);
if(m.isOnline()) {
startRecordingProcess(m);
}
model.setSuspended(false);
config.save();
} else {
LOG.warn("Couldn't resume model {}. Not found in list", model.getName());
return;
}
} catch (ExecutionException | InterruptedException e) {
LOG.error("Couldn't check, if model {} is online", model.getName());
} finally {
lock.unlock();
}
@ -787,6 +800,10 @@ public class LocalRecorder implements Recorder {
private boolean enoughSpaceForRecording() throws IOException {
long minimum = config.getSettings().minimumSpaceLeftInBytes;
return getFreeSpaceBytes() > minimum;
if(minimum == 0) { // 0 means don't check
return true;
} else {
return getFreeSpaceBytes() > minimum;
}
}
}

View File

@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.PlaylistWriter;
@ -190,7 +191,7 @@ public class PlaylistGenerator {
public void validate(File recDir) throws IOException, ParseException, PlaylistException {
File playlist = new File(recDir, "playlist.m3u8");
if(playlist.exists()) {
PlaylistParser playlistParser = new PlaylistParser(new FileInputStream(playlist), Format.EXT_M3U, Encoding.UTF_8);
PlaylistParser playlistParser = new PlaylistParser(new FileInputStream(playlist), Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist m3u = playlistParser.parse();
MediaPlaylist mediaPlaylist = m3u.getMediaPlaylist();
int playlistSize = mediaPlaylist.getTracks().size();

View File

@ -83,12 +83,18 @@ public abstract class AbstractHlsDownload implements Download {
String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException {
LOG.debug("{} stream idx: {}", model.getName(), model.getStreamUrlIndex());
List<StreamSource> streamSources = model.getStreamSources();
Collections.sort(streamSources);
for (StreamSource streamSource : streamSources) {
LOG.debug("{} src {}", model.getName(), streamSource);
}
String url = null;
if(model.getStreamUrlIndex() >= 0 && model.getStreamUrlIndex() < streamSources.size()) {
// TODO don't use the index, but the bandwidth. if the bandwidth does not match, take the closest one
LOG.debug("{} selected {}", model.getName(), streamSources.get(model.getStreamUrlIndex()));
url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl();
} else {
Collections.sort(streamSources);
// filter out stream resolutions, which are too high
int maxRes = Config.getInstance().getSettings().maximumResolution;
if(maxRes > 0) {
@ -103,9 +109,11 @@ public abstract class AbstractHlsDownload implements Download {
if(streamSources.isEmpty()) {
throw new ExecutionException(new RuntimeException("No stream left in playlist"));
} else {
LOG.debug("{} selected {}", model.getName(), streamSources.get(streamSources.size()-1));
url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl();
}
}
LOG.debug("Segment playlist url {}", url);
return url;
}

View File

@ -46,4 +46,9 @@ public abstract class AbstractSite implements Site {
public boolean searchRequiresLogin() {
return false;
}
@Override
public Model createModelFromUrl(String url) {
return null;
}
}

View File

@ -29,4 +29,5 @@ public interface Site {
public boolean isEnabled();
public List<Model> search(String q) throws IOException, InterruptedException;
public boolean searchRequiresLogin();
public Model createModelFromUrl(String url);
}

View File

@ -5,6 +5,8 @@ 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;
@ -160,6 +162,9 @@ public class BongaCams extends AbstractSite {
if(thumb != null) {
model.setPreview("https:" + thumb);
}
if(result.has("display_name")) {
model.setDisplayName(result.getString("display_name"));
}
models.add(model);
}
return models;
@ -184,4 +189,14 @@ public class BongaCams extends AbstractSite {
return username != null && !username.trim().isEmpty();
}
@Override
public Model createModelFromUrl(String url) {
Matcher m = Pattern.compile("https?://.*?bongacams.com(?:/profile)?/([^/]*?)/?").matcher(url);
if(m.matches()) {
String modelName = m.group(1);
return createModel(modelName);
} else {
return super.createModelFromUrl(url);
}
}
}

View File

@ -14,6 +14,7 @@ import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MasterPlaylist;
@ -101,7 +102,7 @@ public class BongaCamsModel extends AbstractModel {
try(Response response = site.getHttpClient().execute(req)) {
if(response.isSuccessful()) {
InputStream inputStream = response.body().byteStream();
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse();
MasterPlaylist master = playlist.getMasterPlaylist();
streamSources.clear();

View File

@ -4,6 +4,8 @@ import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONObject;
@ -154,4 +156,15 @@ public class Cam4 extends AbstractSite {
String username = Config.getInstance().getSettings().cam4Username;
return username != null && !username.trim().isEmpty();
}
@Override
public Model createModelFromUrl(String url) {
Matcher m = Pattern.compile("https?://(?:www\\.)?cam4(?:.*?).com/([^/]*?)/?").matcher(url);
if(m.matches()) {
String modelName = m.group(1);
return createModel(modelName);
} else {
return super.createModelFromUrl(url);
}
}
}

View File

@ -17,6 +17,7 @@ import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MasterPlaylist;
@ -129,7 +130,7 @@ public class Cam4Model extends AbstractModel {
Response response = site.getHttpClient().execute(req);
try {
InputStream inputStream = response.body().byteStream();
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse();
MasterPlaylist master = playlist.getMasterPlaylist();
return master;

View File

@ -5,6 +5,8 @@ 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;
@ -138,6 +140,9 @@ public class Camsoda extends AbstractSite {
if(thumb != null) {
model.setPreview("https:" + thumb);
}
if(result.has("display_name")) {
model.setDisplayName(result.getString("display_name"));
}
models.add(model);
}
return models;
@ -161,4 +166,15 @@ public class Camsoda extends AbstractSite {
String username = Config.getInstance().getSettings().camsodaUsername;
return username != null && !username.trim().isEmpty();
}
@Override
public Model createModelFromUrl(String url) {
Matcher m = Pattern.compile("https?://(?:www\\.)?camsoda.com/([^/]*?)/?").matcher(url);
if(m.matches()) {
String modelName = m.group(1);
return createModel(modelName);
} else {
return super.createModelFromUrl(url);
}
}
}

View File

@ -17,6 +17,7 @@ import com.google.common.cache.CacheBuilder;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MasterPlaylist;
@ -112,7 +113,7 @@ public class CamsodaModel extends AbstractModel {
Response response = site.getHttpClient().execute(req);
try {
InputStream inputStream = response.body().byteStream();
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse();
MasterPlaylist master = playlist.getMasterPlaylist();
PlaylistData playlistData = master.getPlaylists().get(0);

View File

@ -10,6 +10,8 @@ import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -20,6 +22,7 @@ import com.google.common.cache.LoadingCache;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MasterPlaylist;
@ -42,14 +45,14 @@ import okhttp3.Response;
public class Chaturbate extends AbstractSite {
private static final transient Logger LOG = LoggerFactory.getLogger(Chaturbate.class);
public static final String BASE_URI = "https://chaturbate.com";
public static final String AFFILIATE_LINK = BASE_URI + "/in/?track=default&tour=grq0&campaign=55vTi";
public static final String REGISTRATION_LINK = BASE_URI + "/in/?track=default&tour=g4pe&campaign=55vTi";
static String baseUrl = "https://chaturbate.com";
public static final String AFFILIATE_LINK = "https://chaturbate.com/in/?track=default&tour=grq0&campaign=55vTi";
public static final String REGISTRATION_LINK = "https://chaturbate.com/in/?track=default&tour=g4pe&campaign=55vTi";
private ChaturbateHttpClient httpClient;
@Override
public void init() throws IOException {
baseUrl = Config.getInstance().getSettings().chaturbateBaseUrl;
}
@Override
@ -59,7 +62,7 @@ public class Chaturbate extends AbstractSite {
@Override
public String getBaseUrl() {
return "https://chaturbate.com";
return baseUrl;
}
@Override
@ -136,7 +139,7 @@ public class Chaturbate extends AbstractSite {
@Override
public List<Model> search(String q) throws IOException, InterruptedException {
String url = BASE_URI + "?keywords=" + URLEncoder.encode(q, "utf-8");
String url = baseUrl + "?keywords=" + URLEncoder.encode(q, "utf-8");
List<Model> result = new ArrayList<>();
// search online models
@ -152,7 +155,7 @@ public class Chaturbate extends AbstractSite {
// since chaturbate does not return offline models, we at least try, if the profile page
// exists for the search string
url = BASE_URI + '/' + q;
url = baseUrl + '/' + q;
req = new Request.Builder()
.url(url)
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
@ -324,7 +327,7 @@ public class Chaturbate extends AbstractSite {
try (Response response = getHttpClient().execute(req)) {
if(response.isSuccessful()) {
InputStream inputStream = response.body().byteStream();
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse();
MasterPlaylist master = playlist.getMasterPlaylist();
return master;
@ -339,4 +342,15 @@ public class Chaturbate extends AbstractSite {
String username = Config.getInstance().getSettings().username;
return username != null && !username.trim().isEmpty();
}
@Override
public Model createModelFromUrl(String url) {
Matcher m = Pattern.compile("https?://.*?chaturbate.com(?:/p)?/([^/]*?)/?").matcher(url);
if(m.matches()) {
String modelName = m.group(1);
return createModel(modelName);
} else {
return super.createModelFromUrl(url);
}
}
}

View File

@ -54,7 +54,7 @@ public class ChaturbateHttpClient extends HttpClient {
try {
Request login = new Request.Builder()
.url(Chaturbate.BASE_URI + "/auth/login/")
.url(Chaturbate.baseUrl + "/auth/login/")
.build();
Response response = client.newCall(login).execute();
String content = response.body().string();
@ -68,8 +68,8 @@ public class ChaturbateHttpClient extends HttpClient {
.add("csrfmiddlewaretoken", token)
.build();
login = new Request.Builder()
.url(Chaturbate.BASE_URI + "/auth/login/")
.header("Referer", Chaturbate.BASE_URI + "/auth/login/")
.url(Chaturbate.baseUrl + "/auth/login/")
.header("Referer", Chaturbate.baseUrl + "/auth/login/")
.post(body)
.build();

View File

@ -1,7 +1,5 @@
package ctbrec.sites.chaturbate;
import static ctbrec.sites.chaturbate.Chaturbate.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@ -113,6 +111,9 @@ public class ChaturbateModel extends AbstractModel {
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
String segmentUri = baseUrl + playlist.getUri();
src.mediaPlaylistUrl = segmentUri;
if(src.mediaPlaylistUrl.contains("?")) {
src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?'));
}
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
sources.add(src);
}
@ -137,9 +138,9 @@ public class ChaturbateModel extends AbstractModel {
String url = null;
if(follow) {
url = BASE_URI + "/follow/follow/" + getName() + "/";
url = getSite().getBaseUrl() + "/follow/follow/" + getName() + "/";
} else {
url = BASE_URI + "/follow/unfollow/" + getName() + "/";
url = getSite().getBaseUrl() + "/follow/unfollow/" + getName() + "/";
}
RequestBody body = RequestBody.create(null, new byte[0]);

View File

@ -2,6 +2,8 @@ package ctbrec.sites.mfc;
import java.io.IOException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jsoup.select.Elements;
@ -122,4 +124,20 @@ public class MyFreeCams extends AbstractSite {
String username = Config.getInstance().getSettings().mfcUsername;
return username != null && !username.trim().isEmpty();
}
@Override
public Model createModelFromUrl(String url) {
String[] patterns = new String[] {
"https?://profiles.myfreecams.com/([^/]*?)",
"https?://(?:www.)?myfreecams.com/#(.*)"
};
for (String pattern : patterns) {
Matcher m = Pattern.compile(pattern).matcher(url);
if(m.matches()) {
String modelName = m.group(1);
return createModel(modelName);
}
}
return super.createModelFromUrl(url);
}
}

View File

@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MasterPlaylist;
@ -98,7 +99,7 @@ public class MyFreeCamsModel extends AbstractModel {
}
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
if(hlsUrl == null) {
if(getHlsUrl() == null) {
throw new IllegalStateException("Stream url unknown");
}
LOG.trace("Loading master playlist {}", hlsUrl);
@ -106,7 +107,7 @@ public class MyFreeCamsModel extends AbstractModel {
try(Response response = site.getHttpClient().execute(req)) {
if(response.isSuccessful()) {
InputStream inputStream = response.body().byteStream();
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse();
MasterPlaylist master = playlist.getMasterPlaylist();
return master;
@ -116,6 +117,14 @@ public class MyFreeCamsModel extends AbstractModel {
}
}
private String getHlsUrl() {
if(hlsUrl == null) {
MyFreeCams mfc = (MyFreeCams) getSite();
mfc.getClient().update(this);
}
return hlsUrl;
}
@Override
public void invalidateCacheEntries() {
resolution = null;