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 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 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(settingsTab); tabPane.getTabs().add(new SettingsTab2(sites, recorder)); 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) (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 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 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> adapter = moshi.adapter(type); List 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); } } }