From 81f641b7770fa9495fe413e5bb2419d9f321236a Mon Sep 17 00:00:00 2001
From: 0xb00bface <0xboobface@gmail.com>
Date: Sat, 30 Dec 2023 22:34:16 +0100
Subject: [PATCH] Added Voyeur tab for Camsoda

---
 CHANGELOG.md                                  |  3 +-
 .../ui/sites/camsoda/CamsodaTabProvider.java  |  1 +
 .../sites/camsoda/CamsodaUpdateService.java   | 43 ++++++++++++-------
 .../ctbrec/sites/camsoda/CamsodaModel.java    | 20 ++++++++-
 4 files changed, 49 insertions(+), 18 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8697d278..2b33ef93 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@
 * Changes by @WinkRU
   * Added setting to restrict recording by bit rate
   * Added setting to use the shortest side to restrict the resolution
+  * Cam4: Fixed stream URLs search. Slightly increased chances to find good one.
+  * Camsoda: Added "Voyeur" tab
   * Chaturbate: Added "Gaming" tab
   * Streamate:
     - Fixed "Couldn't load model ID" error while adding models by URL or by
@@ -19,7 +21,6 @@
     - 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.
-  * Cam4: Fixed stream URLs search. Slightly increased chances to find good one.
 
 5.2.3
 ========================
diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaTabProvider.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaTabProvider.java
index 5a8c00e0..3fad516b 100644
--- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaTabProvider.java
+++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaTabProvider.java
@@ -33,6 +33,7 @@ public class CamsodaTabProvider extends AbstractTabProvider {
         tabs.add(createTab("Male", API_URL, m -> Objects.equals("m", m.getGender())));
         tabs.add(createTab("Couples", API_URL, m -> Objects.equals("c", m.getGender())));
         tabs.add(createTab("Trans", API_URL, m -> Objects.equals("t", m.getGender())));
+        tabs.add(createTab("Voyeur", API_URL, CamsodaModel::isVoyeur));
         followedTab.setRecorder(recorder);
         followedTab.setScene(scene);
         tabs.add(followedTab);
diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java
index 3de6b8d6..324fdd6e 100644
--- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java
+++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaUpdateService.java
@@ -8,13 +8,15 @@ import ctbrec.sites.camsoda.CamsodaModel;
 import ctbrec.ui.SiteUiFactory;
 import ctbrec.ui.tabs.PaginatedScheduledService;
 import javafx.concurrent.Task;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
 import okhttp3.Request;
 import org.json.JSONArray;
 import org.json.JSONObject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.*;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -23,15 +25,16 @@ import static ctbrec.ErrorMessages.HTTP_RESPONSE_BODY_IS_NULL;
 import static ctbrec.Model.State.OFFLINE;
 import static ctbrec.Model.State.ONLINE;
 
+@Slf4j
 public class CamsodaUpdateService extends PaginatedScheduledService {
 
-    private static final Logger LOG = LoggerFactory.getLogger(CamsodaUpdateService.class);
-
     protected final String url;
     protected final boolean loginRequired;
     protected final Camsoda camsoda;
-    protected int modelsPerPage = 50;
-
+    protected int modelsPerPage = 60;
+    private static List<CamsodaModel> modelsList;
+    private static Instant lastListInfoRequest = Instant.EPOCH;
+    @Setter
     protected Predicate<CamsodaModel> filter;
 
     public CamsodaUpdateService(String url, boolean loginRequired, Camsoda camsoda, Predicate<CamsodaModel> filter) {
@@ -46,7 +49,7 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
         return new Task<>() {
             @Override
             public List<Model> call() throws IOException {
-                return loadOnlineModels().stream()
+                return getModelList().stream()
                         .sorted((m1, m2) -> (int) (m2.getSortOrder() - m1.getSortOrder()))
                         .filter(filter)
                         .skip((page - 1) * (long) modelsPerPage)
@@ -56,11 +59,20 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
         };
     }
 
+    private List<CamsodaModel> getModelList() throws IOException {
+        if (Objects.nonNull(modelsList) && Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 30) {
+            return modelsList;
+        }
+        lastListInfoRequest = Instant.now();
+        modelsList = loadOnlineModels();
+        return Optional.ofNullable(modelsList).orElse(Collections.emptyList());
+    }
+
     protected List<CamsodaModel> loadOnlineModels() throws IOException {
         if (loginRequired && StringUtil.isBlank(ctbrec.Config.getInstance().getSettings().camsodaUsername)) {
             return Collections.emptyList();
         } else {
-            LOG.debug("Fetching page {}", url);
+            log.debug("Fetching page {}", url);
             if (loginRequired) {
                 SiteUiFactory.getUi(camsoda).login();
             }
@@ -87,27 +99,29 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
                 if (templateObject instanceof JSONObject) {
                     parseModelFromObject(result, templateObject, template, models);
                 } else if (templateObject instanceof JSONArray) {
-                    parseModelFromArray(templateObject, template, models);
+                    parseModelFromArray(result, templateObject, template, models);
                 }
             } catch (Exception e) {
-                LOG.warn("Couldn't parse one of the models: {}", result, e);
+                log.warn("Couldn't parse one of the models: {}", result, e);
             }
         }
         return models;
     }
 
-    private void parseModelFromArray(Object templateObject, JSONArray template, List<CamsodaModel> models) {
+    private void parseModelFromArray(JSONObject result, Object templateObject, JSONArray template, List<CamsodaModel> models) {
         var tpl = (JSONArray) templateObject;
         var name = tpl.getString(getTemplateIndex(template, "username"));
         CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
         model.setSortOrder(tpl.getFloat(getTemplateIndex(template, "sort_value")));
         model.setDescription(tpl.getString(getTemplateIndex(template, "subject_html")));
+        model.setNew(result.optBoolean("new"));
+        model.setVoyeur(result.optBoolean("voyeur"));
         var preview = tpl.getString(getTemplateIndex(template, "thumb"));
         if (preview.startsWith("//")) {
             preview = "https:" + preview;
         }
         model.setPreview(preview);
-        LOG.trace("Preview: {}", preview);
+        log.trace("Preview: {}", preview);
         model.setOnlineStateByStatus(tpl.getString(getTemplateIndex(template, "status")));
         var displayName = tpl.getString(getTemplateIndex(template, "display_name"));
         model.setDisplayName(displayName.replaceAll("[^a-zA-Z0-9]", ""));
@@ -124,6 +138,7 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
         model.setDescription(tpl.getString(Integer.toString(getTemplateIndex(template, "subject_html"))));
         model.setSortOrder(tpl.getFloat(Integer.toString(getTemplateIndex(template, "sort_value"))));
         model.setNew(result.optBoolean("new"));
+        model.setVoyeur(result.optBoolean("voyeur"));
         model.setGender(tpl.getString(Integer.toString(getTemplateIndex(template, "gender"))));
         var preview = tpl.getString(Integer.toString(getTemplateIndex(template, "thumb")));
         if (preview.startsWith("//")) {
@@ -160,8 +175,4 @@ public class CamsodaUpdateService extends PaginatedScheduledService {
         }
         throw new NoSuchElementException(string + " not found in template: " + template);
     }
-
-    public void setFilter(Predicate<CamsodaModel> filter) {
-        this.filter = filter;
-    }
 }
diff --git a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java
index 03095c37..d8790792 100644
--- a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java
+++ b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java
@@ -7,6 +7,7 @@ import com.iheartradio.m3u8.data.PlaylistData;
 import com.iheartradio.m3u8.data.StreamInfo;
 import ctbrec.AbstractModel;
 import ctbrec.Config;
+import ctbrec.StringUtil;
 import ctbrec.io.HttpException;
 import ctbrec.recorder.download.StreamSource;
 import lombok.Getter;
@@ -21,6 +22,7 @@ import org.json.JSONObject;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.time.Instant;
 import java.util.*;
 import java.util.concurrent.ExecutionException;
 
@@ -40,6 +42,9 @@ public class CamsodaModel extends AbstractModel {
     private transient String gender;
     @Getter
     @Setter
+    private transient boolean isVoyeur;
+    @Getter
+    @Setter
     private float sortOrder = 0;
     private final Random random = new Random();
     int[] resolution = new int[2];
@@ -166,6 +171,9 @@ public class CamsodaModel extends AbstractModel {
                     JSONObject chat = result.getJSONObject("chat");
                     String status = chat.getString(STATUS);
                     setOnlineStateByStatus(status);
+                    if (onlineState == OFFLINE) {
+                        setLastSeen(chat.optString("lastOnlineAt"));
+                    }
                 } catch (JSONException e) {
                     throw new IOException("Couldn't parse body as JSON:\n" + body, e);
                 }
@@ -203,6 +211,16 @@ public class CamsodaModel extends AbstractModel {
         return onlineState;
     }
 
+    private void setLastSeen(String date) {
+        try {
+            if (StringUtil.isNotBlank(date)) {
+                setLastSeen(Instant.parse(date.replace("+0000", ".00Z")));
+            }
+        } catch (Exception e) {
+            // fail silently
+        }
+    }
+
     @Override
     public void invalidateCacheEntries() {
         streamSources = null;
@@ -219,7 +237,7 @@ public class CamsodaModel extends AbstractModel {
                 if (sources.isEmpty()) {
                     return new int[]{0, 0};
                 } else {
-                    StreamSource src = sources.get(sources.size() - 1);
+                    StreamSource src = sources.getLast();
                     resolution = new int[]{src.getWidth(), src.getHeight()};
                     return resolution;
                 }