From b39fc69299de3349f829e60a38811ec2b4854ab9 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sun, 31 Dec 2023 11:43:42 +0100 Subject: [PATCH] Added support for new "secure" stream URLs format for Flirt4Free --- CHANGELOG.md | 1 + .../Flirt4FreeFavoritesUpdateService.java | 28 +- .../flirt4free/Flirt4FreeUpdateService.java | 7 +- .../main/java/ctbrec/io/HttpConstants.java | 1 + .../sites/flirt4free/Flirt4FreeModel.java | 337 +++++++++--------- 5 files changed, 184 insertions(+), 190 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b33ef93..61182ff7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * Cam4: Fixed stream URLs search. Slightly increased chances to find good one. * Camsoda: Added "Voyeur" tab * Chaturbate: Added "Gaming" tab + * Flirt4Free: Added support for new "secure" stream URLs format. * Streamate: - Fixed "Couldn't load model ID" error while adding models by URL or by nickname diff --git a/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeFavoritesUpdateService.java b/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeFavoritesUpdateService.java index d9bb274a..2b420dcb 100644 --- a/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeFavoritesUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeFavoritesUpdateService.java @@ -1,17 +1,5 @@ package ctbrec.ui.sites.flirt4free; -import static ctbrec.io.HttpClient.*; -import static ctbrec.io.HttpConstants.*; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.ExecutionException; - -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - import ctbrec.Config; import ctbrec.Model; import ctbrec.io.HtmlParser; @@ -22,9 +10,20 @@ import ctbrec.ui.SiteUiFactory; import ctbrec.ui.tabs.PaginatedScheduledService; import javafx.concurrent.Task; import okhttp3.Request; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ExecutionException; + +import static ctbrec.io.HttpClient.gunzipBody; +import static ctbrec.io.HttpConstants.*; public class Flirt4FreeFavoritesUpdateService extends PaginatedScheduledService { - private Flirt4Free flirt4free; + private final Flirt4Free flirt4free; public Flirt4FreeFavoritesUpdateService(Flirt4Free flirt4free) { this.flirt4free = flirt4free; @@ -32,7 +31,7 @@ public class Flirt4FreeFavoritesUpdateService extends PaginatedScheduledService @Override protected Task> createTask() { - return new Task>() { + return new Task<>() { @Override public List call() throws IOException { return loadModelList(); @@ -65,7 +64,6 @@ public class Flirt4FreeFavoritesUpdateService extends PaginatedScheduledService model.setDisplayName(img.attr("alt")); model.setPreview(img.attr("src")); model.setDescription(""); - model.setOnline(modelHtml.contains("I'm Online")); try { model.setOnlineState(model.isOnline() ? Model.State.ONLINE : Model.State.OFFLINE); } catch (InterruptedException e) { diff --git a/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeUpdateService.java b/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeUpdateService.java index 53a66ad9..cc305a13 100644 --- a/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeUpdateService.java @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; import java.util.regex.Pattern; -import java.util.stream.Collectors; import static ctbrec.io.HttpClient.gunzipBody; import static ctbrec.io.HttpConstants.*; @@ -62,7 +61,7 @@ public class Flirt4FreeUpdateService extends PaginatedScheduledService { private List parseResponse(String body) throws IOException { List models = new ArrayList<>(); - var m = Pattern.compile("window\\.__homePageData__ = (\\{.*\\})", Pattern.DOTALL).matcher(body); + var m = Pattern.compile("window\\.__homePageData__ = (\\{.*})", Pattern.DOTALL).matcher(body); if (m.find()) { var data = new JSONObject(m.group(1)); var modelData = data.getJSONArray("models"); @@ -80,7 +79,8 @@ public class Flirt4FreeUpdateService extends PaginatedScheduledService { .filter(filter) .skip((page - 1) * (long) MODELS_PER_PAGE) .limit(MODELS_PER_PAGE) - .collect(Collectors.toList()); + .map(Model.class::cast) + .toList(); } else { throw new IOException("Pattern didn't match model JSON data"); } @@ -100,7 +100,6 @@ public class Flirt4FreeUpdateService extends PaginatedScheduledService { model.setStreamUrl(streamUrl); model.setPreview("https://live-screencaps.vscdns.com/" + modelId + "-desktop.jpg"); model.setOnlineState(ctbrec.Model.State.ONLINE); - model.setOnline(true); if (modelData.has("category_id")) { model.getCategories().add(modelData.getString("category_id")); } diff --git a/common/src/main/java/ctbrec/io/HttpConstants.java b/common/src/main/java/ctbrec/io/HttpConstants.java index aa71d868..a69269b6 100644 --- a/common/src/main/java/ctbrec/io/HttpConstants.java +++ b/common/src/main/java/ctbrec/io/HttpConstants.java @@ -14,6 +14,7 @@ public class HttpConstants { public static final String CONTENT_TYPE = "Content-Type"; public static final String COOKIE = "Cookie"; public static final String FORM_ENCODED = "application/x-www-form-urlencoded; charset=UTF-8"; + public static final String HTTPS = "https"; public static final String KEEP_ALIVE = "keep-alive"; public static final String MIMETYPE_APPLICATION_JSON = "application/json"; public static final String MIMETYPE_IMAGE_JPG = "image/jpeg"; diff --git a/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java index dfa55ab3..7c1b1fda 100644 --- a/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java +++ b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java @@ -8,125 +8,125 @@ import com.iheartradio.m3u8.data.StreamInfo; import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.Model; +import ctbrec.StringUtil; import ctbrec.io.HttpException; import ctbrec.recorder.download.StreamSource; +import ctbrec.sites.ModelOfflineException; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import okhttp3.*; -import org.json.JSONArray; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; +import java.text.MessageFormat; +import java.time.Duration; +import java.time.Instant; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; -import static ctbrec.StringConstants.MODEL_ID; -import static ctbrec.StringConstants.STATUS; import static ctbrec.io.HttpConstants.*; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Locale.ENGLISH; @Slf4j public class Flirt4FreeModel extends AbstractModel { + private static final String KEY_CONFIG = "config"; + private static final String KEY_STATUS = "status"; @Setter private String id; - private String chatHost; - private String chatPort; - private String chatToken; - private String streamHost; @Setter private String streamUrl; int[] resolution = new int[2]; private final transient Object monitor = new Object(); @Getter private final transient List categories = new LinkedList<>(); - @Setter - private boolean online = false; - private boolean isInteractiveShow = false; private boolean isNew = false; private String userIdt = ""; private String userIp = "0.0.0.0"; private static final Semaphore requestThrottle = new Semaphore(2, true); private static volatile long lastRequest = 0; - private long lastOnlineRequest = 0; + private transient JSONObject modelInfo; + private transient JSONObject stateInfo; + private transient Instant lastInfoRequest = Instant.EPOCH; + private transient Instant lastStateRequest = Instant.EPOCH; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { - long now = System.currentTimeMillis(); - long timeSinceLastCheck = now - lastOnlineRequest; - if (ignoreCache && timeSinceLastCheck > TimeUnit.MINUTES.toMillis(1)) { - String url = "https://ws.vs3.com/rooms/check-model-status.php?model_name=" + getName(); - acquireSlot(); - try { - Request request = new Request.Builder() - .url(url) - .header(ACCEPT, "*/*") - .header(ACCEPT_LANGUAGE, ENGLISH.getLanguage()) - .header(REFERER, getUrl()) - .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) - .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) - .build(); - try (Response response = getSite().getHttpClient().execute(request)) { - if (response.isSuccessful()) { - parseOnlineState(response.body().string()); - } else { - throw new HttpException(response.code(), response.message()); - } - } - } finally { - lastOnlineRequest = System.currentTimeMillis(); - releaseSlot(); - } + if (ignoreCache) { + JSONObject info = getStateInfo(); + parseOnlineState(info); } - return online; + return onlineState == Model.State.ONLINE; } - private void parseOnlineState(String body) { - if (body.trim().isEmpty()) { - return; - } - JSONObject json = new JSONObject(body); - if (Objects.equals(json.optString("status"), "failed")) { - if (Objects.equals(json.optString("message"), "Model is inactive")) { + private void parseOnlineState(JSONObject json) throws IOException { + if (json.optString(KEY_STATUS).equals("failed")) { + if (json.optString("message").equals("Model is inactive")) { log.debug("Model inactive or deleted: {}", getName()); setMarkedForLaterRecording(true); } - online = false; onlineState = Model.State.OFFLINE; return; } - online = Objects.equals(json.optString(STATUS), "online"); // online is true, even if the model is in private or away - updateModelId(json); - if (online) { - try { - loadModelInfo(); - } catch (Exception e) { - online = false; - onlineState = Model.State.OFFLINE; + int modelId = json.optInt("model_id"); + if (modelId > 0) { + id = String.valueOf(modelId); + } + if (json.optString(KEY_STATUS).equals("online") && modelId > 0) { + getModelInfo(); + } else { + onlineState = Model.State.OFFLINE; + } + } + + private JSONObject getStateInfo() throws IOException { + if (Objects.nonNull(stateInfo) && Duration.between(lastStateRequest, Instant.now()).getSeconds() < 5) { + return stateInfo; + } + lastStateRequest = Instant.now(); + stateInfo = loadStateInfo(); + return stateInfo; + } + + private JSONObject loadStateInfo() throws IOException { + String url = HTTPS + "://ws.vs3.com/rooms/check-model-status.php?model_name=" + getName(); + Request request = new Request.Builder() + .url(url) + .header(ACCEPT, "*/*") + .header(ACCEPT_LANGUAGE, ENGLISH.getLanguage()) + .header(REFERER, getUrl()) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) + .build(); + try (Response response = getSite().getHttpClient().execute(request)) { + if (response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + return json; + } else { + throw new HttpException(response.code(), response.message()); } } } - private void updateModelId(JSONObject json) { - if (json.has(MODEL_ID)) { - Object modelId = json.get(MODEL_ID); - if (modelId instanceof Number n && n.intValue() > 0) { - id = String.valueOf(json.get(MODEL_ID)); - } + private JSONObject getModelInfo() throws IOException { + if (Objects.nonNull(modelInfo) && Duration.between(lastInfoRequest, Instant.now()).getSeconds() < 5) { + return modelInfo; } + lastInfoRequest = Instant.now(); + modelInfo = loadModelInfo(); + return modelInfo; } - private void loadModelInfo() throws IOException { + private JSONObject loadModelInfo() throws IOException { String url = getSite().getBaseUrl() + "/webservices/chat-room-interface.php?a=login_room&model_id=" + id; log.trace("Loading url {}", url); Request request = new Request.Builder() @@ -140,25 +140,16 @@ public class Flirt4FreeModel extends AbstractModel { try (Response response = getSite().getHttpClient().execute(request)) { if (response.isSuccessful()) { JSONObject json = new JSONObject(response.body().string()); - if (json.optString(STATUS).equals("success")) { - JSONObject config = json.getJSONObject("config"); - JSONObject performer = config.getJSONObject("performer"); - setUrl(getSite().getBaseUrl() + "/rooms/" + getName() + '/'); - setDisplayName(performer.optString("name", getName())); - JSONObject room = config.getJSONObject("room"); - chatHost = room.getString("host"); - chatPort = room.getString("port_to_be"); - chatToken = json.getString("token_enc"); - String status = room.optString(STATUS); - setOnlineState(mapStatus(status)); - online = onlineState == State.ONLINE; - JSONObject user = config.getJSONObject("user"); - userIp = user.getString("ip"); + if (json.optString(KEY_STATUS).equals("success")) { + JSONObject config = json.getJSONObject(KEY_CONFIG); + setDisplayName(config.getJSONObject("performer").optString("name", getName())); + userIp = config.getJSONObject("user").getString("ip"); + onlineState = mapStatus(config.getJSONObject("room").optString(KEY_STATUS)); } else { log.trace("Loading model info failed. Assuming model {} is offline", getName()); - online = false; onlineState = Model.State.OFFLINE; } + return json; } else { throw new HttpException(response.code(), response.message()); } @@ -174,6 +165,7 @@ public class Flirt4FreeModel extends AbstractModel { }; } + @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { MasterPlaylist masterPlaylist; try { @@ -197,7 +189,7 @@ public class Flirt4FreeModel extends AbstractModel { src.setHeight((info.hasResolution()) ? info.getResolution().height : 0); src.setWidth((info.hasResolution()) ? info.getResolution().width : 0); HttpUrl masterPlaylistUrl = HttpUrl.parse(streamUrl); - src.setMediaPlaylistUrl("https://" + masterPlaylistUrl.host() + '/' + playlist.getUri()); + src.setMediaPlaylistUrl(HTTPS + "://" + masterPlaylistUrl.host() + '/' + playlist.getUri()); log.trace("Media playlist {}", src.getMediaPlaylistUrl()); sources.add(src); } @@ -231,105 +223,102 @@ public class Flirt4FreeModel extends AbstractModel { } } - private void loadStreamUrl() throws IOException, InterruptedException { - loadModelInfo(); - Objects.requireNonNull(chatHost, "chatHost is null"); - String h = chatHost.replace("chat", "chat-vip"); - String url = "https://" + h + "/chat?token=" + URLEncoder.encode(chatToken, UTF_8) + "&port_to_be=" + chatPort; - log.trace("Opening chat websocket {}", url); - Request req = new Request.Builder() - .url(url) - .header(ACCEPT, "*/*") - .header(ACCEPT_LANGUAGE, ENGLISH.getLanguage()) - .header(REFERER, getUrl()) - .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) - .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) - .build(); + private void loadStreamUrl() throws InterruptedException { + try { + JSONObject info = getModelInfo(); + streamUrl = ""; - getSite().getHttpClient().newWebSocket(req, new WebSocketListener() { - @Override - public void onOpen(WebSocket webSocket, Response response) { - log.trace("Chat websocket for {} opened", getName()); - } + String chatHost = info.getJSONObject(KEY_CONFIG).getJSONObject("room").getString("host").replace("chat", "chat-vip"); + String chatPort = info.getJSONObject(KEY_CONFIG).getJSONObject("room").getString("port"); + String chatToken = info.getString("token_enc"); + String url = HTTPS + "://" + chatHost + "/chat?token=" + URLEncoder.encode(chatToken, UTF_8) + "&port_to_be=" + chatPort; - @Override - public void onMessage(WebSocket webSocket, String text) { - log.trace("Chat wbesocket for {}: {}", getName(), text); - JSONObject json = new JSONObject(text); - if (json.optString("command").equals("8011")) { - JSONObject data = json.getJSONObject("data"); - log.trace("stream info:\n{}", data.toString(2)); - streamHost = data.getString("stream_host"); - online = true; - isInteractiveShow = data.optString("devices").equals("1"); - String roomState = data.optString("room_state"); - onlineState = mapStatus(roomState); - online = onlineState == State.ONLINE; - if (data.optString("room_state").equals("0") && data.optString("login_group_id").equals("14")) { - onlineState = Model.State.GROUP; - online = false; + log.trace("Opening chat websocket {}", url); + Request req = new Request.Builder() + .url(url) + .header(ACCEPT, "*/*") + .header(ACCEPT_LANGUAGE, ENGLISH.getLanguage()) + .header(REFERER, getUrl()) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) + .build(); + + getSite().getHttpClient().newWebSocket(req, new WebSocketListener() { + @Override + public void onMessage(WebSocket webSocket, String text) { + log.trace("Chat websocket for {}: {}", getName(), text); + JSONObject json = new JSONObject(text); + if (json.optString("command").equals("8011")) { + try { + JSONObject data = json.getJSONObject("data"); + JSONObject stream = data.getJSONObject("video_info") + .getJSONObject("hls") + .getJSONArray("providers") + .getJSONObject(0); + String streamHost = stream.optString("stream_host", "hls.vscdns.com"); + String streamName = stream.optString("stream_name", "manifest.m3u8"); + String streamKey = stream.optString("stream_key", "&"); + if (!streamKey.startsWith("&")) { + streamUrl = MessageFormat.format(HTTPS + "://{0}/{1}?key={2}", streamHost, streamName, streamKey); + } + onlineState = mapStatus(data.optString("room_state")); + resolution[0] = Integer.parseInt(json.optString("stream_width", "0")); + resolution[1] = Integer.parseInt(json.optString("stream_height", "0")); + } catch (Exception e) { + log.trace("Can not get stream info from WS", e); + } finally { + webSocket.close(1000, ""); + } } - try { - resolution[0] = Integer.parseInt(data.getString("stream_width")); - resolution[1] = Integer.parseInt(data.getString("stream_height")); - } catch (Exception e) { - log.warn("Couldn't determine stream resolution", e); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + log.trace("Chat websocket for {} failed", getName(), t); + if (response != null) { + response.close(); + } + synchronized (monitor) { + monitor.notifyAll(); } - webSocket.close(1000, ""); } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + log.trace("Chat websocket for {} closed {} {}", getName(), code, reason); + synchronized (monitor) { + monitor.notifyAll(); + } + } + }); + + synchronized (monitor) { + monitor.wait(10_000); + if (StringUtil.isBlank(streamUrl)) { + if (isOnline(false)) { + String cdn = info.getJSONObject(KEY_CONFIG).getJSONObject("env").getString("cdn").split("\\.")[0].replace(HTTPS + "://", ""); + streamUrl = MessageFormat.format(HTTPS + "://hls.vscdns.com/manifest.m3u8?key=nil&provider={0}&model_id={1}", cdn, id); + } else { + throw new ModelOfflineException(this); + } + } + log.debug("Stream URL is {}", streamUrl); } - @Override - public void onFailure(WebSocket webSocket, Throwable t, Response response) { - log.error("Chat websocket for {} failed", getName(), t); - synchronized (monitor) { - monitor.notifyAll(); - } - if (response != null) { - response.close(); - } - } - - @Override - public void onClosed(WebSocket webSocket, int code, String reason) { - log.trace("Chat websocket for {} closed {} {}", getName(), code, reason); - synchronized (monitor) { - monitor.notifyAll(); - } - } - }); - - synchronized (monitor) { - monitor.wait(10_000); - if (streamHost == null) { - throw new RuntimeException("Couldn't determine streaming server for model " + getName()); - } else { - url = getSite().getBaseUrl() + "/ws/chat/get-stream-urls.php?" - + "model_id=" + id - + "&video_host=" + streamHost - + "&t=" + System.currentTimeMillis(); - log.debug("Loading master playlist information: {}", url); - req = new Request.Builder() - .url(url) - .header(ACCEPT, "*/*") - .header(ACCEPT_LANGUAGE, ENGLISH.getLanguage()) - .header(REFERER, getUrl()) - .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) - .header(REFERER, getUrl()) - .build(); - try (Response response = getSite().getHttpClient().execute(req)) { - JSONObject json = new JSONObject(Objects.requireNonNull(response.body(), "HTTP response body is null").string()); - JSONArray hls = json.getJSONObject("data").getJSONArray("hls"); - streamUrl = "https:" + hls.getJSONObject(0).getString("url"); - log.debug("Stream URL is {}", streamUrl); - } - } + } catch (InterruptedException e) { + log.error("Interrupted while loading stream url"); + Thread.currentThread().interrupt(); + } catch (Exception e) { + throw new RuntimeException("Couldn't determine stream URL for model " + getName()); } } @Override public void invalidateCacheEntries() { - // nothing to do here + stateInfo = null; + modelInfo = null; + lastInfoRequest = Instant.EPOCH; + lastStateRequest = Instant.EPOCH; } @Override @@ -340,10 +329,10 @@ public class Flirt4FreeModel extends AbstractModel { fetchStreamUrl(); // send the tip - int giftId = isInteractiveShow ? 775 : 171; + int giftId = 171; int amount = tokens.intValue(); log.debug("Sending tip of {} to {}", amount, getName()); - String url = "https://ws.vs3.com/rooms/send-tip.php?" + + String url = HTTPS + "://ws.vs3.com/rooms/send-tip.php?" + "gift_id=" + giftId + "&num_credits=" + amount + "&userId=" + getUserIdt() + @@ -432,7 +421,7 @@ public class Flirt4FreeModel extends AbstractModel { try { List streamSources = getStreamSources(); Collections.sort(streamSources); - StreamSource best = streamSources.get(streamSources.size() - 1); + StreamSource best = streamSources.getLast(); resolution = new int[]{best.getHeight(), best.getHeight()}; } catch (IOException | ParseException | PlaylistException e) { throw new ExecutionException("Couldn't determine stream resolution", e); @@ -451,7 +440,7 @@ public class Flirt4FreeModel extends AbstractModel { return changeFavoriteStatus(true); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new IOException("Couldn't change follow status for model " + getName(), e); + throw new IOException("Couldn't follow model " + getName(), e); } } @@ -462,9 +451,9 @@ public class Flirt4FreeModel extends AbstractModel { return changeFavoriteStatus(false); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new IOException("Couldn't change follow status for model " + getName(), e); + throw new IOException("Couldn't unfollow model " + getName(), e); } catch (ExecutionException e) { - throw new IOException("Couldn't change follow status for model " + getName(), e); + throw new IOException("Couldn't unfollow model " + getName(), e); } } @@ -472,7 +461,7 @@ public class Flirt4FreeModel extends AbstractModel { getSite().login(); acquireSlot(); try { - loadModelInfo(); + getModelInfo(); } finally { releaseSlot(); } @@ -528,6 +517,12 @@ public class Flirt4FreeModel extends AbstractModel { return fixed; } + @Override + public Instant getLastSeen() { + Instant lastSeen = super.getLastSeen(); + return (lastSeen.equals(Instant.EPOCH)) ? getAddedTimestamp() : lastSeen; + } + private void acquireSlot() throws InterruptedException { requestThrottle.acquire(); long now = System.currentTimeMillis();