diff --git a/src/main/java/ctbrec/Settings.java b/src/main/java/ctbrec/Settings.java index f826adb5..4d55d2b4 100644 --- a/src/main/java/ctbrec/Settings.java +++ b/src/main/java/ctbrec/Settings.java @@ -15,4 +15,5 @@ public class Settings { public String password = ""; public String lastDownloadDir = ""; public List models = new ArrayList(); + public boolean determineResolution = false; } diff --git a/src/main/java/ctbrec/recorder/Chaturbate.java b/src/main/java/ctbrec/recorder/Chaturbate.java index 6c9ca18b..ec44e92c 100644 --- a/src/main/java/ctbrec/recorder/Chaturbate.java +++ b/src/main/java/ctbrec/recorder/Chaturbate.java @@ -1,10 +1,20 @@ package ctbrec.recorder; import java.io.IOException; +import java.io.InputStream; +import java.net.URL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.iheartradio.m3u8.Encoding; +import com.iheartradio.m3u8.Format; +import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.PlaylistException; +import com.iheartradio.m3u8.PlaylistParser; +import com.iheartradio.m3u8.data.MasterPlaylist; +import com.iheartradio.m3u8.data.Playlist; +import com.iheartradio.m3u8.data.PlaylistData; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; @@ -13,6 +23,7 @@ import ctbrec.Model; import okhttp3.FormBody; import okhttp3.Request; import okhttp3.RequestBody; +import okhttp3.Response; public class Chaturbate { private static final transient Logger LOG = LoggerFactory.getLogger(Chaturbate.class); @@ -27,11 +38,44 @@ public class Chaturbate { .post(body) .addHeader("X-Requested-With", "XMLHttpRequest") .build(); - String content = client.execute(req).body().string(); - LOG.debug("Raw stream info: {}", content); - Moshi moshi = new Moshi.Builder().build(); - JsonAdapter adapter = moshi.adapter(StreamInfo.class); - StreamInfo streamInfo = adapter.fromJson(content); - return streamInfo; + Response response = client.execute(req); + if(response.isSuccessful()) { + String content = response.body().string(); + LOG.debug("Raw stream info: {}", content); + Moshi moshi = new Moshi.Builder().build(); + JsonAdapter adapter = moshi.adapter(StreamInfo.class); + StreamInfo streamInfo = adapter.fromJson(content); + return streamInfo; + } else { + int code = response.code(); + String message = response.message(); + response.close(); + throw new IOException("Server responded with " + code + " - " + message + " headers: [" + response.headers() + "]"); + } + } + + public static int[] getResolution(Model model, HttpClient client) throws IOException, ParseException, PlaylistException { + int[] res = new int[2]; + StreamInfo streamInfo = getStreamInfo(model, client); + if(!streamInfo.url.startsWith("http")) { + return res; + } + + URL masterUrl = new URL(streamInfo.url); + InputStream inputStream = masterUrl.openStream(); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); + Playlist playlist = parser.parse(); + MasterPlaylist master = playlist.getMasterPlaylist(); + for (PlaylistData playlistData : master.getPlaylists()) { + if(playlistData.hasStreamInfo() && playlistData.getStreamInfo().hasResolution()) { + int h = playlistData.getStreamInfo().getResolution().height; + int w = playlistData.getStreamInfo().getResolution().width; + if(w > res[1]) { + res[0] = w; + res[1] = h; + } + } + } + return res; } } diff --git a/src/main/java/ctbrec/ui/SettingsTab.java b/src/main/java/ctbrec/ui/SettingsTab.java index 1ee69c25..b115b111 100644 --- a/src/main/java/ctbrec/ui/SettingsTab.java +++ b/src/main/java/ctbrec/ui/SettingsTab.java @@ -14,6 +14,7 @@ import javafx.scene.Node; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.RadioButton; @@ -41,6 +42,7 @@ public class SettingsTab extends Tab { private TextField username; private TextField server; private TextField port; + private CheckBox loadResolution; private PasswordField password; private RadioButton recordLocal; private RadioButton recordRemote; @@ -101,6 +103,12 @@ public class SettingsTab extends Tab { GridPane.setColumnSpan(password, 2); layout.add(password, 1, row); + layout.add(new Label("Display stream resolution in overview"), 0, ++row); + loadResolution = new CheckBox(); + loadResolution.setSelected(Config.getInstance().getSettings().determineResolution); + loadResolution.setOnAction((e) -> Config.getInstance().getSettings().determineResolution = loadResolution.isSelected()); + layout.add(loadResolution, 1, row); + layout.add(new Label(), 0, ++row); layout.add(new Label("Record Location"), 0, ++row); diff --git a/src/main/java/ctbrec/ui/ThumbCell.java b/src/main/java/ctbrec/ui/ThumbCell.java index be6f4faf..f83a8e21 100644 --- a/src/main/java/ctbrec/ui/ThumbCell.java +++ b/src/main/java/ctbrec/ui/ThumbCell.java @@ -1,11 +1,17 @@ package ctbrec.ui; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.PlaylistException; + +import ctbrec.Config; import ctbrec.HttpClient; import ctbrec.Model; import ctbrec.recorder.Chaturbate; @@ -54,12 +60,17 @@ public class ThumbCell extends StackPane { private static final int HEIGHT = 135; private static final Duration ANIMATION_DURATION = new Duration(250); + // this acts like a cache, once the stream resolution for a model has been determined, we don't do it again (until ctbrec is restarted) + private static Map resolutions = new HashMap<>(); + private Model model; private ImageView iv; + private Rectangle resolutionBackground; private Rectangle nameBackground; private Rectangle topicBackground; private Text name; private Text topic; + private Text resolutionTag; private Recorder recorder; private Circle recordingIndicator; private FadeTransition recordingAnimation; @@ -102,6 +113,15 @@ public class ThumbCell extends StackPane { StackPane.setAlignment(topicBackground, Pos.TOP_LEFT); getChildren().add(topicBackground); + resolutionBackground = new Rectangle(34, 16); + resolutionBackground.setFill(new Color(0.22, 0.8, 0.29, 1)); + resolutionBackground.setVisible(false); + resolutionBackground.setArcHeight(5); + resolutionBackground.setArcWidth(resolutionBackground.getArcHeight()); + StackPane.setAlignment(resolutionBackground, Pos.TOP_RIGHT); + StackPane.setMargin(resolutionBackground, new Insets(2)); + getChildren().add(resolutionBackground); + name = new Text(model.getName()); name.setFill(Color.WHITE); name.setFont(new Font("Sansserif", 16)); @@ -125,10 +145,17 @@ public class ThumbCell extends StackPane { StackPane.setAlignment(topic, Pos.TOP_CENTER); getChildren().add(topic); + resolutionTag = new Text(); + resolutionTag.setFill(Color.WHITE); + resolutionTag.setVisible(false); + StackPane.setAlignment(resolutionTag, Pos.TOP_RIGHT); + StackPane.setMargin(resolutionTag, new Insets(2, 4, 2, 2)); + getChildren().add(resolutionTag); + recordingIndicator = new Circle(8); recordingIndicator.setFill(colorRecording); StackPane.setMargin(recordingIndicator, new Insets(3)); - StackPane.setAlignment(recordingIndicator, Pos.TOP_RIGHT); + StackPane.setAlignment(recordingIndicator, Pos.TOP_LEFT); getChildren().add(recordingIndicator); recordingAnimation = new FadeTransition(Duration.millis(1000), recordingIndicator); recordingAnimation.setInterpolator(Interpolator.EASE_BOTH); @@ -163,6 +190,50 @@ public class ThumbCell extends StackPane { setPrefSize(WIDTH, HEIGHT); setRecording(recording); + if(Config.getInstance().getSettings().determineResolution) { + determineResolution(); + } + } + + private void determineResolution() { + if(ThumbOverviewTab.resolutionProcessing.contains(model)) { + return; + } + + ThumbOverviewTab.resolutionProcessing.add(model); + int[] res = resolutions.get(model.getName()); + if(res == null) { + ThumbOverviewTab.threadPool.submit(() -> { + try { + Thread.sleep(500); // throttle down, so that we don't do too many requests + int[] resolution = Chaturbate.getResolution(model, client); + resolutions.put(model.getName(), resolution); + if (resolution[1] > 0) { + LOG.trace("Model resolution {} {}x{}", model.getName(), resolution[0], resolution[1]); + LOG.trace("Resolution queue size: {}", ThumbOverviewTab.queue.size()); + final int w = resolution[1]; + Platform.runLater(() -> { + resolutionTag.setText(Integer.toString(w)); + resolutionTag.setVisible(true); + resolutionBackground.setVisible(true); + resolutionBackground.setWidth(resolutionTag.getBoundsInLocal().getWidth() + 4); + }); + } + } catch (IOException | ParseException | PlaylistException | InterruptedException e) { + LOG.error("Coulnd't get resolution for model {}", model, e); + } finally { + ThumbOverviewTab.resolutionProcessing.remove(model); + } + }); + } else { + ThumbOverviewTab.resolutionProcessing.remove(model); + Platform.runLater(() -> { + resolutionTag.setText(Integer.toString(res[1])); + resolutionTag.setVisible(true); + resolutionBackground.setVisible(true); + resolutionBackground.setWidth(resolutionTag.getBoundsInLocal().getWidth() + 4); + }); + } } private void setImage(String url) { @@ -369,6 +440,9 @@ public class ThumbCell extends StackPane { topic.setText(model.getDescription()); setRecording(recorder.isRecording(model)); requestLayout(); + if(Config.getInstance().getSettings().determineResolution) { + determineResolution(); + } } @Override diff --git a/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/src/main/java/ctbrec/ui/ThumbOverviewTab.java index 5b013e73..13860416 100644 --- a/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -5,11 +5,16 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Set; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; @@ -42,6 +47,10 @@ import okhttp3.Response; public class ThumbOverviewTab extends Tab implements TabSelectionListener { private static final transient Logger LOG = LoggerFactory.getLogger(ThumbOverviewTab.class); + static Set resolutionProcessing = Collections.synchronizedSet(new HashSet<>()); + static BlockingQueue queue = new LinkedBlockingQueue<>(); + static ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 10, TimeUnit.MINUTES, queue); + ScheduledService> updateService; Recorder recorder; List filteredThumbCells = Collections.synchronizedList(new ArrayList<>());