From 959b41e3b90222b983869364a44f347ae23198c7 Mon Sep 17 00:00:00 2001
From: 0xb00bface <0xboobface@gmail.com>
Date: Sat, 30 Dec 2023 17:13:57 +0100
Subject: [PATCH] Fix thumbnail caching

---
 CHANGELOG.md                                  |   1 +
 .../java/ctbrec/ui/CamrecApplication.java     |   8 +-
 .../java/ctbrec/ui/settings/SettingsTab.java  |   2 +-
 .../sites/jasmin/LiveJasminUpdateService.java |  23 ++--
 .../main/java/ctbrec/ui/tabs/ThumbCell.java   |   6 +-
 .../src/main/java/ctbrec/io/HttpClient.java   | 103 +++++++++++-------
 .../ctbrec/io/HttpClientCacheProvider.java    |  46 ++++++++
 7 files changed, 131 insertions(+), 58 deletions(-)
 create mode 100644 common/src/main/java/ctbrec/io/HttpClientCacheProvider.java

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 492bba88..468f06bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 * Added menu entry to force recording of models without changing the prio
 * Added blacklist and whitelist settings to automatically filter out models
 * Added setting to delete orphaned recording metadata (switched off by default)
+* Fixed thumbnail caching
 
 5.2.3
 ========================
diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java
index 0f870944..d085481a 100644
--- a/client/src/main/java/ctbrec/ui/CamrecApplication.java
+++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java
@@ -14,10 +14,7 @@ import ctbrec.event.EventHandlerConfiguration;
 import ctbrec.image.LocalPortraitStore;
 import ctbrec.image.PortraitStore;
 import ctbrec.image.RemotePortraitStore;
-import ctbrec.io.BandwidthMeter;
-import ctbrec.io.ByteUnitFormatter;
-import ctbrec.io.HttpClient;
-import ctbrec.io.HttpException;
+import ctbrec.io.*;
 import ctbrec.io.json.ObjectMapperFactory;
 import ctbrec.notes.LocalModelNotesService;
 import ctbrec.notes.ModelNotesService;
@@ -434,6 +431,9 @@ public class CamrecApplication extends Application {
             }
             try {
                 ExternalBrowser.getInstance().close();
+                HttpClientCacheProvider.getCache(config).evictAll();
+                HttpClientCacheProvider.getCache(config).close();
+                IoUtils.deleteDirectory(new File(config.getConfigDir(), "cache"));
             } catch (IOException e12) {
                 // noop
             }
diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java
index 01618358..cbf04842 100644
--- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java
+++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java
@@ -234,7 +234,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
                                 Setting.of("Update overview interval (seconds)", overviewUpdateIntervalInSecs, "Update the thumbnail overviews every x seconds").needsRestart(),
                                 Setting.of("Update thumbnails", updateThumbnails,
                                         "The overviews will still be updated, but the thumbnails won't be changed. This is useful for less powerful systems."),
-                                Setting.of("Cache size", new CacheSettingsPane(this, config)).needsRestart(),
+                                Setting.of("Thumbnails cache size", new CacheSettingsPane(this, config)).needsRestart(),
                                 Setting.of("Manually select stream quality", chooseStreamQuality, "Opens a dialog to select the video resolution before recording"),
                                 Setting.of("Enable live previews (experimental)", livePreviews),
                                 Setting.of("Enable recently watched tab", recentlyWatched).needsRestart(),
diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java
index 5e0623cc..b3c7ca42 100644
--- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java
+++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminUpdateService.java
@@ -170,19 +170,20 @@ public class LiveJasminUpdateService extends PaginatedScheduledService {
                 }
                 listPageId = content.optString("listPageId");
                 JSONArray performers = content.getJSONArray("performers");
-        for (var i = 0; i < performers.length(); i++) {
-            var m = performers.getJSONObject(i);
-            var name = m.optString("pid");
-            if (name.isEmpty()) {
-                continue;
-            }
-            LiveJasminModel model = (LiveJasminModel) liveJasmin.createModel(name);
-            model.setId(m.getString("id"));
-            model.setPreview(m.optString("profilePictureUrl"));
+                for (var i = 0; i < performers.length(); i++) {
+                    var m = performers.getJSONObject(i);
+                    var name = m.optString("pid");
+                    if (name.isEmpty()) {
+                        continue;
+                    }
+                    LiveJasminModel model = (LiveJasminModel) liveJasmin.createModel(name);
+                    model.setId(m.getString("id"));
+                    model.setPreview(m.optString("profilePictureUrl"));
                     model.setOnlineState(LiveJasminModel.mapStatus(m.optInt("status")));
                     model.setDisplayName(m.optString("display_name", null));
-            models.add(model);
-        }} // if content
+                    models.add(model);
+                }
+            } // if content
         } // if data
     }
 }
diff --git a/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java b/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java
index 2a28487c..14d035eb 100644
--- a/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java
+++ b/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java
@@ -429,7 +429,7 @@ public class ThumbCell extends StackPane {
                     .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
                     .header(REFERER, getModel().getSite().getBaseUrl())
                     .build();
-            try (Response resp = model.getSite().getHttpClient().execute(req)) {
+            try (Response resp = model.getSite().getHttpClient().executeWithCache(req)) {
                 if (resp.isSuccessful()) {
                     double width = 480;
                     double height = width * imgAspectRatio;
@@ -500,9 +500,9 @@ public class ThumbCell extends StackPane {
             } else {
                 modelRecordingState = ModelRecordingState.RECORDING;
                 if (model.isForcePriority()) {
-                	recordingIndicator.setImage(imgForceRecordIndicator);
+                    recordingIndicator.setImage(imgForceRecordIndicator);
                 } else {
-                	recordingIndicator.setImage(imgRecordIndicator);
+                    recordingIndicator.setImage(imgRecordIndicator);
                 }
                 recordingIndicatorTooltip.setText("Pause Recording");
             }
diff --git a/common/src/main/java/ctbrec/io/HttpClient.java b/common/src/main/java/ctbrec/io/HttpClient.java
index 69dd3fb1..e90a6b79 100644
--- a/common/src/main/java/ctbrec/io/HttpClient.java
+++ b/common/src/main/java/ctbrec/io/HttpClient.java
@@ -8,6 +8,7 @@ import ctbrec.io.json.ObjectMapperFactory;
 import ctbrec.io.json.dto.CookieDto;
 import ctbrec.io.json.mapper.CookieMapper;
 import lombok.Data;
+import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
 import okhttp3.*;
 import okhttp3.OkHttpClient.Builder;
@@ -38,11 +39,14 @@ import static java.nio.charset.StandardCharsets.UTF_8;
 public abstract class HttpClient {
     private static final ConnectionPool GLOBAL_HTTP_CONN_POOL = new ConnectionPool(10, 2, TimeUnit.MINUTES);
 
+    @Getter
+    protected CookieJarImpl cookieJar;
     protected OkHttpClient client;
     protected Cache cache;
-    protected CookieJarImpl cookieJar;
     protected Config config;
     protected boolean loggedIn = false;
+    protected long cacheSize;
+    protected int cacheLifeTime = 600;
     private final String name;
 
     protected HttpClient(String name, Config config) {
@@ -60,26 +64,26 @@ public abstract class HttpClient {
         ProxyType proxyType = config.getSettings().proxyType;
         switch (proxyType) {
             case HTTP:
-                System.setProperty("http.proxyHost", config.getSettings().proxyHost);
-                System.setProperty("http.proxyPort", config.getSettings().proxyPort);
-                System.setProperty("https.proxyHost", config.getSettings().proxyHost);
-                System.setProperty("https.proxyPort", config.getSettings().proxyPort);
+                System.setProperty(ProxyConstants.HTTP_PROXY_HOST, config.getSettings().proxyHost);
+                System.setProperty(ProxyConstants.HTTP_PROXY_PORT, config.getSettings().proxyPort);
+                System.setProperty(ProxyConstants.HTTPS_PROXY_HOST, config.getSettings().proxyHost);
+                System.setProperty(ProxyConstants.HTTPS_PROXY_PORT, config.getSettings().proxyPort);
                 if (config.getSettings().proxyUser != null && !config.getSettings().proxyUser.isEmpty()) {
                     String username = config.getSettings().proxyUser;
                     String password = config.getSettings().proxyPassword;
-                    System.setProperty("http.proxyUser", username);
-                    System.setProperty("http.proxyPassword", password);
+                    System.setProperty(ProxyConstants.HTTP_PROXY_USER, username);
+                    System.setProperty(ProxyConstants.HTTP_PROXY_PASSWORD, password);
                 }
                 break;
             case SOCKS4:
-                System.setProperty("socksProxyVersion", "4");
-                System.setProperty("socksProxyHost", config.getSettings().proxyHost);
-                System.setProperty("socksProxyPort", config.getSettings().proxyPort);
+                System.setProperty(ProxyConstants.SOCKS_PROXY_VERSION, "4");
+                System.setProperty(ProxyConstants.SOCKS_PROXY_HOST, config.getSettings().proxyHost);
+                System.setProperty(ProxyConstants.SOCKS_PROXY_PORT, config.getSettings().proxyPort);
                 break;
             case SOCKS5:
-                System.setProperty("socksProxyVersion", "5");
-                System.setProperty("socksProxyHost", config.getSettings().proxyHost);
-                System.setProperty("socksProxyPort", config.getSettings().proxyPort);
+                System.setProperty(ProxyConstants.SOCKS_PROXY_VERSION, "5");
+                System.setProperty(ProxyConstants.SOCKS_PROXY_HOST, config.getSettings().proxyHost);
+                System.setProperty(ProxyConstants.SOCKS_PROXY_PORT, config.getSettings().proxyPort);
                 if (config.getSettings().proxyUser != null && !config.getSettings().proxyUser.isEmpty()) {
                     String username = config.getSettings().proxyUser;
                     String password = config.getSettings().proxyPassword;
@@ -88,44 +92,55 @@ public abstract class HttpClient {
                 break;
             case DIRECT:
             default:
-                System.clearProperty("http.proxyHost");
-                System.clearProperty("http.proxyPort");
-                System.clearProperty("https.proxyHost");
-                System.clearProperty("https.proxyPort");
-                System.clearProperty("socksProxyVersion");
-                System.clearProperty("socksProxyHost");
-                System.clearProperty("socksProxyPort");
-                System.clearProperty("java.net.socks.username");
-                System.clearProperty("java.net.socks.password");
-                System.clearProperty("http.proxyUser");
-                System.clearProperty("http.proxyPassword");
+                System.clearProperty(ProxyConstants.HTTP_PROXY_HOST);
+                System.clearProperty(ProxyConstants.HTTP_PROXY_PORT);
+                System.clearProperty(ProxyConstants.HTTPS_PROXY_HOST);
+                System.clearProperty(ProxyConstants.HTTPS_PROXY_PORT);
+                System.clearProperty(ProxyConstants.SOCKS_PROXY_VERSION);
+                System.clearProperty(ProxyConstants.SOCKS_PROXY_HOST);
+                System.clearProperty(ProxyConstants.SOCKS_PROXY_PORT);
+                System.clearProperty(ProxyConstants.JAVA_NET_SOCKS_USERNAME);
+                System.clearProperty(ProxyConstants.JAVA_NET_SOCKS_PASSWORD);
+                System.clearProperty(ProxyConstants.HTTP_PROXY_USER);
+                System.clearProperty(ProxyConstants.HTTP_PROXY_PASSWORD);
                 break;
         }
     }
 
     public Response execute(Request req) throws IOException {
-        if (cache != null) {
-            log.trace("Cache hit ratio {}/{} = {}", cache.hitCount(), cache.requestCount(), NumberFormat.getPercentInstance().format(cache.hitCount() / (double) cache.requestCount()));
-        }
         Response resp = client.newCall(req).execute();
         return resp;
     }
 
     public Response execute(Request request, int timeoutInMillis) throws IOException {
-        if (cache != null) {
-            log.trace("Cache hit ratio {}/{} = {}", cache.hitCount(), cache.requestCount(), NumberFormat.getPercentInstance().format(cache.hitCount() / (double) cache.requestCount()));
-        }
         return client.newBuilder() //
                 .connectTimeout(timeoutInMillis, TimeUnit.MILLISECONDS) //
                 .readTimeout(timeoutInMillis, TimeUnit.MILLISECONDS).build() //
                 .newCall(request).execute();
     }
 
+    public Response executeWithCache(Request req) throws IOException {
+        log.trace("Cached request for {}", req.url());
+        if (Objects.nonNull(cache)) {
+            log.trace("Cache hit ratio {}/{} = {}", cache.hitCount(), cache.requestCount(), NumberFormat.getPercentInstance().format(cache.hitCount() / (double) cache.requestCount()));
+        }
+        if (cacheSize > 0 && Objects.nonNull(cache)) {
+            Request r = req.newBuilder()
+                    .cacheControl(new CacheControl.Builder().maxAge(cacheLifeTime, TimeUnit.SECONDS).build())
+                    .build();
+            return execute(r);
+        } else {
+            return execute(req);
+        }
+    }
+
     public abstract boolean login() throws IOException;
 
     public void reconfigure() {
         loadProxySettings();
         loadCookies();
+        cacheSize = (long) config.getSettings().thumbCacheSize * 1024 * 1024;
+
         Builder builder = new OkHttpClient.Builder()
                 .cookieJar(cookieJar)
                 .connectionPool(GLOBAL_HTTP_CONN_POOL)
@@ -133,12 +148,11 @@ public abstract class HttpClient {
                 .readTimeout(config.getSettings().httpTimeout, TimeUnit.MILLISECONDS)
                 .addNetworkInterceptor(new LoggingInterceptor());
 
-        long cacheSize = (long) config.getSettings().thumbCacheSize * 1024 * 1024;
         if (cacheSize > 0) {
-            File configDir = config.getConfigDir();
-            File cacheDir = new File(configDir, "cache");
-            cache = new Cache(cacheDir, cacheSize);
-            builder.cache(cache);
+            cache = HttpClientCacheProvider.getCache(config);
+            if (cache != null) {
+                builder.cache(cache);
+            }
         }
 
         ProxyType proxyType = config.getSettings().proxyType;
@@ -265,10 +279,6 @@ public abstract class HttpClient {
         }
     }
 
-    public CookieJarImpl getCookieJar() {
-        return cookieJar;
-    }
-
     public void logout() {
         getCookieJar().clear();
         loggedIn = false;
@@ -332,4 +342,19 @@ public abstract class HttpClient {
     public void clearCookies() {
         logout();
     }
+
+    private static class ProxyConstants {
+        public static final String HTTP_PROXY_HOST = "http.proxyHost";
+        public static final String HTTP_PROXY_PORT = "http.proxyPort";
+        public static final String HTTPS_PROXY_HOST = "https.proxyHost";
+        public static final String HTTPS_PROXY_PORT = "https.proxyPort";
+        public static final String HTTP_PROXY_USER = "https.proxyUser";
+        public static final String HTTP_PROXY_PASSWORD = "https.proxyPassword";
+
+        public static final String SOCKS_PROXY_HOST = "socksProxyHost";
+        public static final String SOCKS_PROXY_PORT = "socksProxyPort";
+        public static final String SOCKS_PROXY_VERSION = "socksProxyVersion";
+        public static final String JAVA_NET_SOCKS_USERNAME = "java.net.socks.username";
+        public static final String JAVA_NET_SOCKS_PASSWORD = "java.net.socks.password";
+    }
 }
diff --git a/common/src/main/java/ctbrec/io/HttpClientCacheProvider.java b/common/src/main/java/ctbrec/io/HttpClientCacheProvider.java
new file mode 100644
index 00000000..86a5c9c7
--- /dev/null
+++ b/common/src/main/java/ctbrec/io/HttpClientCacheProvider.java
@@ -0,0 +1,46 @@
+package ctbrec.io;
+
+import ctbrec.Config;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.Cache;
+
+import java.io.File;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+@Slf4j
+public class HttpClientCacheProvider {
+
+    private static final Lock lock = new ReentrantLock();
+    private static HttpClientCacheProvider provider;
+
+    private Cache cache;
+
+    private HttpClientCacheProvider(Config config) {
+        File configDir = config.getConfigDir();
+        File cacheDir = new File(configDir, "cache");
+        long cacheSize = (long) config.getSettings().thumbCacheSize * 1024 * 1024;
+        try {
+            cache = new Cache(cacheDir, cacheSize);
+        } catch (Exception ex) {
+            log.error("Could not create HTTP client cache", ex);
+        }
+    }
+
+    private Cache getCache() {
+        return cache;
+    }
+
+    public static Cache getCache(Config config) {
+        lock.lock();
+        try {
+            if (provider == null) {
+                provider = new HttpClientCacheProvider(config);
+            }
+            return provider.getCache();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+}