diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index ca40f8d6..831fbbf3 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -22,6 +22,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import ctbrec.sites.secretfriends.SecretFriends; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -179,6 +180,7 @@ public class CamrecApplication extends Application { sites.add(new LiveJasmin()); sites.add(new MVLive()); sites.add(new MyFreeCams()); + sites.add(new SecretFriends()); sites.add(new Showup()); sites.add(new Streamate()); sites.add(new Stripchat()); diff --git a/client/src/main/java/ctbrec/ui/SiteUiFactory.java b/client/src/main/java/ctbrec/ui/SiteUiFactory.java index b9056890..b97c1760 100644 --- a/client/src/main/java/ctbrec/ui/SiteUiFactory.java +++ b/client/src/main/java/ctbrec/ui/SiteUiFactory.java @@ -11,6 +11,7 @@ import ctbrec.sites.flirt4free.Flirt4Free; import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.manyvids.MVLive; import ctbrec.sites.mfc.MyFreeCams; +import ctbrec.sites.secretfriends.SecretFriends; import ctbrec.sites.showup.Showup; import ctbrec.sites.streamate.Streamate; import ctbrec.sites.stripchat.Stripchat; @@ -25,6 +26,7 @@ import ctbrec.ui.sites.flirt4free.Flirt4FreeSiteUi; import ctbrec.ui.sites.jasmin.LiveJasminSiteUi; import ctbrec.ui.sites.manyvids.MVLiveSiteUi; import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi; +import ctbrec.ui.sites.secretfriends.SecretFriendsSiteUi; import ctbrec.ui.sites.showup.ShowupSiteUi; import ctbrec.ui.sites.streamate.StreamateSiteUi; import ctbrec.ui.sites.stripchat.StripchatSiteUi; @@ -42,6 +44,7 @@ public class SiteUiFactory { private static LiveJasminSiteUi jasminSiteUi; private static MVLiveSiteUi mvLiveSiteUi; private static MyFreeCamsSiteUi mfcSiteUi; + private static SecretFriendsSiteUi secretFriendsSiteUi; private static ShowupSiteUi showupSiteUi; private static StreamateSiteUi streamateSiteUi; private static StripchatSiteUi stripchatSiteUi; @@ -95,6 +98,11 @@ public class SiteUiFactory { mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site); } return mfcSiteUi; + } else if (site instanceof SecretFriends) { + if (secretFriendsSiteUi == null) { + secretFriendsSiteUi = new SecretFriendsSiteUi((SecretFriends) site); + } + return secretFriendsSiteUi; } else if (site instanceof Showup) { if (showupSiteUi == null) { showupSiteUi = new ShowupSiteUi((Showup) site); diff --git a/client/src/main/java/ctbrec/ui/sites/secretfriends/SecretFriendsConfigUI.java b/client/src/main/java/ctbrec/ui/sites/secretfriends/SecretFriendsConfigUI.java new file mode 100644 index 00000000..f52bb11e --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/secretfriends/SecretFriendsConfigUI.java @@ -0,0 +1,89 @@ + +package ctbrec.ui.sites.secretfriends; + +import ctbrec.Config; +import ctbrec.sites.secretfriends.SecretFriends; +import ctbrec.ui.settings.SettingsTab; +import ctbrec.ui.sites.AbstractConfigUI; +import javafx.geometry.Insets; +import javafx.scene.Parent; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; + +public class SecretFriendsConfigUI extends AbstractConfigUI { + private final SecretFriends site; + + public SecretFriendsConfigUI(SecretFriends secretFriends) { + this.site = secretFriends; + } + + @Override + public Parent createConfigPanel() { + GridPane layout = SettingsTab.createGridLayout(); + var settings = Config.getInstance().getSettings(); + + var row = 0; + var l = new Label("Active"); + layout.add(l, 0, row); + var enabled = new CheckBox(); + enabled.setSelected(!settings.disabledSites.contains(site.getName())); + enabled.setOnAction(e -> { + if(enabled.isSelected()) { + settings.disabledSites.remove(site.getName()); + } else { + settings.disabledSites.add(site.getName()); + } + save(); + }); + GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + layout.add(enabled, 1, row++); + +// layout.add(new Label(site.getName() + " User"), 0, row); +// var username = new TextField(Config.getInstance().getSettings().stripchatUsername); +// username.textProperty().addListener((ob, o, n) -> { +// if(!n.equals(Config.getInstance().getSettings().stripchatUsername)) { +// Config.getInstance().getSettings().stripchatUsername = username.getText(); +// site.getHttpClient().logout(); +// save(); +// } +// }); +// GridPane.setFillWidth(username, true); +// GridPane.setHgrow(username, Priority.ALWAYS); +// GridPane.setColumnSpan(username, 2); +// layout.add(username, 1, row++); +// +// layout.add(new Label(site.getName() + " Password"), 0, row); +// var password = new PasswordField(); +// password.setText(Config.getInstance().getSettings().stripchatPassword); +// password.textProperty().addListener((ob, o, n) -> { +// if(!n.equals(Config.getInstance().getSettings().stripchatPassword)) { +// Config.getInstance().getSettings().stripchatPassword = password.getText(); +// site.getHttpClient().logout(); +// save(); +// } +// }); +// GridPane.setFillWidth(password, true); +// GridPane.setHgrow(password, Priority.ALWAYS); +// GridPane.setColumnSpan(password, 2); +// layout.add(password, 1, row++); + +// var createAccount = new Button("Create new Account"); +// createAccount.setOnAction(e -> DesktopIntegration.open(site.getAffiliateLink())); +// layout.add(createAccount, 1, row++); +// GridPane.setColumnSpan(createAccount, 2); + + var deleteCookies = new Button("Delete Cookies"); + deleteCookies.setOnAction(e -> site.getHttpClient().clearCookies()); + layout.add(deleteCookies, 1, row); + GridPane.setColumnSpan(deleteCookies, 2); + +// GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); +// GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); +// GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(deleteCookies, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + return layout; + } + +} diff --git a/client/src/main/java/ctbrec/ui/sites/secretfriends/SecretFriendsSiteUi.java b/client/src/main/java/ctbrec/ui/sites/secretfriends/SecretFriendsSiteUi.java new file mode 100644 index 00000000..6164ac0e --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/secretfriends/SecretFriendsSiteUi.java @@ -0,0 +1,40 @@ +package ctbrec.ui.sites.secretfriends; + +import ctbrec.sites.secretfriends.SecretFriends; +import ctbrec.ui.sites.AbstractSiteUi; +import ctbrec.ui.sites.ConfigUI; +import ctbrec.ui.tabs.TabProvider; + +import java.io.IOException; + +public class SecretFriendsSiteUi extends AbstractSiteUi { + + private SecretFriendsTabProvider tabProvider; + private SecretFriendsConfigUI configUi; + private final SecretFriends site; + + public SecretFriendsSiteUi(SecretFriends site) { + this.site = site; + } + + @Override + public TabProvider getTabProvider() { + if (tabProvider == null) { + tabProvider = new SecretFriendsTabProvider(site); + } + return tabProvider; + } + + @Override + public ConfigUI getConfigUI() { + if (configUi == null) { + configUi = new SecretFriendsConfigUI(site); + } + return configUi; + } + + @Override + public synchronized boolean login() throws IOException { + return site.login(); + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/secretfriends/SecretFriendsTabProvider.java b/client/src/main/java/ctbrec/ui/sites/secretfriends/SecretFriendsTabProvider.java new file mode 100644 index 00000000..ced85dc7 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/secretfriends/SecretFriendsTabProvider.java @@ -0,0 +1,58 @@ +package ctbrec.ui.sites.secretfriends; + +import ctbrec.recorder.Recorder; +import ctbrec.sites.secretfriends.SecretFriends; +import ctbrec.ui.tabs.TabProvider; +import ctbrec.ui.tabs.ThumbOverviewTab; +import javafx.scene.Scene; +import javafx.scene.control.Tab; + +import java.util.ArrayList; +import java.util.List; + +public class SecretFriendsTabProvider implements TabProvider { + + private final SecretFriends site; + private final Recorder recorder; + +// StripchatFollowedTab followedTab; + + public SecretFriendsTabProvider(SecretFriends site) { + this.site = site; + this.recorder = site.getRecorder(); + //followedTab = new StripchatFollowedTab("Followed", site); + } + + @Override + public List getTabs(Scene scene) { + List tabs = new ArrayList<>(); + tabs.add(createTab("Girls", SecretFriends.BASE_URI + "/users")); + tabs.add(createTab("New", SecretFriends.BASE_URI + "/newgirls")); + tabs.add(createTab("Couples", SecretFriends.BASE_URI + "/site/couple")); + //tabs.add(createTab("Vibrating Toy", SecretFriends.BASE_URI + "/tag/view?slug=vibrating-toy")); +// tabs.add(createTab("Girls HD", site.getBaseUrl() + "/api/front/models?primaryTag=girls&filterGroupTags=%5B%5B%22autoTagHd%22%5D%5D&parentTag=autoTagHd")); +// tabs.add(createTab("New Girls", site.getBaseUrl() +"/api/front/models?primaryTag=girls&filterGroupTags=%5B%5B%22autoTagNew%22%5D%5D&parentTag=autoTagNew")); +// tabs.add(createTab("Couples", MessageFormat.format(urlTemplate, "couples"))); +// tabs.add(createTab("Boys", MessageFormat.format(urlTemplate, "men"))); +// tabs.add(createTab("Trans", MessageFormat.format(urlTemplate, "trans"))); +// followedTab.setRecorder(recorder); +// followedTab.setScene(scene); +// followedTab.setImageAspectRatio(9.0 / 16.0); +// tabs.add(followedTab); + return tabs; + } + + @Override + public Tab getFollowedTab() { +// return followedTab; + return null; + } + + private Tab createTab(String title, String url) { + var updateService = new SecretFriendsUpdateService(url, false, site); + var tab = new ThumbOverviewTab(title, updateService, site); + tab.setRecorder(recorder); + return tab; + } + +} diff --git a/client/src/main/java/ctbrec/ui/sites/secretfriends/SecretFriendsUpdateService.java b/client/src/main/java/ctbrec/ui/sites/secretfriends/SecretFriendsUpdateService.java new file mode 100644 index 00000000..c9095ec9 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/secretfriends/SecretFriendsUpdateService.java @@ -0,0 +1,85 @@ +package ctbrec.ui.sites.secretfriends; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HtmlParser; +import ctbrec.io.HttpException; +import ctbrec.sites.secretfriends.SecretFriends; +import ctbrec.sites.secretfriends.SecretFriendsModelParser; +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.*; + +import static ctbrec.io.HttpConstants.*; + +public class SecretFriendsUpdateService extends PaginatedScheduledService { + + private static final Logger LOG = LoggerFactory.getLogger(SecretFriendsUpdateService.class); + + private final String url; + private final boolean loginRequired; + private final SecretFriends site; + + public SecretFriendsUpdateService(String url, boolean loginRequired, SecretFriends site) { + this.url = url; + this.loginRequired = loginRequired; + this.site = site; + } + + @Override + protected Task> createTask() { + return new Task<>() { + @Override + public List call() throws IOException { + if (loginRequired && !site.credentialsAvailable()) { + return Collections.emptyList(); + } else { + String paginatedUrl = url + (url.indexOf('?') >= 0 ? '&' : '?') + "Friend_page=" + page; + LOG.debug("Fetching page {}", paginatedUrl); + if (loginRequired) { + SiteUiFactory.getUi(site).login(); + } + var request = new Request.Builder() + .url(paginatedUrl) + .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) + .build(); + try (var response = site.getHttpClient().execute(request)) { + if (response.isSuccessful()) { + return parseModels(Objects.requireNonNull(response.body(), "HTTP response body is null").string()); + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + } + }; + } + + private List parseModels(String body) { + List models = new ArrayList<>(); + Elements modelDivs = HtmlParser.getTags(body, "div[class~=model-wrapper]"); + LOG.debug("Found {} models", modelDivs.size()); + for (Element div : modelDivs) { + try { + models.add(SecretFriendsModelParser.parse(site, div)); + } catch (Exception e) { + LOG.warn("Couldn't parse one of the models: {}", div.html(), e); + } + } + return models; + } + + +} diff --git a/common/src/main/java/ctbrec/sites/secretfriends/SecretFriends.java b/common/src/main/java/ctbrec/sites/secretfriends/SecretFriends.java new file mode 100644 index 00000000..fc7af2a7 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/secretfriends/SecretFriends.java @@ -0,0 +1,152 @@ +package ctbrec.sites.secretfriends; + +import ctbrec.Model; +import ctbrec.io.HtmlParser; +import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; +import ctbrec.sites.AbstractSite; +import okhttp3.Request; +import okhttp3.Response; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static ctbrec.io.HttpConstants.USER_AGENT; + +public class SecretFriends extends AbstractSite { + + private static final Logger LOG = LoggerFactory.getLogger(SecretFriends.class); + public static final String BASE_URI = "https://www.secretfriends.com"; + private HttpClient httpClient; + + @Override + public void init() throws IOException { + // nothing to do + } + + @Override + public String getName() { + return "SecretFriends"; + } + + @Override + public String getBaseUrl() { + return BASE_URI; + } + + @Override + public String getAffiliateLink() { + return BASE_URI; + } + + @Override + public String getBuyTokensLink() { + return getAffiliateLink(); + } + + @Override + public SecretFriendsModel createModel(String name) { + SecretFriendsModel model = new SecretFriendsModel(); + model.setName(name); + model.setUrl(getBaseUrl() + "/friends/" + name); + model.setSite(this); + return model; + } + + @Override + public Double getTokenBalance() throws IOException { + return 0d; + } + + @Override + public synchronized boolean login() throws IOException { + return false; + } + + @Override + public HttpClient getHttpClient() { + if (httpClient == null) { + httpClient = new SecretFriendsHttpClient(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 supportsSearch() { + return true; + } + + @Override + public List search(String q) throws IOException, InterruptedException { + String url = BASE_URI + "/user?SearchForm[keyword]=" + URLEncoder.encode(q, "utf-8"); + Request req = new Request.Builder() + .url(url) + .header(USER_AGENT, getConfig().getSettings().httpUserAgent) + .build(); + try (Response response = getHttpClient().execute(req)) { + if (response.isSuccessful()) { + String body = Objects.requireNonNull(response.body(), "HTTP response body is null").string(); + List models = new ArrayList<>(); + Elements modelDivs = HtmlParser.getTags(body, "div[class~=model-wrapper]"); + LOG.debug("Found {} models", modelDivs.size()); + for (Element div : modelDivs) { + try { + models.add(SecretFriendsModelParser.parse(this, div)); + } catch (Exception e) { + LOG.warn("Couldn't parse one of the models: {}", div.html(), e); + } + } + return models; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + @Override + public boolean isSiteForModel(Model m) { + return m instanceof SecretFriendsModel; + } + + @Override + public boolean credentialsAvailable() { + return false; + } + + @Override + public Model createModelFromUrl(String url) { + Matcher m = Pattern.compile("https?://(?:www\\.)?secretfriends.com/friends/([^/]*?)/?").matcher(url); + if (m.matches()) { + String modelName = m.group(1); + return createModel(modelName); + } else { + return super.createModelFromUrl(url); + } + } +} diff --git a/common/src/main/java/ctbrec/sites/secretfriends/SecretFriendsHttpClient.java b/common/src/main/java/ctbrec/sites/secretfriends/SecretFriendsHttpClient.java new file mode 100644 index 00000000..db5fa62f --- /dev/null +++ b/common/src/main/java/ctbrec/sites/secretfriends/SecretFriendsHttpClient.java @@ -0,0 +1,174 @@ +package ctbrec.sites.secretfriends; + +import ctbrec.Config; +import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import static ctbrec.io.HttpConstants.*; + +public class SecretFriendsHttpClient extends HttpClient { + + private static final Logger LOG = LoggerFactory.getLogger(SecretFriendsHttpClient.class); + public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + + private long userId; + private String csrfToken; + private String csrfTimestamp; + private String csrfNotifyTimestamp; + + public SecretFriendsHttpClient(Config config) { + super("secretfirends", config); + } + + @Override + public boolean login() throws IOException { + if (loggedIn) { + if (csrfToken == null) { + loadCsrfToken(); + } + return true; + } + + // persisted cookies might let us log in + if (checkLoginSuccess()) { + loggedIn = true; + LOG.debug("Logged in with cookies"); + if (csrfToken == null) { + loadCsrfToken(); + } + return true; + } + + if (csrfToken == null) { + loadCsrfToken(); + } + + String url = SecretFriends.BASE_URI + "/api/front/auth/login"; + JSONObject requestParams = new JSONObject(); + requestParams.put("loginOrEmail", config.getSettings().stripchatUsername); + requestParams.put("password", config.getSettings().stripchatPassword); + requestParams.put("remember", true); + requestParams.put("csrfToken", csrfToken); + requestParams.put("csrfTimestamp", csrfTimestamp); + requestParams.put("csrfNotifyTimestamp", csrfNotifyTimestamp); + RequestBody body = RequestBody.Companion.create(requestParams.toString(), JSON); + Request request = new Request.Builder() + .url(url) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(USER_AGENT, config.getSettings().httpUserAgent) + .header(ORIGIN, SecretFriends.BASE_URI) + .header(REFERER, SecretFriends.BASE_URI) + .header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON) + .post(body) + .build(); + try (Response response = execute(request)) { + if (response.isSuccessful()) { + JSONObject resp = new JSONObject(response.body().string()); + if(resp.has("user")) { + JSONObject user = resp.getJSONObject("user"); + userId = user.optLong("id"); + return true; + } else { + return false; + } + } else { + LOG.info("Auto-Login failed: {} {} {}", response.code(), response.message(), url); + return false; + } + } + } + + private void loadCsrfToken() throws IOException { + String url = SecretFriends.BASE_URI + "/api/front/v2/config/data?requestPath=%2F&timezoneOffset=0"; + Request request = new Request.Builder() + .url(url) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(USER_AGENT, config.getSettings().httpUserAgent) + .header(ORIGIN, SecretFriends.BASE_URI) + .header(REFERER, SecretFriends.BASE_URI) + .header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON) + .build(); + try (Response response = execute(request)) { + if (response.isSuccessful()) { + JSONObject resp = new JSONObject(response.body().string()); + JSONObject data = resp.getJSONObject("data"); + csrfToken = data.optString("csrfToken"); + csrfTimestamp = data.optString("csrfTimestamp"); + csrfNotifyTimestamp = data.optString("csrfNotifyTimestamp"); + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + /** + * check, if the login worked + * @throws IOException + */ + public boolean checkLoginSuccess() throws IOException { + userId = getUserId(); + String url = SecretFriends.BASE_URI + "/api/front/users/" + userId + "/favorites"; + Request request = new Request.Builder() + .url(url) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(USER_AGENT, config.getSettings().httpUserAgent) + .header(ORIGIN, SecretFriends.BASE_URI) + .header(REFERER, SecretFriends.BASE_URI + "/favorites") + .header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON) + .build(); + try (Response response = execute(request)) { + if (response.isSuccessful()) { + return true; + } + } catch (Exception e) { + LOG.info("Login check returned unsuccessful: {}", e.getLocalizedMessage()); + } + return false; + } + + public long getUserId() throws JSONException, IOException { + if (userId == 0) { + String url = SecretFriends.BASE_URI + "/api/front/users/username/" + config.getSettings().stripchatUsername; + Request request = new Request.Builder() + .url(url) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(USER_AGENT, config.getSettings().httpUserAgent) + .header(ORIGIN, SecretFriends.BASE_URI) + .header(REFERER, SecretFriends.BASE_URI) + .header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON) + .build(); + try (Response response = execute(request)) { + if (response.isSuccessful()) { + JSONObject resp = new JSONObject(response.body().string()); + JSONObject user = resp.getJSONObject("user"); + userId = user.optLong("id"); + } else { + throw new HttpException(url, response.code(), response.message()); + } + } + } + return userId; + } + + public String getCsrfNotifyTimestamp() { + return csrfNotifyTimestamp; + } + + public String getCsrfTimestamp() { + return csrfTimestamp; + } + + public String getCsrfToken() { + return csrfToken; + } +} diff --git a/common/src/main/java/ctbrec/sites/secretfriends/SecretFriendsModel.java b/common/src/main/java/ctbrec/sites/secretfriends/SecretFriendsModel.java new file mode 100644 index 00000000..4d06f2f1 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/secretfriends/SecretFriendsModel.java @@ -0,0 +1,117 @@ +package ctbrec.sites.secretfriends; + +import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.PlaylistException; +import ctbrec.AbstractModel; +import ctbrec.Config; +import ctbrec.io.HtmlParser; +import ctbrec.io.HttpException; +import ctbrec.recorder.download.StreamSource; +import okhttp3.Request; +import okhttp3.Response; +import org.jsoup.nodes.Element; + +import javax.xml.bind.JAXBException; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.ExecutionException; + +import static ctbrec.Model.State.ONLINE; +import static ctbrec.io.HttpConstants.*; + +public class SecretFriendsModel extends AbstractModel { + + private String status = null; + private int[] resolution = new int[]{0, 0}; + + @Override + public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { + if (ignoreCache || status == null) { + String url = SecretFriends.BASE_URI + "/friend/bio/" + getName(); + 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 = Objects.requireNonNull(response.body(), "HTTP response body is null").string(); + Element wrapper = HtmlParser.getTag(body, "div[class~=model-wrapper]"); + SecretFriendsModel parsedModel = SecretFriendsModelParser.parse((SecretFriends) getSite(), wrapper); + setName(parsedModel.getName()); + setUrl(parsedModel.getUrl()); + setPreview(parsedModel.getPreview()); + setOnlineState(parsedModel.getOnlineState(true)); + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + return onlineState == ONLINE; + } + + @Override + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException { + 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()) { + return Collections.emptyList(); + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + + @Override + public void invalidateCacheEntries() { + status = null; + resolution = new int[]{0, 0}; + } + + @Override + public void receiveTip(Double tokens) throws IOException { + // not implemented + } + + @Override + public int[] getStreamResolution(boolean failFast) throws ExecutionException { + if (!failFast) { + try { + List sources = getStreamSources(); + if (!sources.isEmpty()) { + StreamSource best = sources.get(sources.size() - 1); + resolution = new int[]{best.getWidth(), best.getHeight()}; + } + } catch (IOException | ParseException | PlaylistException | JAXBException e) { + throw new ExecutionException(e); + } + } + return resolution; + } + + @Override + public boolean follow() throws IOException { + return false; + } + + @Override + public boolean unfollow() throws IOException { + return false; + } +} diff --git a/common/src/main/java/ctbrec/sites/secretfriends/SecretFriendsModelParser.java b/common/src/main/java/ctbrec/sites/secretfriends/SecretFriendsModelParser.java new file mode 100644 index 00000000..623accd9 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/secretfriends/SecretFriendsModelParser.java @@ -0,0 +1,71 @@ +package ctbrec.sites.secretfriends; + +import ctbrec.Model; +import org.jsoup.nodes.Element; + +import java.util.Objects; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SecretFriendsModelParser { + private SecretFriendsModelParser() { + } + + public static SecretFriendsModel parse(SecretFriends site, Element modelWrapper) { + String name = parseName(modelWrapper); + SecretFriendsModel model = site.createModel(name); + model.setPreview(parsePreview(modelWrapper)); + model.setOnlineState(extractOnlineState(modelWrapper)); + return model; + } + + private static String parsePreview(Element div) { + Element wrapper = div.selectFirst("div.placeholder-wrapper"); + if (wrapper == null) { + return null; + } + String style = wrapper.attr("style"); + Pattern p = Pattern.compile("background-image: url\\('(.*?)'\\)"); + Matcher m = p.matcher(style); + if (m.find()) { + return m.group(1); + } else { + return null; + } + } + + private static String parseName(Element div) { + Element bioLink = Objects.requireNonNull(div.selectFirst("a[href*=/friend]"), "a[href*=/friend] not found"); + bioLink.setBaseUri(SecretFriends.BASE_URI); + String href = bioLink.attr("href"); + String name = href.substring(href.lastIndexOf('/') + 1); + if (name.indexOf('?') >= 0) { + name = name.substring(0, name.indexOf('?')); + } + if (name.indexOf('#') >= 0) { + name = name.substring(0, name.indexOf('#')); + } + return name; + } + + private static Model.State extractOnlineState(Element div) { + Element modelTag = Objects.requireNonNull(div.selectFirst("div[class~=model-tag]"), "div.model-tag not found"); + Set cssClasses = modelTag.classNames(); + for (String cssClass : cssClasses) { + switch (cssClass) { + case "model-online": + return Model.State.ONLINE; + case "model-private": + case "model-show": + case "model-vip": + return Model.State.PRIVATE; + case "model-offline": + return Model.State.OFFLINE; + default: + // keep going + } + } + return Model.State.OFFLINE; + } +}