diff --git a/CHANGELOG.md b/CHANGELOG.md index 61182ff7..959dd25d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ online is merged correctly into my codebase. This should reduce 429 errors and speed up the online check quite a bit. * Java 21 is now required -* Changes by @WinkRU +* Changes from @WinkRU's fork * 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. @@ -22,6 +22,10 @@ - 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. + * Stripchat: + - Added "Private" tab. + - CTBRec can record your Spy/Private/Ticket shows (login required) + * 5.2.3 ======================== diff --git a/client/src/main/java/ctbrec/ui/sites/stripchat/AbstractStripchatUpdateService.java b/client/src/main/java/ctbrec/ui/sites/stripchat/AbstractStripchatUpdateService.java index 91ed7173..fe77dc4d 100644 --- a/client/src/main/java/ctbrec/ui/sites/stripchat/AbstractStripchatUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/stripchat/AbstractStripchatUpdateService.java @@ -10,20 +10,16 @@ import java.util.List; import java.util.Random; public abstract class AbstractStripchatUpdateService extends PaginatedScheduledService { - private static final Random RNG = new Random(); + + protected static final Random RNG = new Random(); protected String getPreviewUrl(JSONObject model) { - try { - long id = model.getLong("id"); - long timestamp = model.getLong("snapshotTimestamp"); - String snapshotServer = model.getString("snapshotServer"); - if (timestamp == 0 || StringUtil.isBlank(snapshotServer)) { - throw new IllegalStateException("Model seems to be offline"); - } - return MessageFormat.format("https://img.strpst.com/thumbs/{0}/{1}_jpg", String.valueOf(timestamp), String.valueOf(id)); - } catch (Exception e) { + long id = model.optLong("id"); + long timestamp = model.optLong("snapshotTimestamp"); + if (timestamp == 0) { return model.optString("previewUrlThumbBig"); } + return MessageFormat.format("https://img.strpst.com/thumbs/{0}/{1}_jpg", String.valueOf(timestamp), String.valueOf(id)); } protected List createTags(JSONObject model) { @@ -59,7 +55,7 @@ public abstract class AbstractStripchatUpdateService extends PaginatedScheduledS } protected String getUniq() { - String dict = "0123456789abcdefghijklmnopqarstvwxyz"; + String dict = "0123456789abcdefghijklmnopqrstuvwxyz"; char[] text = new char[16]; for (int i = 0; i < 16; i++) { text[i] = dict.charAt(RNG.nextInt(dict.length())); diff --git a/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatFollowedUpdateService.java b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatFollowedUpdateService.java index 62ad45dc..3a402f44 100644 --- a/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatFollowedUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatFollowedUpdateService.java @@ -89,6 +89,14 @@ public class StripchatFollowedUpdateService extends AbstractStripchatUpdateServi StripchatModel model = stripchat.createModel(jsonModel.getString("username")); model.setPreview(getPreviewUrl(jsonModel)); model.setDisplayName(model.getName()); + String status = jsonModel.optString("status"); + mapOnlineState(model, status); + model.setTags(createTags(jsonModel)); + StringBuilder description = new StringBuilder(); + for (String tag : model.getTags()) { + description.append("#").append(tag).append(" "); + } + model.setDescription(description.toString()); models.add(model); } catch (Exception e) { log.warn("Couldn't parse one of the models: {}", jsonModel, e); @@ -97,6 +105,15 @@ public class StripchatFollowedUpdateService extends AbstractStripchatUpdateServi return models; } + private void mapOnlineState(StripchatModel model, String status) { + switch (status) { + case "public" -> model.setOnlineState(Model.State.ONLINE); + case "idle" -> model.setOnlineState(Model.State.AWAY); + case "private", "p2p", "groupShow", "virtualPrivate" -> model.setOnlineState(Model.State.PRIVATE); + default -> model.setOnlineState(Model.State.OFFLINE); + } + } + public void setOnline(boolean online) { if (online) { url = stripchat.getBaseUrl() + "/api/front/models/favorites?sortBy=lastAdded"; diff --git a/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatTabProvider.java b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatTabProvider.java index a95ac062..694e5e50 100644 --- a/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatTabProvider.java @@ -31,6 +31,7 @@ public class StripchatTabProvider extends AbstractTabProvider { tabs.add(createTab("Girls HD", MessageFormat.format(urlFilterTemplate, "autoTagHd"))); tabs.add(createTab("Girls VR", MessageFormat.format(urlFilterTemplate, "autoTagVr"))); tabs.add(createTab("Mobile", MessageFormat.format(urlFilterTemplate, "mobile"))); + tabs.add(createTab("Private", MessageFormat.format(urlFilterTemplate, "autoTagSpy"))); tabs.add(createTab("Couples", MessageFormat.format(urlTemplate, "couples"))); tabs.add(createTab("Boys", MessageFormat.format(urlTemplate, "men"))); tabs.add(createTab("Trans", MessageFormat.format(urlTemplate, "trans"))); diff --git a/common/src/main/java/ctbrec/sites/stripchat/Stripchat.java b/common/src/main/java/ctbrec/sites/stripchat/Stripchat.java index 1b2f21cb..465e42a4 100644 --- a/common/src/main/java/ctbrec/sites/stripchat/Stripchat.java +++ b/common/src/main/java/ctbrec/sites/stripchat/Stripchat.java @@ -1,6 +1,7 @@ package ctbrec.sites.stripchat; import ctbrec.Model; +import ctbrec.StringUtil; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import ctbrec.sites.AbstractSite; @@ -14,6 +15,7 @@ import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Random; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -21,6 +23,7 @@ import static ctbrec.io.HttpConstants.USER_AGENT; import static java.nio.charset.StandardCharsets.UTF_8; public class Stripchat extends AbstractSite { + private static final Random RNG = new Random(); public static String domain = "stripchat.com"; public static String baseUri = "https://stripchat.com"; @@ -69,7 +72,6 @@ public class Stripchat extends AbstractSite { if (!credentialsAvailable()) { throw new IOException("Account settings not available"); } - String username = getConfig().getSettings().stripchatUsername; String url = baseUri + "/api/front/users/username/" + username; Request request = new Request.Builder().url(url).build(); @@ -161,7 +163,7 @@ public class Stripchat extends AbstractSite { @Override public boolean credentialsAvailable() { String username = getConfig().getSettings().stripchatUsername; - return username != null && !username.trim().isEmpty(); + return StringUtil.isNotBlank(username); } @Override @@ -174,4 +176,14 @@ public class Stripchat extends AbstractSite { return super.createModelFromUrl(url); } } + + public String getUniq() { + String dict = "0123456789abcdefghijklmnopqrstuvwxyz"; + char[] text = new char[16]; + for (int i = 0; i < 16; i++) { + text[i] = dict.charAt(RNG.nextInt(dict.length())); + } + return new String(text); + } + } diff --git a/common/src/main/java/ctbrec/sites/stripchat/StripchatHttpClient.java b/common/src/main/java/ctbrec/sites/stripchat/StripchatHttpClient.java index 63b60ae4..eb840b18 100644 --- a/common/src/main/java/ctbrec/sites/stripchat/StripchatHttpClient.java +++ b/common/src/main/java/ctbrec/sites/stripchat/StripchatHttpClient.java @@ -148,6 +148,7 @@ public class StripchatHttpClient extends HttpClient { loadJwtToken(); } catch (Exception e) { log.info("Login check returned unsuccessful: {}", e.getLocalizedMessage()); + jwtToken = ""; return false; } return StringUtil.isNotBlank(jwtToken); diff --git a/common/src/main/java/ctbrec/sites/stripchat/StripchatModel.java b/common/src/main/java/ctbrec/sites/stripchat/StripchatModel.java index 56dd7e2d..5c26194d 100644 --- a/common/src/main/java/ctbrec/sites/stripchat/StripchatModel.java +++ b/common/src/main/java/ctbrec/sites/stripchat/StripchatModel.java @@ -6,6 +6,7 @@ import com.iheartradio.m3u8.data.Playlist; import com.iheartradio.m3u8.data.PlaylistData; import ctbrec.AbstractModel; import ctbrec.Config; +import ctbrec.StringUtil; import ctbrec.io.HttpException; import ctbrec.recorder.download.RecordingProcess; import ctbrec.recorder.download.StreamSource; @@ -24,7 +25,10 @@ import java.io.InputStream; import java.text.MessageFormat; import java.time.Duration; import java.time.Instant; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; import java.util.concurrent.ExecutionException; import static ctbrec.Model.State.*; @@ -34,12 +38,9 @@ import static java.nio.charset.StandardCharsets.UTF_8; @Slf4j public class StripchatModel extends AbstractModel { - private static final Random RNG = new Random(); + private static final String KEY_MODEL_TOKEN = "modelToken"; private int[] resolution = new int[]{0, 0}; - private int modelId = 0; - private boolean isVr = false; private transient JSONObject modelInfo; - private transient Instant lastInfoRequest = Instant.EPOCH; @Override @@ -47,15 +48,27 @@ public class StripchatModel extends AbstractModel { if (ignoreCache) { JSONObject jsonResponse = getModelInfo(); if (jsonResponse.has("user")) { - JSONObject user = jsonResponse.getJSONObject("user"); + JSONObject user = jsonResponse.getJSONObject("user").getJSONObject("user"); String status = user.optString("status"); mapOnlineState(status); + if (onlineState == OFFLINE) { + setLastSeen(user.optString("statusChangedAt")); + } if (isBanned(user)) { log.debug("Model inactive or deleted: {}", getName()); setMarkedForLaterRecording(true); } - modelId = user.optInt("id"); - isVr = user.optBoolean("isVr", false); + if ((onlineState == PRIVATE) && jsonResponse.has("cam")) { + JSONObject cam = jsonResponse.getJSONObject("cam"); + if (StringUtil.isNotBlank(cam.optString(KEY_MODEL_TOKEN))) { + setOnlineState(ONLINE); + return true; + } + } + } + if (jsonResponse.optString("error").equals("Not Found")) { + setMarkedForLaterRecording(true); + setOnlineState(OFFLINE); } } return onlineState == ONLINE; @@ -64,7 +77,17 @@ public class StripchatModel extends AbstractModel { private boolean isBanned(JSONObject user) { boolean isDeleted = user.optBoolean("isDeleted", false); boolean isApprovedModel = user.optBoolean("isApprovedModel", true); - return (!isApprovedModel || isDeleted); + return (!isApprovedModel && isDeleted); + } + + private void setLastSeen(String date) { + try { + if (StringUtil.isNotBlank(date)) { + setLastSeen(Instant.parse(date)); + } + } catch (Exception e) { + // fail silently + } } private void mapOnlineState(String status) { @@ -90,8 +113,7 @@ public class StripchatModel extends AbstractModel { } private JSONObject loadModelInfo() throws IOException { - String name = getName(); - String url = getSite().getBaseUrl() + "/api/front/users/username/" + name; + String url = getSite().getBaseUrl() + "/api/front/v2/models/username/" + getName() + "/cam?timezoneOffset=0&triggerRequest=loadCam&uniq=" + getUniq(); Request req = new Request.Builder() .url(url) .header(ACCEPT, MIMETYPE_APPLICATION_JSON) @@ -99,6 +121,7 @@ public class StripchatModel extends AbstractModel { .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(REFERER, getUrl()) + .header(ORIGIN, getSite().getBaseUrl()) .build(); try (Response response = site.getHttpClient().execute(req)) { if (response.isSuccessful()) { @@ -175,39 +198,30 @@ public class StripchatModel extends AbstractModel { } private String getMasterPlaylistUrl() throws IOException { - boolean isVirtualRealityStream = Config.getInstance().getSettings().stripchatVR; - String hlsUrlTemplate = "https://edge-hls.doppiocdn.com/hls/{0}{1}/master/{0}{1}_auto.m3u8?playlistType=Standart"; - String vrSuffix = (isVirtualRealityStream && isVr) ? "_vr" : ""; - if (modelId > 0) { - return MessageFormat.format(hlsUrlTemplate, String.valueOf(modelId), vrSuffix); - } - String name = getName(); - String url = getSite().getBaseUrl() + "/api/front/models/username/" + name + "/cam?triggerRequest=loadCam"; - Request req = new Request.Builder() - .url(url) - .header(ACCEPT, MIMETYPE_APPLICATION_JSON) - .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) - .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) - .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) - .header(REFERER, getUrl()) - .build(); - try (Response response = site.getHttpClient().execute(req)) { - if (response.isSuccessful()) { - String body = response.body().string(); - log.trace(body); - JSONObject jsonResponse = new JSONObject(body); - String streamName = jsonResponse.optString("streamName"); - JSONObject broadcastSettings = jsonResponse.getJSONObject("broadcastSettings"); - String vrBroadcastServer = broadcastSettings.optString("vrBroadcastServer"); - vrSuffix = (!isVirtualRealityStream || vrBroadcastServer.isEmpty()) ? "" : "_vr"; - return MessageFormat.format(hlsUrlTemplate, streamName, vrSuffix); - } else { - throw new HttpException(response.code(), response.message()); + JSONObject info = getModelInfo(); + if (info.has("user")) { + JSONObject user = info.getJSONObject("user").getJSONObject("user"); + long id = user.optLong("id"); + + boolean saveVR = Config.getInstance().getSettings().stripchatVR; + boolean isVRStream = user.optBoolean("isVr", false); + String vrSuffix = (saveVR && isVRStream) ? "_vr" : ""; + + String token = ""; + if (info.has("cam")) { + JSONObject cam = info.getJSONObject("cam"); + if (StringUtil.isNotBlank(cam.optString(KEY_MODEL_TOKEN))) { + token = "&aclAuth=" + cam.getString(KEY_MODEL_TOKEN); + log.debug("Spy start for {}", getName()); + } } + String hlsUrlTemplate = "https://edge-hls.doppiocdn.com/hls/{0}{1}/master/{0}{1}_auto.m3u8?playlistType=Standart{2}"; + return MessageFormat.format(hlsUrlTemplate, String.valueOf(id), vrSuffix, token); + } else { + throw new IOException("Playlist URL not found"); } } - @Override public void invalidateCacheEntries() { resolution = new int[]{0, 0}; @@ -226,7 +240,7 @@ public class StripchatModel extends AbstractModel { try { List sources = getStreamSources(); if (!sources.isEmpty()) { - StreamSource best = sources.get(sources.size() - 1); + StreamSource best = sources.getLast(); resolution = new int[]{best.getWidth(), best.getHeight()}; } } catch (IOException | ParseException | PlaylistException e) { @@ -238,19 +252,19 @@ public class StripchatModel extends AbstractModel { @Override public boolean follow() throws IOException { - getSite().getHttpClient().login(); + StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient(); + client.login(); JSONObject info = getModelInfo(); - JSONObject user = info.getJSONObject("user"); + JSONObject user = info.getJSONObject("user").getJSONObject("user"); long id = user.optLong("id"); - StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient(); String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites/" + id; - JSONObject requestParams = new JSONObject(); - requestParams.put("csrfToken", client.getCsrfToken()); - requestParams.put("csrfTimestamp", client.getCsrfTimestamp()); - requestParams.put("csrfNotifyTimestamp", client.getCsrfNotifyTimestamp()); - requestParams.put("uniq", getUniq()); - requestParams.put("ampl", client.getAmpl()); + JSONObject requestParams = new JSONObject() + .put("csrfToken", client.getCsrfToken()) + .put("csrfTimestamp", client.getCsrfTimestamp()) + .put("csrfNotifyTimestamp", client.getCsrfNotifyTimestamp()) + .put("uniq", getUniq()) + .put("ampl", client.getAmpl()); RequestBody body = RequestBody.Companion.create(requestParams.toString(), JSON); Request request = new Request.Builder() .url(url) @@ -272,21 +286,21 @@ public class StripchatModel extends AbstractModel { @Override public boolean unfollow() throws IOException { - getSite().getHttpClient().login(); + StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient(); + client.login(); JSONObject info = getModelInfo(); - JSONObject user = info.getJSONObject("user"); + JSONObject user = info.getJSONObject("user").getJSONObject("user"); long id = user.optLong("id"); - JSONArray favoriteIds = new JSONArray(); + JSONArray favoriteIds = new JSONArray().put(id); favoriteIds.put(id); - StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient(); String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites"; - JSONObject requestParams = new JSONObject(); - requestParams.put("favoriteIds", favoriteIds); - requestParams.put("csrfToken", client.getCsrfToken()); - requestParams.put("csrfTimestamp", client.getCsrfTimestamp()); - requestParams.put("csrfNotifyTimestamp", client.getCsrfNotifyTimestamp()); - requestParams.put("uniq", getUniq()); + JSONObject requestParams = new JSONObject() + .put("favoriteIds", favoriteIds) + .put("csrfToken", client.getCsrfToken()) + .put("csrfTimestamp", client.getCsrfTimestamp()) + .put("csrfNotifyTimestamp", client.getCsrfNotifyTimestamp()) + .put("uniq", getUniq()); RequestBody body = RequestBody.Companion.create(requestParams.toString(), JSON); Request request = new Request.Builder() .url(url) @@ -308,20 +322,15 @@ public class StripchatModel extends AbstractModel { @Override public boolean exists() throws IOException { - Request req = new Request.Builder() // @formatter:off - .url(getUrl()) - .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) - .build(); // @formatter:on - try (Response response = getSite().getHttpClient().execute(req)) { - if (!response.isSuccessful() && response.code() == 404) { - return false; - } - } JSONObject jsonResponse = getModelInfo(); + if (jsonResponse.optString("error").equals("Not Found")) { + log.info("Model not found: {}", getName()); + return false; + } if (jsonResponse.has("user")) { JSONObject user = jsonResponse.getJSONObject("user"); if (isBanned(user)) { - log.debug("Model inactive or deleted: {}", getName()); + log.info("Model inactive or deleted: {}", getName()); return false; } } @@ -337,13 +346,7 @@ public class StripchatModel extends AbstractModel { } } - protected String getUniq() { - String dict = "0123456789abcdefghijklmnopqarstvwxyz"; - char[] text = new char[16]; - for (int i = 0; i < 16; i++) { - text[i] = dict.charAt(RNG.nextInt(dict.length())); - } - return new String(text); + private String getUniq() { + return ((Stripchat) getSite()).getUniq(); } - }