ctbrec-5.3.2-experimental/client/src/main/java/ctbrec/ui/CamrecApplication.java

517 lines
20 KiB
Java

package ctbrec.ui;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.Version;
import ctbrec.docs.DocServer;
import ctbrec.event.Event;
import ctbrec.event.EventBusHolder;
import ctbrec.event.EventHandler;
import ctbrec.event.EventHandlerConfiguration;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.ByteUnitFormatter;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.recorder.NextGenLocalRecorder;
import ctbrec.recorder.OnlineMonitor;
import ctbrec.recorder.Recorder;
import ctbrec.recorder.RemoteRecorder;
import ctbrec.sites.Site;
import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.flirt4free.Flirt4Free;
import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.showup.Showup;
import ctbrec.sites.streamate.Streamate;
import ctbrec.sites.stripchat.Stripchat;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.news.NewsTab;
import ctbrec.ui.settings.SettingsTab;
import ctbrec.ui.settings.SettingsTab2;
import ctbrec.ui.tabs.DonateTabFx;
import ctbrec.ui.tabs.HelpTab;
import ctbrec.ui.tabs.RecordedModelsTab;
import ctbrec.ui.tabs.RecordingsTab;
import ctbrec.ui.tabs.SiteTab;
import ctbrec.ui.tabs.TabSelectionListener;
import ctbrec.ui.tabs.UpdateTab;
import javafx.application.Application;
import javafx.application.HostServices;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import okhttp3.Request;
import okhttp3.Response;
public class CamrecApplication extends Application {
static final Logger LOG = LoggerFactory.getLogger(CamrecApplication.class);
private Config config;
private Recorder recorder;
private OnlineMonitor onlineMonitor;
static HostServices hostServices;
private SettingsTab settingsTab;
private BorderPane rootPane = new BorderPane();
private HBox statusBar = new HBox();
private Label statusLabel = new Label();
private TabPane tabPane = new TabPane();
private List<Site> sites = new ArrayList<>();
public static HttpClient httpClient;
public static String title;
private Stage primaryStage;
private RecordedModelsTab modelsTab;
private RecordingsTab recordingsTab;
private int activeRecordings = 0;
private double bytesPerSecond = 0;
@Override
public void start(Stage primaryStage) throws Exception {
this.primaryStage = primaryStage;
logEnvironment();
sites.add(new BongaCams());
sites.add(new Cam4());
sites.add(new Camsoda());
sites.add(new Chaturbate());
sites.add(new Fc2Live());
sites.add(new Flirt4Free());
sites.add(new LiveJasmin());
sites.add(new MyFreeCams());
sites.add(new Showup());
sites.add(new Streamate());
sites.add(new Stripchat());
loadConfig();
registerAlertSystem();
registerActiveRecordingsCounter();
registerBandwidthMeterListener();
createHttpClient();
hostServices = getHostServices();
createRecorder();
startOnlineMonitor();
createGui(primaryStage);
checkForUpdates();
startHelpServer();
}
private void startHelpServer() {
new Thread(() -> {
try {
DocServer.start();
} catch (Exception e) {
LOG.error("Couldn't start documentation server", e);
}
}).start();
}
private void startOnlineMonitor() {
onlineMonitor = new OnlineMonitor(recorder);
onlineMonitor.start();
for (Site site : sites) {
if(site.isEnabled()) {
try {
site.setRecorder(recorder);
site.init();
} catch(Exception e) {
LOG.error("Error while initializing site {}", site.getName(), e);
}
}
}
}
private void logEnvironment() {
LOG.debug("OS:\t{} {}", System.getProperty("os.name"), System.getProperty("os.version"));
LOG.debug("Java:\t{} {} {}", System.getProperty("java.vendor"), System.getProperty("java.vm.name"), System.getProperty("java.version"));
LOG.debug("JavaFX:\t{} ({})", System.getProperty("javafx.version"), System.getProperty("javafx.runtime.version"));
}
private void createGui(Stage primaryStage) throws IOException {
LOG.debug("Creating GUI");
CamrecApplication.title = "CTB Recorder " + getVersion();
primaryStage.setTitle(title);
InputStream icon = getClass().getResourceAsStream("/icon.png");
primaryStage.getIcons().add(new Image(icon));
int windowWidth = Config.getInstance().getSettings().windowWidth;
int windowHeight = Config.getInstance().getSettings().windowHeight;
Scene scene = new Scene(rootPane, windowWidth, windowHeight);
primaryStage.setScene(scene);
rootPane.setCenter(tabPane);
rootPane.setBottom(statusBar);
for (Iterator<Site> iterator = sites.iterator(); iterator.hasNext();) {
Site site = iterator.next();
if(site.isEnabled()) {
SiteTab siteTab = new SiteTab(site, scene);
tabPane.getTabs().add(siteTab);
}
}
modelsTab = new RecordedModelsTab("Recording", recorder, sites);
tabPane.getTabs().add(modelsTab);
recordingsTab = new RecordingsTab("Recordings", recorder, config, sites);
tabPane.getTabs().add(recordingsTab);
settingsTab = new SettingsTab(sites, recorder);
tabPane.getTabs().add(new SettingsTab2(sites, recorder));
tabPane.getTabs().add(settingsTab);
tabPane.getTabs().add(new NewsTab());
tabPane.getTabs().add(new DonateTabFx());
tabPane.getTabs().add(new HelpTab());
switchToStartTab();
writeColorSchemeStyleSheet();
Color base = Color.web(Config.getInstance().getSettings().colorBase);
if(!base.equals(Color.WHITE)) {
loadStyleSheet(primaryStage, "color.css");
}
loadStyleSheet(primaryStage, "style.css");
primaryStage.getScene().getStylesheets().add("/ctbrec/ui/settings/ColorSettingsPane.css");
primaryStage.getScene().getStylesheets().add("/ctbrec/ui/ThumbCell.css");
primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/SearchBox.css");
primaryStage.getScene().getStylesheets().add("/ctbrec/ui/controls/Popover.css");
primaryStage.getScene().getStylesheets().add("/ctbrec/ui/settings/api/Preferences.css");
primaryStage.getScene().widthProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowWidth = newVal.intValue());
primaryStage.getScene().heightProperty()
.addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowHeight = newVal.intValue());
primaryStage.setMaximized(Config.getInstance().getSettings().windowMaximized);
primaryStage.maximizedProperty()
.addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowMaximized = newVal.booleanValue());
Player.scene = primaryStage.getScene();
primaryStage.setX(Config.getInstance().getSettings().windowX);
primaryStage.setY(Config.getInstance().getSettings().windowY);
primaryStage.xProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowX = newVal.intValue());
primaryStage.yProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowY = newVal.intValue());
primaryStage.show();
primaryStage.setOnCloseRequest(createShutdownHandler());
// register changelistener to activate / deactivate tabs, when the user switches between them
tabPane.getSelectionModel().selectedItemProperty().addListener((ChangeListener<Tab>) (ov, from, to) -> {
if (from instanceof TabSelectionListener) {
((TabSelectionListener) from).deselected();
}
if (to instanceof TabSelectionListener) {
((TabSelectionListener) to).selected();
}
});
statusBar.getChildren().add(statusLabel);
HBox.setMargin(statusLabel, new Insets(10, 10, 10, 10));
}
private javafx.event.EventHandler<WindowEvent> createShutdownHandler() {
return e -> {
e.consume();
// check for active downloads
if (recordingsTab.isDownloadRunning()) {
boolean exitAnyway = Dialogs.showConfirmDialog("Shutdown", "Do you want to exit anyway?", "There are downloads running",
primaryStage.getScene());
if (!exitAnyway) {
return;
}
}
Alert shutdownInfo = new AutosizeAlert(Alert.AlertType.INFORMATION, primaryStage.getScene());
shutdownInfo.setTitle("Shutdown");
shutdownInfo.setContentText("Shutting down. Please wait while recordings are finished...");
shutdownInfo.show();
new Thread() {
@Override
public void run() {
modelsTab.saveState();
recordingsTab.saveState();
settingsTab.saveConfig();
onlineMonitor.shutdown();
recorder.shutdown();
for (Site site : sites) {
if(site.isEnabled()) {
site.shutdown();
}
}
try {
Config.getInstance().save();
LOG.info("Shutdown complete. Goodbye!");
Platform.runLater(() -> {
primaryStage.close();
shutdownInfo.close();
Platform.exit();
// This is needed, because OkHttp?! seems to block the shutdown with its writer threads. They are not daemon threads :(
System.exit(0);
});
} catch (IOException e1) {
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, primaryStage.getScene());
alert.setTitle("Error saving settings");
alert.setContentText("Couldn't save settings: " + e1.getLocalizedMessage());
alert.showAndWait();
System.exit(1);
});
}
try {
ExternalBrowser.getInstance().close();
} catch (IOException e) {
// noop
}
}
}.start();
};
}
private void registerAlertSystem() {
new Thread(() -> {
try {
// don't register before 1 minute has passed, because directly after
// the start of ctbrec, an event for every online model would be fired,
// which is annoying as f
Thread.sleep(TimeUnit.MINUTES.toMillis(1));
for (EventHandlerConfiguration eventHandlerConfig : Config.getInstance().getSettings().eventHandlers) {
EventHandler handler = new EventHandler(eventHandlerConfig);
EventBusHolder.register(handler);
LOG.debug("Registered event handler for {} {}", eventHandlerConfig.getEvent(), eventHandlerConfig.getName());
}
LOG.debug("Alert System registered");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.info("Interrupted before alter system has been registered");
}
}).start();
}
private void registerActiveRecordingsCounter() {
EventBusHolder.BUS.register(new Object() {
@Subscribe
public void handleEvent(Event evt) {
if(evt.getType() == Event.Type.MODEL_ONLINE || evt.getType() == Event.Type.MODEL_STATUS_CHANGED || evt.getType() == Event.Type.RECORDING_STATUS_CHANGED) {
try {
List<Model> models = recorder.getCurrentlyRecording();
activeRecordings = models.size();
String windowTitle = activeRecordings > 0 ? "(" + activeRecordings + ") " + title : title;
Platform.runLater(() -> primaryStage.setTitle(windowTitle));
updateStatus();
} catch (Exception e) {
LOG.warn("Couldn't update window title", e);
}
}
}
});
}
private void registerBandwidthMeterListener() {
BandwidthMeter.addListener((bytes, dur) -> {
long seconds = dur.getSeconds();
bytesPerSecond = bytes / (double)seconds;
updateStatus();
});
}
private void updateStatus() {
if (activeRecordings == 0) {
bytesPerSecond = 0;
}
String humanreadable = ByteUnitFormatter.format(bytesPerSecond);
String status = String.format("Recording %s / %s models @ %s/s", activeRecordings, recorder.getModels().size(), humanreadable);
Platform.runLater(() -> statusLabel.setText(status));
}
private void writeColorSchemeStyleSheet() {
File colorCss = new File(Config.getInstance().getConfigDir(), "color.css");
try(FileOutputStream fos = new FileOutputStream(colorCss)) {
String content = ".root {\n" +
" -fx-base: "+Config.getInstance().getSettings().colorBase+";\n" +
" -fx-accent: "+Config.getInstance().getSettings().colorAccent+";\n" +
" -fx-default-button: -fx-accent;\n" +
" -fx-focus-color: -fx-accent;\n" +
" -fx-control-inner-background-alt: derive(-fx-base, 95%);\n" +
"}";
fos.write(content.getBytes("utf-8"));
} catch(Exception e) {
LOG.error("Couldn't write stylesheet for user defined color theme");
}
}
private void loadStyleSheet(Stage primaryStage, String filename) {
File css = new File(Config.getInstance().getConfigDir(), filename);
if(css.exists() && css.isFile()) {
primaryStage.getScene().getStylesheets().add(css.toURI().toString());
}
}
private void switchToStartTab() {
String startTab = Config.getInstance().getSettings().startTab;
if(StringUtil.isNotBlank(startTab)) {
for (Tab tab : tabPane.getTabs()) {
if(Objects.equals(startTab, tab.getText())) {
tabPane.getSelectionModel().select(tab);
break;
}
}
}
if(tabPane.getSelectionModel().getSelectedItem() instanceof TabSelectionListener) {
((TabSelectionListener)tabPane.getSelectionModel().getSelectedItem()).selected();
}
}
private void createRecorder() {
if (config.getSettings().localRecording) {
//recorder = new LocalRecorder(config);
try {
recorder = new NextGenLocalRecorder(config, sites);
} catch (IOException e) {
LOG.error("Couldn't initialize recorder", e);
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, primaryStage.getScene());
alert.setTitle("Whoopsie");
alert.setContentText("Couldn't initialize recorder: " + e.getLocalizedMessage());
alert.showAndWait();
}
} else {
recorder = new RemoteRecorder(config, httpClient, sites);
}
}
private void loadConfig() {
try {
Config.init(sites);
} catch (Exception e) {
LOG.error("Couldn't load settings", e);
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, primaryStage.getScene());
alert.setTitle("Whoopsie");
alert.setContentText("Couldn't load settings. Falling back to defaults. A backup of your settings has been created.");
alert.showAndWait();
}
config = Config.getInstance();
}
private void createHttpClient() {
httpClient = new HttpClient("camrec") {
@Override
public boolean login() throws IOException {
return false;
}
};
}
public static void main(String[] args) {
launch(args);
}
private void checkForUpdates() {
Thread updateCheck = new Thread(() -> {
String url = "https://pastebin.com/raw/mUxtKzyB";
Request request = new Request.Builder().url(url).build();
try (Response response = httpClient.execute(request)) {
String body = response.body().string();
LOG.trace("Version check respone: {}", body);
if (response.isSuccessful()) {
Moshi moshi = new Moshi.Builder().build();
Type type = Types.newParameterizedType(List.class, Release.class);
JsonAdapter<List<Release>> adapter = moshi.adapter(type);
List<Release> releases = adapter.fromJson(body);
Release latest = releases.get(0);
Version latestVersion = latest.getVersion();
Version ctbrecVersion = getVersion();
if (latestVersion.compareTo(ctbrecVersion) > 0) {
LOG.debug("Update available {} < {}", ctbrecVersion, latestVersion);
Platform.runLater(() -> tabPane.getTabs().add(new UpdateTab(latest)));
} else {
LOG.debug("ctbrec is up-to-date {}", ctbrecVersion);
}
} else {
throw new HttpException(response.code(), response.message());
}
} catch (Exception e) {
LOG.warn("Update check failed: {}", e.getMessage());
}
});
updateCheck.setName("Update Check");
updateCheck.setDaemon(true);
updateCheck.start();
}
public static Version getVersion() throws IOException {
if (Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
return Version.of("0.0.0-DEV");
} else {
try (InputStream is = CamrecApplication.class.getClassLoader().getResourceAsStream("version")) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String versionString = reader.readLine();
Version version = Version.of(versionString);
return version;
}
}
}
public static class Release {
private String name;
private String tag_name; // NOSONAR - name pattern is needed by moshi
private String html_url; // NOSONAR - name pattern is needed by moshi
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTagName() {
return tag_name;
}
public void setTagName(String tagName) {
this.tag_name = tagName;
}
public String getHtmlUrl() {
return html_url;
}
public void setHtmlUrl(String htmlUrl) {
this.html_url = htmlUrl;
}
public Version getVersion() {
return Version.of(tag_name);
}
}
}