From f45991effced963e1a75821f929ffa449250c4d0 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sun, 31 Dec 2023 14:01:12 +0100 Subject: [PATCH] Added Online / Offline switch on "Favorites" tab for Streamray --- CHANGELOG.md | 10 +- .../AbstractStreamrayUpdateService.java | 90 ++++++++++- .../streamray/StreamrayFavoritesService.java | 140 ++++++++++++++++-- .../streamray/StreamrayFavoritesTab.java | 31 +++- .../streamray/StreamrayUpdateService.java | 90 +++++++++-- .../ui/tabs/PaginatedScheduledService.java | 16 +- 6 files changed, 336 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 959dd25d..d2aea796 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,13 +19,15 @@ * Streamate: - Fixed "Couldn't load model ID" error while adding models by URL or by nickname - - Online/Offline switch on all tabs. Up to 10 000 offline models in each + - Online / Offline switch on all tabs. Up to 10 000 offline models in each category. How do you like it, Elon Musk? - - Added "New Girls" tab and adjusted others. All same models on less tabs. + - Added "New Girls" tab and adjusted others. All same models on less tabs * Stripchat: - - Added "Private" tab. + - Added "Private" tab - CTBRec can record your Spy/Private/Ticket shows (login required) - * + * Streamray: + - Added models tags + - Added Online / Offline switch on "Favorites" tab 5.2.3 ======================== diff --git a/client/src/main/java/ctbrec/ui/sites/streamray/AbstractStreamrayUpdateService.java b/client/src/main/java/ctbrec/ui/sites/streamray/AbstractStreamrayUpdateService.java index 907f82c9..f38a04eb 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamray/AbstractStreamrayUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/streamray/AbstractStreamrayUpdateService.java @@ -2,17 +2,95 @@ package ctbrec.ui.sites.streamray; import ctbrec.sites.streamray.Streamray; import ctbrec.ui.tabs.PaginatedScheduledService; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.json.JSONArray; -abstract class AbstractStreamrayUpdateService extends PaginatedScheduledService { +import java.net.URLEncoder; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; - protected int modelsPerPage = 48; +import static java.nio.charset.StandardCharsets.UTF_8; + +@RequiredArgsConstructor +public abstract class AbstractStreamrayUpdateService extends PaginatedScheduledService { + + @Getter + @Setter + private static JSONArray mapping; + protected static final int MODELS_PER_PAGE = 48; + protected static final String API_URL = "https://beta-api.cams.com/won/compressed/"; protected final Streamray site; - AbstractStreamrayUpdateService(Streamray site) { - this.site = site; + protected String getPreviewURL(String name) { + String lname = name.toLowerCase(); + String url = MessageFormat.format("https://images4.streamray.com/images/streamray/won/jpg/{0}/{1}/{2}_640.jpg", lname.substring(0, 1), lname.substring(lname.length() - 1), lname); + try { + return MessageFormat.format("https://dynimages.securedataimages.com/unsigned/rs:fill:640::0/g:no/plain/{0}@jpg", URLEncoder.encode(url, UTF_8)); + } catch (Exception ex) { + return url; + } } - boolean isLoggedIn() { - return site.isLoggedIn(); + protected List createTags(JSONArray m) { + List tags = new ArrayList<>(); + int idx1 = mappingIndex("gender"); + switch (m.optString(idx1)) { + case "M" -> tags.add("male"); + case "F" -> tags.add("female"); + case "TS" -> tags.add("trans"); + default -> { + // don't add anything + } + } + int idx2 = mappingIndex("ethnicity"); + switch (m.optString(idx2)) { + case "02" -> tags.add("asian"); + case "03" -> tags.add("ebony"); + case "04" -> tags.add("white"); + case "05" -> tags.add("indian"); + case "06" -> tags.add("latina"); + case "07" -> tags.add("middle-eastern"); + default -> { + // don't add anything + } + } + int idx3 = mappingIndex("hair_color"); + switch (m.optString(idx3)) { + case "01" -> tags.add("black-hair"); + case "02" -> tags.add("blonde"); + case "03" -> tags.add("brunette"); + case "06" -> tags.add("redhead"); + default -> { + // don't add anything + } + } + int idx4 = mappingIndex("chat_type"); + switch (m.optString(idx4)) { + case "0" -> tags.add("offline"); + case "1" -> tags.add("public"); + case "2" -> tags.add("nude-show"); + case "3" -> tags.add("private"); + case "4" -> tags.add("exclusive"); + case "6" -> tags.add("ticket-show"); + case "7" -> tags.add("voyeur"); + case "10" -> tags.add("party"); + case "13" -> tags.add("group"); + case "14" -> tags.add("c2c"); + default -> { + // don't add anything + } + } + return tags; + } + + protected int mappingIndex(String s) { + for (var i = 0; i < mapping.length(); i++) { + if (Objects.equals(s, mapping.get(i))) return i; + } + return -1; } } diff --git a/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesService.java b/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesService.java index 93b2087b..da03f3a9 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesService.java +++ b/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesService.java @@ -1,18 +1,36 @@ package ctbrec.ui.sites.streamray; +import ctbrec.Config; import ctbrec.Model; +import ctbrec.StringUtil; +import ctbrec.io.HttpException; import ctbrec.sites.streamray.Streamray; +import ctbrec.sites.streamray.StreamrayHttpClient; import ctbrec.sites.streamray.StreamrayModel; import javafx.concurrent.Task; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import okhttp3.Request; +import okhttp3.Response; +import org.json.JSONArray; +import org.json.JSONObject; import java.io.IOException; -import java.util.Collections; -import java.util.List; +import java.time.Duration; +import java.time.Instant; +import java.util.*; + +import static ctbrec.io.HttpConstants.*; @Slf4j public class StreamrayFavoritesService extends AbstractStreamrayUpdateService { + private List modelsList; + private Instant lastListInfoRequest = Instant.EPOCH; + @Getter + private boolean loggedIn = false; + private boolean showOnline = true; + public StreamrayFavoritesService(Streamray site) { super(site); } @@ -22,20 +40,120 @@ public class StreamrayFavoritesService extends AbstractStreamrayUpdateService { return new Task<>() { @Override public List call() throws IOException { - return getModelList().stream() - .skip((page - 1) * (long) modelsPerPage) - .limit(modelsPerPage) - .map(Model.class::cast) - .toList(); + if (showOnline) { + return getModelList().stream() + .skip((page - 1) * (long) MODELS_PER_PAGE) + .limit(MODELS_PER_PAGE) + .map(Model.class::cast) + .toList(); + } else { + return loadOfflineModelList().stream() + .map(Model.class::cast) + .toList(); + } } }; } private List getModelList() throws IOException { - List models = site.loadModelList(true).stream().filter(StreamrayModel::isFavorite).toList(); - if (models == null) { - models = Collections.emptyList(); + if (Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 30) { + return Objects.nonNull(modelsList) ? modelsList : loadModelList(API_URL); } - return models; + modelsList = loadModelList(API_URL); + return modelsList; + } + + private List loadOfflineModelList() throws IOException { + String url = "https://beta-api.cams.com/favorites/member_favorites/?gender=female"; + int offset = (getPage() - 1) * MODELS_PER_PAGE; + String paginatedUrl = url + "&offset=" + offset + "&limit=" + MODELS_PER_PAGE; + return loadModelList(paginatedUrl); + } + + private List loadModelList(String url) throws IOException { + log.debug("Fetching page {}", url); + lastListInfoRequest = Instant.now(); + StreamrayHttpClient client = (StreamrayHttpClient) site.getHttpClient(); + String token; + if (site.login()) { + loggedIn = true; + token = client.getUserToken(); + } else { + loggedIn = false; + return Collections.emptyList(); + } + Request req = new Request.Builder() + .url(url) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .header(REFERER, site.getBaseUrl() + "/") + .header(ORIGIN, site.getBaseUrl()) + .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) + .header(AUTHORIZATION, "Bearer " + token) + .build(); + try (Response response = client.execute(req)) { + if (response.isSuccessful()) { + List models = new ArrayList<>(); + JSONObject json = new JSONObject(response.body().string()); + if (showOnline) { + if (json.has("models")) { + JSONArray modelNodes = json.getJSONArray("models"); + AbstractStreamrayUpdateService.setMapping(json.getJSONArray("mapping")); + parseModels(modelNodes, models); + } + } else { + if (json.has("results")) { + JSONArray modelNodes = json.getJSONArray("results"); + parseOfflineModels(modelNodes, models); + } + } + return models; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private void parseModels(JSONArray jsonModels, List models) { + int nameIdx = mappingIndex("stream_name"); + int favIdx = mappingIndex("is_favorite"); + for (int i = 0; i < jsonModels.length(); i++) { + JSONArray m = jsonModels.getJSONArray(i); + String name = m.optString(nameIdx); + boolean favorite = m.optBoolean(favIdx); + if (favorite) { + StreamrayModel model = site.createModel(name); + String preview = getPreviewURL(name); + model.setPreview(preview); + model.setTags(createTags(m)); + StringBuilder description = new StringBuilder(); + for (String tag : model.getTags()) { + description.append("#").append(tag).append(" "); + } + model.setDescription(description.toString()); + models.add(model); + } + } + } + + private void parseOfflineModels(JSONArray jsonModels, List models) { + for (int i = 0; i < jsonModels.length(); i++) { + JSONObject m = jsonModels.getJSONObject(i); + String name = m.optString("stream_name"); + if (StringUtil.isBlank(name) || m.optBoolean("is_online")) { + continue; + } + StreamrayModel model = site.createModel(name); + String preview = getPreviewURL(name); + model.setPreview(preview); + model.setDisplayName(m.getString("screen_name")); + model.setOnlineState(Model.State.OFFLINE); + models.add(model); + } + } + + public void setOnline(boolean online) { + showOnline = online; } } diff --git a/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesTab.java b/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesTab.java index dbe1449c..2265564a 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesTab.java +++ b/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayFavoritesTab.java @@ -9,8 +9,11 @@ import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ToggleGroup; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; public class StreamrayFavoritesTab extends ThumbOverviewTab implements FollowedTab { private final Label status; @@ -29,12 +32,38 @@ public class StreamrayFavoritesTab extends ThumbOverviewTab implements FollowedT loginButton.setOnAction(e -> { try { new StreamrayElectronLoginDialog(site.getHttpClient().getCookieJar()); + queue.clear(); updateService.restart(); } catch (Exception ex) { + // fail silently } }); } + @Override + protected void createGui() { + super.createGui(); + addOnlineOfflineSelector(); + } + + private void addOnlineOfflineSelector() { + var group = new ToggleGroup(); + var online = new RadioButton("online"); + online.setToggleGroup(group); + var offline = new RadioButton("offline"); + offline.setToggleGroup(group); + pagination.getChildren().add(online); + pagination.getChildren().add(offline); + HBox.setMargin(online, new Insets(5, 5, 5, 40)); + HBox.setMargin(offline, new Insets(5, 5, 5, 5)); + online.setSelected(true); + group.selectedToggleProperty().addListener(e -> { + ((StreamrayFavoritesService) updateService).setOnline(online.isSelected()); + queue.clear(); + updateService.restart(); + }); + } + protected void addLoginButton() { grid.getChildren().clear(); grid.setAlignment(Pos.CENTER); @@ -45,7 +74,7 @@ public class StreamrayFavoritesTab extends ThumbOverviewTab implements FollowedT protected void onSuccess() { grid.getChildren().removeAll(status, loginButton); grid.setAlignment(Pos.TOP_LEFT); - if (!((AbstractStreamrayUpdateService) updateService).isLoggedIn()) { + if (!streamrayFavoritesService.isLoggedIn()) { addLoginButton(); } else { super.onSuccess(); diff --git a/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayUpdateService.java b/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayUpdateService.java index 623dc213..aec262d5 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/streamray/StreamrayUpdateService.java @@ -1,22 +1,37 @@ package ctbrec.ui.sites.streamray; +import ctbrec.Config; import ctbrec.Model; +import ctbrec.io.HttpException; import ctbrec.sites.streamray.Streamray; +import ctbrec.sites.streamray.StreamrayHttpClient; import ctbrec.sites.streamray.StreamrayModel; import javafx.concurrent.Task; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import okhttp3.Request; +import okhttp3.Response; +import org.json.JSONArray; +import org.json.JSONObject; import java.io.IOException; import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; import java.util.List; +import java.util.Locale; +import java.util.Objects; import java.util.function.Predicate; +import static ctbrec.io.HttpConstants.*; + @Slf4j public class StreamrayUpdateService extends AbstractStreamrayUpdateService { - private List models; + private List modelsList; private Instant lastListInfoRequest = Instant.EPOCH; @Setter @@ -32,21 +47,78 @@ public class StreamrayUpdateService extends AbstractStreamrayUpdateService { return new Task<>() { @Override public List call() throws IOException { - return getModels().stream() + return getModelList().stream() .filter(filter) - .skip((page - 1) * (long) modelsPerPage) - .limit(modelsPerPage) + .skip((page - 1) * (long) MODELS_PER_PAGE) + .limit(MODELS_PER_PAGE) .map(Model.class::cast) .toList(); } }; } - private List getModels() throws IOException { - if (models == null || Duration.between(lastListInfoRequest, Instant.now()).getSeconds() >= 10) { - models = site.loadModelList(); - lastListInfoRequest = Instant.now(); + private List getModelList() throws IOException { + if (Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 30) { + return Objects.nonNull(modelsList) ? modelsList : loadModelList(); + } + modelsList = loadModelList(); + return modelsList; + } + + private List loadModelList() throws IOException { + log.debug("Fetching page {}", API_URL); + lastListInfoRequest = Instant.now(); + StreamrayHttpClient client = (StreamrayHttpClient) site.getHttpClient(); + Request req = new Request.Builder() + .url(API_URL) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .header(REFERER, site.getBaseUrl() + "/") + .header(ORIGIN, site.getBaseUrl()) + .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) + .build(); + try (Response response = client.execute(req)) { + if (response.isSuccessful()) { + List models = new ArrayList<>(); + String content = response.body().string(); + JSONObject json = new JSONObject(content); + JSONArray modelNodes = json.getJSONArray("models"); + AbstractStreamrayUpdateService.setMapping(json.getJSONArray("mapping")); + parseModels(modelNodes, models); + return models; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private void parseModels(JSONArray jsonModels, List models) { + int nameIdx = mappingIndex("stream_name"); + int dateIdx = mappingIndex("create_date"); + int genIdx = mappingIndex("gender"); + for (var i = 0; i < jsonModels.length(); i++) { + var m = jsonModels.getJSONArray(i); + String name = m.optString(nameIdx); + String gender = m.optString(genIdx); + String reg = m.optString(dateIdx); + StreamrayModel model = site.createModel(name); + try { + LocalDate regDate = LocalDate.parse(reg, DateTimeFormatter.BASIC_ISO_DATE); + model.setRegDate(regDate); + } catch (DateTimeParseException e) { + model.setRegDate(LocalDate.EPOCH); + } + String preview = getPreviewURL(name); + model.setPreview(preview); + model.setGender(gender); + model.setTags(createTags(m)); + StringBuilder description = new StringBuilder(); + for (String tag : model.getTags()) { + description.append("#").append(tag).append(" "); + } + model.setDescription(description.toString()); + models.add(model); } - return models; } } diff --git a/client/src/main/java/ctbrec/ui/tabs/PaginatedScheduledService.java b/client/src/main/java/ctbrec/ui/tabs/PaginatedScheduledService.java index a8a3e4b1..bd1afb7e 100644 --- a/client/src/main/java/ctbrec/ui/tabs/PaginatedScheduledService.java +++ b/client/src/main/java/ctbrec/ui/tabs/PaginatedScheduledService.java @@ -1,19 +1,15 @@ package ctbrec.ui.tabs; -import java.util.List; - import ctbrec.Model; import javafx.concurrent.ScheduledService; +import lombok.Getter; +import lombok.Setter; +import java.util.List; + +@Getter +@Setter public abstract class PaginatedScheduledService extends ScheduledService> { protected int page = 1; - - public int getPage() { - return page; - } - - public void setPage(int page) { - this.page = page; - } }