Add support for Spy/Private/Ticket shows for Stripchat

- added private tab for Stripchat
This commit is contained in:
0xb00bface 2023-12-31 13:30:07 +01:00
parent 3fa5ed66a3
commit 67ef95cc8c
7 changed files with 126 additions and 92 deletions

View File

@ -9,7 +9,7 @@
online is merged correctly into my codebase. This should reduce 429 errors online is merged correctly into my codebase. This should reduce 429 errors
and speed up the online check quite a bit. and speed up the online check quite a bit.
* Java 21 is now required * Java 21 is now required
* Changes by @WinkRU * Changes from @WinkRU's fork
* Added setting to restrict recording by bit rate * Added setting to restrict recording by bit rate
* Added setting to use the shortest side to restrict the resolution * Added setting to use the shortest side to restrict the resolution
* Cam4: Fixed stream URLs search. Slightly increased chances to find good one. * 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 - Online/Offline switch on all tabs. Up to 10 000 offline models in each
category. How do you like it, Elon Musk? 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.
- CTBRec can record your Spy/Private/Ticket shows (login required)
*
5.2.3 5.2.3
======================== ========================

View File

@ -10,20 +10,16 @@ import java.util.List;
import java.util.Random; import java.util.Random;
public abstract class AbstractStripchatUpdateService extends PaginatedScheduledService { 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) { protected String getPreviewUrl(JSONObject model) {
try { long id = model.optLong("id");
long id = model.getLong("id"); long timestamp = model.optLong("snapshotTimestamp");
long timestamp = model.getLong("snapshotTimestamp"); if (timestamp == 0) {
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) {
return model.optString("previewUrlThumbBig"); return model.optString("previewUrlThumbBig");
} }
return MessageFormat.format("https://img.strpst.com/thumbs/{0}/{1}_jpg", String.valueOf(timestamp), String.valueOf(id));
} }
protected List<String> createTags(JSONObject model) { protected List<String> createTags(JSONObject model) {
@ -59,7 +55,7 @@ public abstract class AbstractStripchatUpdateService extends PaginatedScheduledS
} }
protected String getUniq() { protected String getUniq() {
String dict = "0123456789abcdefghijklmnopqarstvwxyz"; String dict = "0123456789abcdefghijklmnopqrstuvwxyz";
char[] text = new char[16]; char[] text = new char[16];
for (int i = 0; i < 16; i++) { for (int i = 0; i < 16; i++) {
text[i] = dict.charAt(RNG.nextInt(dict.length())); text[i] = dict.charAt(RNG.nextInt(dict.length()));

View File

@ -89,6 +89,14 @@ public class StripchatFollowedUpdateService extends AbstractStripchatUpdateServi
StripchatModel model = stripchat.createModel(jsonModel.getString("username")); StripchatModel model = stripchat.createModel(jsonModel.getString("username"));
model.setPreview(getPreviewUrl(jsonModel)); model.setPreview(getPreviewUrl(jsonModel));
model.setDisplayName(model.getName()); 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); models.add(model);
} catch (Exception e) { } catch (Exception e) {
log.warn("Couldn't parse one of the models: {}", jsonModel, e); log.warn("Couldn't parse one of the models: {}", jsonModel, e);
@ -97,6 +105,15 @@ public class StripchatFollowedUpdateService extends AbstractStripchatUpdateServi
return models; 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) { public void setOnline(boolean online) {
if (online) { if (online) {
url = stripchat.getBaseUrl() + "/api/front/models/favorites?sortBy=lastAdded"; url = stripchat.getBaseUrl() + "/api/front/models/favorites?sortBy=lastAdded";

View File

@ -31,6 +31,7 @@ public class StripchatTabProvider extends AbstractTabProvider {
tabs.add(createTab("Girls HD", MessageFormat.format(urlFilterTemplate, "autoTagHd"))); tabs.add(createTab("Girls HD", MessageFormat.format(urlFilterTemplate, "autoTagHd")));
tabs.add(createTab("Girls VR", MessageFormat.format(urlFilterTemplate, "autoTagVr"))); tabs.add(createTab("Girls VR", MessageFormat.format(urlFilterTemplate, "autoTagVr")));
tabs.add(createTab("Mobile", MessageFormat.format(urlFilterTemplate, "mobile"))); 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("Couples", MessageFormat.format(urlTemplate, "couples")));
tabs.add(createTab("Boys", MessageFormat.format(urlTemplate, "men"))); tabs.add(createTab("Boys", MessageFormat.format(urlTemplate, "men")));
tabs.add(createTab("Trans", MessageFormat.format(urlTemplate, "trans"))); tabs.add(createTab("Trans", MessageFormat.format(urlTemplate, "trans")));

View File

@ -1,6 +1,7 @@
package ctbrec.sites.stripchat; package ctbrec.sites.stripchat;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.sites.AbstractSite; import ctbrec.sites.AbstractSite;
@ -14,6 +15,7 @@ import java.net.URLEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Random;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -21,6 +23,7 @@ import static ctbrec.io.HttpConstants.USER_AGENT;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
public class Stripchat extends AbstractSite { public class Stripchat extends AbstractSite {
private static final Random RNG = new Random();
public static String domain = "stripchat.com"; public static String domain = "stripchat.com";
public static String baseUri = "https://stripchat.com"; public static String baseUri = "https://stripchat.com";
@ -69,7 +72,6 @@ public class Stripchat extends AbstractSite {
if (!credentialsAvailable()) { if (!credentialsAvailable()) {
throw new IOException("Account settings not available"); throw new IOException("Account settings not available");
} }
String username = getConfig().getSettings().stripchatUsername; String username = getConfig().getSettings().stripchatUsername;
String url = baseUri + "/api/front/users/username/" + username; String url = baseUri + "/api/front/users/username/" + username;
Request request = new Request.Builder().url(url).build(); Request request = new Request.Builder().url(url).build();
@ -161,7 +163,7 @@ public class Stripchat extends AbstractSite {
@Override @Override
public boolean credentialsAvailable() { public boolean credentialsAvailable() {
String username = getConfig().getSettings().stripchatUsername; String username = getConfig().getSettings().stripchatUsername;
return username != null && !username.trim().isEmpty(); return StringUtil.isNotBlank(username);
} }
@Override @Override
@ -174,4 +176,14 @@ public class Stripchat extends AbstractSite {
return super.createModelFromUrl(url); 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);
}
} }

View File

@ -148,6 +148,7 @@ public class StripchatHttpClient extends HttpClient {
loadJwtToken(); loadJwtToken();
} catch (Exception e) { } catch (Exception e) {
log.info("Login check returned unsuccessful: {}", e.getLocalizedMessage()); log.info("Login check returned unsuccessful: {}", e.getLocalizedMessage());
jwtToken = "";
return false; return false;
} }
return StringUtil.isNotBlank(jwtToken); return StringUtil.isNotBlank(jwtToken);

View File

@ -6,6 +6,7 @@ import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData; import com.iheartradio.m3u8.data.PlaylistData;
import ctbrec.AbstractModel; import ctbrec.AbstractModel;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.StringUtil;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.RecordingProcess; import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
@ -24,7 +25,10 @@ import java.io.InputStream;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; 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 java.util.concurrent.ExecutionException;
import static ctbrec.Model.State.*; import static ctbrec.Model.State.*;
@ -34,12 +38,9 @@ import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j @Slf4j
public class StripchatModel extends AbstractModel { 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[] resolution = new int[]{0, 0};
private int modelId = 0;
private boolean isVr = false;
private transient JSONObject modelInfo; private transient JSONObject modelInfo;
private transient Instant lastInfoRequest = Instant.EPOCH; private transient Instant lastInfoRequest = Instant.EPOCH;
@Override @Override
@ -47,15 +48,27 @@ public class StripchatModel extends AbstractModel {
if (ignoreCache) { if (ignoreCache) {
JSONObject jsonResponse = getModelInfo(); JSONObject jsonResponse = getModelInfo();
if (jsonResponse.has("user")) { if (jsonResponse.has("user")) {
JSONObject user = jsonResponse.getJSONObject("user"); JSONObject user = jsonResponse.getJSONObject("user").getJSONObject("user");
String status = user.optString("status"); String status = user.optString("status");
mapOnlineState(status); mapOnlineState(status);
if (onlineState == OFFLINE) {
setLastSeen(user.optString("statusChangedAt"));
}
if (isBanned(user)) { if (isBanned(user)) {
log.debug("Model inactive or deleted: {}", getName()); log.debug("Model inactive or deleted: {}", getName());
setMarkedForLaterRecording(true); setMarkedForLaterRecording(true);
} }
modelId = user.optInt("id"); if ((onlineState == PRIVATE) && jsonResponse.has("cam")) {
isVr = user.optBoolean("isVr", false); 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; return onlineState == ONLINE;
@ -64,7 +77,17 @@ public class StripchatModel extends AbstractModel {
private boolean isBanned(JSONObject user) { private boolean isBanned(JSONObject user) {
boolean isDeleted = user.optBoolean("isDeleted", false); boolean isDeleted = user.optBoolean("isDeleted", false);
boolean isApprovedModel = user.optBoolean("isApprovedModel", true); 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) { private void mapOnlineState(String status) {
@ -90,8 +113,7 @@ public class StripchatModel extends AbstractModel {
} }
private JSONObject loadModelInfo() throws IOException { private JSONObject loadModelInfo() throws IOException {
String name = getName(); String url = getSite().getBaseUrl() + "/api/front/v2/models/username/" + getName() + "/cam?timezoneOffset=0&triggerRequest=loadCam&uniq=" + getUniq();
String url = getSite().getBaseUrl() + "/api/front/users/username/" + name;
Request req = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON) .header(ACCEPT, MIMETYPE_APPLICATION_JSON)
@ -99,6 +121,7 @@ public class StripchatModel extends AbstractModel {
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST) .header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(REFERER, getUrl()) .header(REFERER, getUrl())
.header(ORIGIN, getSite().getBaseUrl())
.build(); .build();
try (Response response = site.getHttpClient().execute(req)) { try (Response response = site.getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
@ -175,38 +198,29 @@ public class StripchatModel extends AbstractModel {
} }
private String getMasterPlaylistUrl() throws IOException { private String getMasterPlaylistUrl() throws IOException {
boolean isVirtualRealityStream = Config.getInstance().getSettings().stripchatVR; JSONObject info = getModelInfo();
String hlsUrlTemplate = "https://edge-hls.doppiocdn.com/hls/{0}{1}/master/{0}{1}_auto.m3u8?playlistType=Standart"; if (info.has("user")) {
String vrSuffix = (isVirtualRealityStream && isVr) ? "_vr" : ""; JSONObject user = info.getJSONObject("user").getJSONObject("user");
if (modelId > 0) { long id = user.optLong("id");
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());
}
}
}
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 @Override
public void invalidateCacheEntries() { public void invalidateCacheEntries() {
@ -226,7 +240,7 @@ public class StripchatModel extends AbstractModel {
try { try {
List<StreamSource> sources = getStreamSources(); List<StreamSource> sources = getStreamSources();
if (!sources.isEmpty()) { if (!sources.isEmpty()) {
StreamSource best = sources.get(sources.size() - 1); StreamSource best = sources.getLast();
resolution = new int[]{best.getWidth(), best.getHeight()}; resolution = new int[]{best.getWidth(), best.getHeight()};
} }
} catch (IOException | ParseException | PlaylistException e) { } catch (IOException | ParseException | PlaylistException e) {
@ -238,19 +252,19 @@ public class StripchatModel extends AbstractModel {
@Override @Override
public boolean follow() throws IOException { public boolean follow() throws IOException {
getSite().getHttpClient().login(); StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient();
client.login();
JSONObject info = getModelInfo(); JSONObject info = getModelInfo();
JSONObject user = info.getJSONObject("user"); JSONObject user = info.getJSONObject("user").getJSONObject("user");
long id = user.optLong("id"); long id = user.optLong("id");
StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient();
String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites/" + id; String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites/" + id;
JSONObject requestParams = new JSONObject(); JSONObject requestParams = new JSONObject()
requestParams.put("csrfToken", client.getCsrfToken()); .put("csrfToken", client.getCsrfToken())
requestParams.put("csrfTimestamp", client.getCsrfTimestamp()); .put("csrfTimestamp", client.getCsrfTimestamp())
requestParams.put("csrfNotifyTimestamp", client.getCsrfNotifyTimestamp()); .put("csrfNotifyTimestamp", client.getCsrfNotifyTimestamp())
requestParams.put("uniq", getUniq()); .put("uniq", getUniq())
requestParams.put("ampl", client.getAmpl()); .put("ampl", client.getAmpl());
RequestBody body = RequestBody.Companion.create(requestParams.toString(), JSON); RequestBody body = RequestBody.Companion.create(requestParams.toString(), JSON);
Request request = new Request.Builder() Request request = new Request.Builder()
.url(url) .url(url)
@ -272,21 +286,21 @@ public class StripchatModel extends AbstractModel {
@Override @Override
public boolean unfollow() throws IOException { public boolean unfollow() throws IOException {
getSite().getHttpClient().login(); StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient();
client.login();
JSONObject info = getModelInfo(); JSONObject info = getModelInfo();
JSONObject user = info.getJSONObject("user"); JSONObject user = info.getJSONObject("user").getJSONObject("user");
long id = user.optLong("id"); long id = user.optLong("id");
JSONArray favoriteIds = new JSONArray(); JSONArray favoriteIds = new JSONArray().put(id);
favoriteIds.put(id); favoriteIds.put(id);
StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient();
String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites"; String url = Stripchat.baseUri + "/api/front/users/" + client.getUserId() + "/favorites";
JSONObject requestParams = new JSONObject(); JSONObject requestParams = new JSONObject()
requestParams.put("favoriteIds", favoriteIds); .put("favoriteIds", favoriteIds)
requestParams.put("csrfToken", client.getCsrfToken()); .put("csrfToken", client.getCsrfToken())
requestParams.put("csrfTimestamp", client.getCsrfTimestamp()); .put("csrfTimestamp", client.getCsrfTimestamp())
requestParams.put("csrfNotifyTimestamp", client.getCsrfNotifyTimestamp()); .put("csrfNotifyTimestamp", client.getCsrfNotifyTimestamp())
requestParams.put("uniq", getUniq()); .put("uniq", getUniq());
RequestBody body = RequestBody.Companion.create(requestParams.toString(), JSON); RequestBody body = RequestBody.Companion.create(requestParams.toString(), JSON);
Request request = new Request.Builder() Request request = new Request.Builder()
.url(url) .url(url)
@ -308,20 +322,15 @@ public class StripchatModel extends AbstractModel {
@Override @Override
public boolean exists() throws IOException { public boolean exists() throws IOException {
Request req = new Request.Builder() // @formatter:off JSONObject jsonResponse = getModelInfo();
.url(getUrl()) if (jsonResponse.optString("error").equals("Not Found")) {
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) log.info("Model not found: {}", getName());
.build(); // @formatter:on
try (Response response = getSite().getHttpClient().execute(req)) {
if (!response.isSuccessful() && response.code() == 404) {
return false; return false;
} }
}
JSONObject jsonResponse = getModelInfo();
if (jsonResponse.has("user")) { if (jsonResponse.has("user")) {
JSONObject user = jsonResponse.getJSONObject("user"); JSONObject user = jsonResponse.getJSONObject("user");
if (isBanned(user)) { if (isBanned(user)) {
log.debug("Model inactive or deleted: {}", getName()); log.info("Model inactive or deleted: {}", getName());
return false; return false;
} }
} }
@ -337,13 +346,7 @@ public class StripchatModel extends AbstractModel {
} }
} }
protected String getUniq() { private String getUniq() {
String dict = "0123456789abcdefghijklmnopqarstvwxyz"; return ((Stripchat) getSite()).getUniq();
char[] text = new char[16];
for (int i = 0; i < 16; i++) {
text[i] = dict.charAt(RNG.nextInt(dict.length()));
} }
return new String(text);
}
} }