package ctbrec.sites.manyvids;
import ctbrec.Model;
import ctbrec.Model.State;
import ctbrec.io.HtmlParser;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.sites.AbstractSite;
import okhttp3.*;
import org.json.JSONArray;
import org.json.JSONObject;
import org.jsoup.nodes.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8;
public class MVLive extends AbstractSite {
private static final Logger LOG = LoggerFactory.getLogger(MVLive.class);
private static final String STARS = "stars";
public static final String WS_ORIGIN = "https://live.manyvids.com";
public static final String BASE_URL = "https://www.manyvids.com";
public static final String LIVE_URL = BASE_URL + "/mv-live/";
private final Pattern configPattern = Pattern.compile("");
private final Pattern graphQlUrlPattern = Pattern.compile("api:\\s*\"(https://.+?.appsync-api..+?.amazonaws.com/graphql)\",");
private final Pattern graphQlApiKeyPattern = Pattern.compile("apiKey:\\s*\"(.*?)\"");
private String graphqlBaseUri;
private String apiKey;
private MVLiveHttpClient httpClient;
private String mvtoken;
@Override
public String getName() {
return "MV Live";
}
@Override
public String getBaseUrl() {
return LIVE_URL;
}
@Override
public String getAffiliateLink() {
return getBaseUrl() + "/Join-MV/1002294529";
}
@Override
public MVLiveModel createModel(String name) {
MVLiveModel model = new MVLiveModel();
model.setName(name);
model.setDescription("");
model.setSite(this);
model.setUrl(WS_ORIGIN + '/' + name);
return model;
}
@Override
public Double getTokenBalance() throws IOException {
return 0d;
}
@Override
public String getBuyTokensLink() {
return getAffiliateLink();
}
@Override
public boolean login() throws IOException {
return false;
}
@Override
public HttpClient getHttpClient() {
if (httpClient == null) {
httpClient = new MVLiveHttpClient(getConfig());
}
return httpClient;
}
@Override
public void shutdown() {
if (httpClient != null) {
httpClient.shutdown();
}
}
@Override
public boolean supportsTips() {
return false;
}
@Override
public boolean supportsFollow() {
return false;
}
@Override
public boolean isSiteForModel(Model m) {
return m instanceof MVLiveModel;
}
@Override
public boolean credentialsAvailable() {
return false;
}
@Override
public void init() {
// nothing to do
}
private String getGraphQlUrl() {
if (graphqlBaseUri == null) {
try {
String spaBundleUrl = getSpaBundleUrl();
loadGraphqlApiConfig(spaBundleUrl);
LOG.debug("Using graphql API at {}", graphqlBaseUri);
} catch (IOException e) {
LOG.error("Error while initializing {}", getName(), e);
}
}
return graphqlBaseUri;
}
private void loadGraphqlApiConfig(String spaBundleUrl) throws IOException {
Request request = new Request.Builder()
.url(spaBundleUrl)
.header(USER_AGENT, getConfig().getSettings().httpUserAgent)
.header(ORIGIN, MVLive.BASE_URL)
.header(REFERER, MVLive.BASE_URL)
.build();
LOG.debug("Loading graphql config {}", spaBundleUrl);
try (Response response = getHttpClient().execute(request)) {
if (response.isSuccessful()) {
String content = response.body().string();
Matcher m = graphQlUrlPattern.matcher(content);
if (m.find()) {
graphqlBaseUri = m.group(1);
m = graphQlApiKeyPattern.matcher(content);
if (m.find()) {
apiKey = m.group(1);
} else {
throw new IllegalStateException("GraphQL API key not found");
}
} else {
throw new IllegalStateException("GraphQL URL not found");
}
} else {
throw new HttpException(response.code(), response.message());
}
}
}
private String getSpaBundleUrl() throws IOException {
Request request = new Request.Builder()
.url(getBaseUrl())
.header(USER_AGENT, getConfig().getSettings().httpUserAgent)
.header(REFERER, MVLive.BASE_URL)
.build();
try (Response response = getHttpClient().execute(request)) {
if (response.isSuccessful()) {
String content = response.body().string();
Matcher m = configPattern.matcher(content);
if (m.find()) {
JSONObject apps = new JSONObject(m.group(1)).getJSONObject("apps");
JSONObject mvlive = apps.getJSONObject("@manyvids/live");
String spaBundle = mvlive.getString("spaBundle");
return spaBundle;
} else {
throw new IllegalStateException("config not found");
}
} else {
throw new HttpException(response.code(), response.message());
}
}
}
public List getModels() throws IOException {
String body = new JSONObject()
.put("query", "query LanderLiveSessions($nextToken: String, $limit: Int, $country: String, $subdivision: String, $audience: String) { liveSessions( nextToken: $nextToken limit: $limit country: $country subdivision: $subdivision audience: $audience ) { presenters { id age avatarSrc city country name previewImgSrc starOfTheDay status sessionType streamingStartDate towerImgSrc profileHandle } nextToken }}")
.put("variables", new JSONObject()
.put("audience", "public")
.put("country", Locale.getDefault().getDisplayCountry(Locale.ENGLISH))
.put("limit", 100)
.put("nextToken", JSONObject.NULL)
.put("subdivision", "")
).toString(2);
RequestBody requestBody = RequestBody.Companion.create(body, MediaType.parse("application/json"));
Request request = new Request.Builder()
.url(getGraphQlUrl())
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(USER_AGENT, getConfig().getSettings().httpUserAgent)
.header(ORIGIN, MVLive.BASE_URL)
.header(REFERER, MVLive.BASE_URL)
.header("x-api-key", apiKey)
.post(requestBody)
.build();
try (Response response = getHttpClient().execute(request)) {
String content = response.body().string();
if (response.isSuccessful()) {
JSONObject responseBody = new JSONObject(content);
JSONArray presenters = responseBody.getJSONObject("data").getJSONObject("liveSessions").getJSONArray("presenters");
List models = new LinkedList<>();
for (int i = 0; i < presenters.length(); i++) {
JSONObject presenter = presenters.getJSONObject(i);
try {
MVLiveModel model = createModel(presenter.getString("name"));
model.setId(presenter.getString("id"));
model.setDisplayName(presenter.optString("name", model.getName()));
model.setPreview(presenter.optString("towerImgSrc"));
model.setOnlineState(mapState(presenter.getString("sessionType")));
models.add(model);
} catch (Exception e) {
LOG.error("Couldn't parse model: {}", presenter.toString(2), e);
}
}
return models;
} else {
LOG.debug("Response: {}", content);
throw new HttpException(response.code(), response.message());
}
}
}
private State mapState(String status) {
return switch (status) {
case "PUBLIC" -> State.ONLINE;
case "PRIVATE" -> State.PRIVATE;
default -> State.OFFLINE;
};
}
@Override
public boolean supportsSearch() {
return true;
}
@Override
public boolean searchRequiresLogin() {
return false;
}
String getMvToken() throws IOException {
if (mvtoken == null) {
Request request = new Request.Builder()
.url("https://www.manyvids.com/")
.header(USER_AGENT, getConfig().getSettings().httpUserAgent)
.build();
try (Response response = getHttpClient().execute(request)) {
if (response.isSuccessful()) {
Element tag = HtmlParser.getTag(response.body().string(), "html");
mvtoken = tag.attr("data-mvtoken");
} else {
throw new HttpException(response.code(), response.message());
}
}
}
return mvtoken;
}
@Override
public List search(String q) throws IOException, InterruptedException {
List result = new ArrayList<>();
RequestBody body = new FormBody.Builder()
.add("mvtoken", getMvToken())
.add("type", "search")
.add("category", STARS)
.add("search", q)
.build();
Request request = new Request.Builder()
.url("https://www.manyvids.com/includes/filterSearch.php")
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(USER_AGENT, getConfig().getSettings().httpUserAgent)
.header(ORIGIN, MVLive.BASE_URL)
.header(REFERER, MVLive.BASE_URL)
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.post(body)
.build();
try (Response response = getHttpClient().execute(request)) {
if (response.isSuccessful()) {
String responseBody = response.body().string();
parseSearchResult(result, responseBody);
} else {
throw new HttpException(response.code(), response.message());
}
}
return result;
}
private void parseSearchResult(List result, String responseBody) {
JSONObject json = new JSONObject(responseBody);
if (json.has(STARS)) {
JSONArray stars = json.getJSONArray(STARS);
for (int i = 0; i < stars.length(); i++) {
JSONObject star = stars.getJSONObject(i);
String name = star.getString("label");
MVLiveModel model = createModel(name);
long id = star.getLong("id");
String url = BASE_URL + "/Profile/" + id + '/' + model.getDisplayName().replace(" ", "-") + '/';
model.setUrl(url);
model.setPreview(star.getString("img"));
if (star.optString("is_live").equals("1")) {
if (star.optString("is_private").equals("1")) {
model.setOnlineState(State.PRIVATE);
} else {
model.setOnlineState(State.ONLINE);
}
} else {
model.setOnlineState(State.OFFLINE);
}
result.add(model);
}
}
}
@Override
public Model createModelFromUrl(String url) {
Matcher m = Pattern.compile("https://live.manyvids.com/(?:stream/)?(.*?)(?:/.*?)?").matcher(url.trim());
if (m.matches()) {
String modelName = URLDecoder.decode(m.group(1), UTF_8);
return createModel(modelName);
}
m = Pattern.compile("https://www.manyvids.com/MVLive/(.*?)/\\d+/?").matcher(url.trim());
if (m.matches()) {
String modelName = URLDecoder.decode(m.group(1), UTF_8);
return createModel(modelName);
}
return super.createModelFromUrl(url);
}
}