package de.bisquallisoft.twitch.controller; import de.bisquallisoft.twitch.utils.FxScheduler; import de.bisquallisoft.twitch.Settings; import de.bisquallisoft.twitch.Stream; import de.bisquallisoft.twitch.TwitchApi; import de.bisquallisoft.twitch.utils.IOUtils; import javafx.animation.Timeline; import javafx.application.HostServices; import javafx.application.Platform; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.ListView; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TextField; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Pane; import javafx.scene.web.WebView; import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.stage.Window; import javafx.util.Duration; import org.apache.commons.lang3.StringUtils; import org.controlsfx.control.Notifications; import org.controlsfx.dialog.Dialogs; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.net.SocketTimeoutException; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.ResourceBundle; public class MainController implements Initializable { private static final Logger log = LoggerFactory.getLogger(MainController.class); public static final int NOTIFICATION_DURATION = 5; private Stage primaryStage; @FXML private ListView<Stream> streamList; @FXML private ImageView previewImage; @FXML private ProgressIndicator livestreamerProgess; @FXML private TextField streamLink; @FXML private TextField txtStreamStatus; @FXML private TextField txtViewers; @FXML private Pane imageParent; @FXML private TextField txtGame; @FXML private ImageView imgLogo; @FXML private TextField txtStreamName; private TwitchApi api; private Settings settings = Settings.getInstance(); private Timeline scheduledRefresh; private HostServices hostServices; @Override public void initialize(URL url, ResourceBundle resourceBundle) { previewImage.fitWidthProperty().bind(imageParent.widthProperty()); previewImage.fitHeightProperty().bind(imageParent.heightProperty()); //refresh streams periodically scheduledRefresh = FxScheduler.schedule(Duration.minutes(settings.getUpdateInterval()), this::refreshStreams); //add handler to reschedule stream refreshing when interval changes settings.updateIntervalProperty().addListener((observableValue, ov, nv) -> { scheduledRefresh.stop(); scheduledRefresh = FxScheduler.schedule(Duration.minutes(nv.doubleValue()), this::refreshStreams); }); initTwitchApi(); initStreamList(); Platform.runLater(streamList::requestFocus); } private void initTwitchApi() { if (settings.getAuthToken() == null) { String authToken = authenticate(); settings.setAuthToken(authToken); } api = new TwitchApi(settings.getAuthToken()); if (!api.isAuthValid()) { String authToken = authenticate(); settings.setAuthToken(authToken); api.setAuthToken(authToken); } settings.save(); } private void initStreamList() { streamList.getSelectionModel().selectedItemProperty().addListener((observableValue, ov, nv) -> { if (nv != null) { previewImage.setImage(null); setPreview(nv); } }); streamLink.setOnAction(this::streamLinkAction); try { streamList.getItems().setAll(api.getFollowedStreams()); } catch (SocketTimeoutException e) { log.info("Twitch API timeout"); } if (!streamList.getItems().isEmpty()) { streamList.getSelectionModel().select(0); } } @FXML private void openSettingsWindow(ActionEvent event) { Parent root; try { root = FXMLLoader.load(getClass().getResource("/settings.fxml")); Stage stage = new Stage(); stage.initModality(Modality.APPLICATION_MODAL); stage.initStyle(StageStyle.UTILITY); stage.setTitle("Settings"); stage.setScene(new Scene(root)); stage.setResizable(false); stage.show(); } catch (IOException e) { e.printStackTrace(); } } private void refreshStreams() { log.debug("refreshing streams"); List<Stream> oldStreamList = new ArrayList<>(); streamList.getItems().forEach(oldStreamList::add); Stream selectedItem = streamList.getSelectionModel().getSelectedItem(); try { streamList.getItems().setAll(api.getFollowedStreams()); if (!streamList.getItems().isEmpty()) { if (selectedItem != null && streamList.getItems().contains(selectedItem)) { streamList.getSelectionModel().select(selectedItem); } else { streamList.getSelectionModel().select(0); } showNotifications(oldStreamList); } } catch (SocketTimeoutException e) { log.info("Twitch API timeout"); } } private void showNotifications(List<Stream> oldStreamList) { if (settings.isNotifications()) { streamList.getItems().stream() .filter(Objects::nonNull) .filter(s -> !oldStreamList.contains(s)).forEach(s -> { ImageView img = new ImageView(s.getLogo()); img.setFitHeight(40.0); img.setFitWidth(40.0); Notifications.create() .title(s.getName() + " just went live!") .text(s.getStatus()) .graphic(img) .onAction(e -> launchLivestreamer(s.getUrl())) .hideAfter(Duration.seconds(NOTIFICATION_DURATION)) .darkStyle() .show(); }); } } void streamLinkAction(ActionEvent event) { String text = streamLink.getText(); if (text.startsWith("http") || text.startsWith("www") || text.startsWith("twitch")) { launchLivestreamer(text); } else { launchLivestreamer("twitch.tv/" + text); } streamLink.setText(""); } @FXML void previewClicked(MouseEvent event) { if (event.getButton() == MouseButton.PRIMARY) { launchLivestreamer(streamList.getSelectionModel().getSelectedItem().getUrl()); } else if (event.getButton() == MouseButton.SECONDARY) { final Clipboard clipboard = Clipboard.getSystemClipboard(); final ClipboardContent content = new ClipboardContent(); content.putString(streamList.getSelectionModel().getSelectedItem().getUrl()); clipboard.setContent(content); } } @FXML void streamListClicked(MouseEvent event) { Stream selectedItem = streamList.getSelectionModel().getSelectedItem(); if (selectedItem != null && event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) { launchLivestreamer(selectedItem.getUrl()); } } private void setPreview(Stream stream) { new Thread(() -> { InputStream inputStream = IOUtils.downloadFile(stream.getPreviewImage()); if (inputStream != null) { Image previewImage = new Image(inputStream); Platform.runLater(() -> this.previewImage.setImage(previewImage)); } }).start(); new Thread(() -> { InputStream inputStream = IOUtils.downloadFile(stream.getLogo()); if (inputStream != null) { Image previewImage = new Image(inputStream); Platform.runLater(() -> this.imgLogo.setImage(previewImage)); } }).start(); txtStreamName.setText(stream.getName()); txtStreamStatus.setText(stream.getStatus()); txtViewers.setText(stream.getViewers() + ""); txtGame.setText(stream.getGame()); } @FXML void refreshPressed(ActionEvent event) { refreshStreams(); } private void launchLivestreamer(String url) { livestreamerProgess.setPrefHeight(imageParent.getHeight()); livestreamerProgess.setPrefWidth(imageParent.getWidth()); try { new ProcessBuilder("livestreamer", url, settings.getQuality(), "--no-version-check", "-Q", "--retry-open", "3").start(); livestreamerProgess.setVisible(true); livestreamerProgess.setProgress(-1); FxScheduler.scheduleOnce(Duration.seconds(6), () -> { livestreamerProgess.setProgress(1); livestreamerProgess.setVisible(false); }); } catch (IOException e) { Dialogs.create() .owner(primaryStage) .title("Livestreamer is not in path") .message("Please install livestreamer: livestreamer.readthedocs.org") .showError(); } } public void setHostServices(HostServices hostServices) { this.hostServices = hostServices; } private String authenticate() { WebView webView = new WebView(); webView.getEngine().load(TwitchApi.AUTH_URL); Stage popup = new Stage(); popup.setScene(new Scene((webView))); popup.initModality(Modality.APPLICATION_MODAL); popup.initOwner(primaryStage); StringBuilder result = new StringBuilder(); webView.getEngine().locationProperty().addListener((observableValue, ov, url) -> { if (url.startsWith("http://localhost")) { String token = StringUtils.substringBetween(url, "=", "&"); result.append(token); popup.close(); } }); popup.showAndWait(); return result.toString(); } public void setPrimaryStage(Stage primaryStage) { this.primaryStage = primaryStage; } @FXML void openChat(ActionEvent actionEvent) { String chatLink = "http://www.twitch.tv/%s/chat"; Stream selectedStream = streamList.getSelectionModel().getSelectedItem(); hostServices.showDocument(String.format(chatLink, selectedStream.getName().toLowerCase())); } }