diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 562d88a7..d62e01f5 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -47,6 +47,7 @@ import ctbrec.sites.flirt4free.Flirt4Free; import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.streamate.Streamate; +import ctbrec.sites.stripchat.Stripchat; import ctbrec.ui.news.NewsTab; import ctbrec.ui.settings.SettingsTab; import ctbrec.ui.tabs.DonateTabFx; @@ -98,6 +99,7 @@ public class CamrecApplication extends Application { sites.add(new LiveJasmin()); sites.add(new MyFreeCams()); sites.add(new Streamate()); + sites.add(new Stripchat()); loadConfig(); registerAlertSystem(); registerActiveRecordingsCounter(); diff --git a/client/src/main/java/ctbrec/ui/SiteUiFactory.java b/client/src/main/java/ctbrec/ui/SiteUiFactory.java index 2135d854..65ec1496 100644 --- a/client/src/main/java/ctbrec/ui/SiteUiFactory.java +++ b/client/src/main/java/ctbrec/ui/SiteUiFactory.java @@ -10,6 +10,7 @@ import ctbrec.sites.flirt4free.Flirt4Free; import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.streamate.Streamate; +import ctbrec.sites.stripchat.Stripchat; import ctbrec.ui.sites.bonga.BongaCamsSiteUi; import ctbrec.ui.sites.cam4.Cam4SiteUi; import ctbrec.ui.sites.camsoda.CamsodaSiteUi; @@ -19,6 +20,7 @@ import ctbrec.ui.sites.flirt4free.Flirt4FreeSiteUi; import ctbrec.ui.sites.jasmin.LiveJasminSiteUi; import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi; import ctbrec.ui.sites.streamate.StreamateSiteUi; +import ctbrec.ui.sites.stripchat.StripchatSiteUi; public class SiteUiFactory { @@ -31,6 +33,9 @@ public class SiteUiFactory { private static LiveJasminSiteUi jasminSiteUi; private static MyFreeCamsSiteUi mfcSiteUi; private static StreamateSiteUi streamateSiteUi; + private static StripchatSiteUi stripchatSiteUi; + + private SiteUiFactory () {} public static synchronized SiteUI getUi(Site site) { if (site instanceof BongaCams) { @@ -78,6 +83,11 @@ public class SiteUiFactory { jasminSiteUi = new LiveJasminSiteUi((LiveJasmin) site); } return jasminSiteUi; + } else if (site instanceof Stripchat) { + if (stripchatSiteUi == null) { + stripchatSiteUi = new StripchatSiteUi((Stripchat) site); + } + return stripchatSiteUi; } throw new RuntimeException("Unknown site " + site.getName()); } diff --git a/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatConfigUI.java b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatConfigUI.java new file mode 100644 index 00000000..1a0f745e --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatConfigUI.java @@ -0,0 +1,86 @@ +package ctbrec.ui.sites.stripchat; + +import ctbrec.Config; +import ctbrec.Settings; +import ctbrec.sites.stripchat.Stripchat; +import ctbrec.ui.DesktopIntegration; +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.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; + +public class StripchatConfigUI extends AbstractConfigUI { + private Stripchat stripchat; + + public StripchatConfigUI(Stripchat stripchat) { + this.stripchat = stripchat; + } + + @Override + public Parent createConfigPanel() { + GridPane layout = SettingsTab.createGridLayout(); + Settings settings = Config.getInstance().getSettings(); + + int row = 0; + Label l = new Label("Active"); + layout.add(l, 0, row); + CheckBox enabled = new CheckBox(); + enabled.setSelected(!settings.disabledSites.contains(stripchat.getName())); + enabled.setOnAction(e -> { + if(enabled.isSelected()) { + settings.disabledSites.remove(stripchat.getName()); + } else { + settings.disabledSites.add(stripchat.getName()); + } + save(); + }); + GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + layout.add(enabled, 1, row++); + + layout.add(new Label("Stripchat User"), 0, row); + TextField 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(); + stripchat.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("Stripchat Password"), 0, row); + PasswordField 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(); + stripchat.getHttpClient().logout(); + save(); + } + }); + GridPane.setFillWidth(password, true); + GridPane.setHgrow(password, Priority.ALWAYS); + GridPane.setColumnSpan(password, 2); + layout.add(password, 1, row++); + + Button createAccount = new Button("Create new Account"); + createAccount.setOnAction(e -> DesktopIntegration.open(stripchat.getAffiliateLink())); + layout.add(createAccount, 1, row); + GridPane.setColumnSpan(createAccount, 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)); + return layout; + } + +} diff --git a/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatFollowedTab.java b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatFollowedTab.java new file mode 100644 index 00000000..f82525ab --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatFollowedTab.java @@ -0,0 +1,79 @@ +package ctbrec.ui.sites.stripchat; + +import ctbrec.sites.stripchat.Stripchat; +import ctbrec.ui.tabs.FollowedTab; +import ctbrec.ui.tabs.ThumbOverviewTab; +import javafx.concurrent.WorkerStateEvent; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; + +public class StripchatFollowedTab extends ThumbOverviewTab implements FollowedTab { + private Label status; + boolean showOnline = true; + + public StripchatFollowedTab(String title, Stripchat stripchat) { + super(title, new StripchatFollowedUpdateService(stripchat), stripchat); + status = new Label("Logging in..."); + grid.getChildren().add(status); + } + + @Override + protected void createGui() { + super.createGui(); + addOnlineOfflineSelector(); + } + + private void addOnlineOfflineSelector() { + ToggleGroup group = new ToggleGroup(); + RadioButton online = new RadioButton("online"); + online.setToggleGroup(group); + RadioButton offline = new RadioButton("offline"); + offline.setToggleGroup(group); + pagination.getChildren().add(online); + pagination.getChildren().add(offline); + HBox.setMargin(online, new Insets(5, 5, 5, 40)); + HBox.setMargin(offline, new Insets(5, 5, 5, 5)); + online.setSelected(true); + group.selectedToggleProperty().addListener(e -> { + queue.clear(); + ((StripchatFollowedUpdateService)updateService).showOnline(online.isSelected()); + updateService.restart(); + }); + } + + @Override + protected void onSuccess() { + grid.getChildren().remove(status); + super.onSuccess(); + } + + @Override + protected void onFail(WorkerStateEvent event) { + String msg = ""; + if (event.getSource().getException() != null) { + msg = ": " + event.getSource().getException().getMessage(); + } + status.setText("Login failed" + msg); + super.onFail(event); + } + + @Override + public void selected() { + status.setText("Logging in..."); + super.selected(); + } + + public void setScene(Scene scene) { + scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (this.isSelected() && event.getCode() == KeyCode.DELETE) { + follow(selectedThumbCells, false); + } + }); + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatFollowedUpdateService.java b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatFollowedUpdateService.java new file mode 100644 index 00000000..5a8410d0 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatFollowedUpdateService.java @@ -0,0 +1,119 @@ +package ctbrec.ui.sites.stripchat; + +import static ctbrec.io.HttpConstants.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.json.JSONArray; +import org.json.JSONObject; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HttpException; +import ctbrec.sites.stripchat.Stripchat; +import ctbrec.sites.stripchat.StripchatHttpClient; +import ctbrec.sites.stripchat.StripchatModel; +import ctbrec.ui.SiteUiFactory; +import ctbrec.ui.tabs.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class StripchatFollowedUpdateService extends PaginatedScheduledService { + private Stripchat stripchat; + private boolean showOnline = true; + + public StripchatFollowedUpdateService(Stripchat stripchat) { + this.stripchat = stripchat; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + JSONArray favoriteModelIds = loadFavoriteModelIds(); + List models = loadModels(favoriteModelIds); + return models; + } + + private List loadModels(JSONArray favoriteModelIds) throws IOException { + List models = new ArrayList<>(); + StripchatHttpClient client = (StripchatHttpClient) stripchat.getHttpClient(); + String url = stripchat.getBaseUrl() + "/api/front/users/list"; + JSONObject requestParams = new JSONObject(); + requestParams.put("userIds", favoriteModelIds); + requestParams.put("csrfToken", client.getCsrfToken()); + requestParams.put("csrfTimestamp", client.getCsrfTimestamp()); + requestParams.put("csrfNotifyTimestamp", client.getCsrfNotifyTimestamp()); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), requestParams.toString()); + Request request = new Request.Builder() + .url(url) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(ORIGIN, Stripchat.BASE_URI) + .header(REFERER, Stripchat.BASE_URI + "/favorites") + .header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON) + .post(body) + .build(); + try (Response response = stripchat.getHttpClient().execute(request)) { + if (response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if (json.has("users")) { + JSONArray users = json.getJSONArray("users"); + for (int i = 0; i < users.length(); i++) { + JSONObject user = users.getJSONObject(i); + StripchatModel model = stripchat.createModel(user.optString("username")); + model.setDescription(user.optString("description")); + model.setPreview(user.optString("previewUrlThumbBig")); + boolean online = Objects.equals(user.optString("status"), "public"); + if (showOnline == online) { + models.add(model); + } + } + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + return models; + } + + private JSONArray loadFavoriteModelIds() throws IOException { + SiteUiFactory.getUi(stripchat).login(); + long userId = ((StripchatHttpClient) stripchat.getHttpClient()).getUserId(); + String url = stripchat.getBaseUrl() + "/api/front/users/" + userId + "/favorites"; + Request request = new Request.Builder() + .url(url) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(ORIGIN, Stripchat.BASE_URI) + .header(REFERER, Stripchat.BASE_URI + "/favorites") + .header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON) + .build(); + try (Response response = stripchat.getHttpClient().execute(request)) { + if (response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.has("userIds")) { + JSONArray userIds = json.getJSONArray("userIds"); + return userIds; + } else { + return new JSONArray(); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + }; + } + + void showOnline(boolean online) { + this.showOnline = online; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatSiteUi.java b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatSiteUi.java new file mode 100644 index 00000000..6f5ca88a --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatSiteUi.java @@ -0,0 +1,37 @@ +package ctbrec.ui.sites.stripchat; + +import java.io.IOException; + +import ctbrec.sites.ConfigUI; +import ctbrec.sites.stripchat.Stripchat; +import ctbrec.ui.sites.AbstractSiteUi; +import ctbrec.ui.tabs.TabProvider; + +public class StripchatSiteUi extends AbstractSiteUi { + + private StripchatTabProvider tabProvider; + private StripchatConfigUI configUi; + private Stripchat stripchat; + + public StripchatSiteUi(Stripchat stripchat) { + this.stripchat = stripchat; + tabProvider = new StripchatTabProvider(stripchat); + configUi = new StripchatConfigUI(stripchat); + } + + @Override + public TabProvider getTabProvider() { + return tabProvider; + } + + @Override + public ConfigUI getConfigUI() { + return configUi; + } + + @Override + public synchronized boolean login() throws IOException { + boolean automaticLogin = stripchat.login(); + return automaticLogin; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatTabProvider.java b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatTabProvider.java new file mode 100644 index 00000000..5878bad9 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatTabProvider.java @@ -0,0 +1,55 @@ +package ctbrec.ui.sites.stripchat; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +import ctbrec.recorder.Recorder; +import ctbrec.sites.stripchat.Stripchat; +import ctbrec.ui.tabs.TabProvider; +import ctbrec.ui.tabs.ThumbOverviewTab; +import javafx.scene.Scene; +import javafx.scene.control.Tab; + +public class StripchatTabProvider extends TabProvider { + + private String urlTemplate; + private Stripchat stripchat; + private Recorder recorder; + + StripchatFollowedTab followedTab; + + + public StripchatTabProvider(Stripchat stripchat) { + this.stripchat = stripchat; + this.recorder = stripchat.getRecorder(); + followedTab = new StripchatFollowedTab("Followed", stripchat); + urlTemplate = stripchat.getBaseUrl() + "/api/front/models?primaryTag={0}&sortBy=viewersRating&withMixedTags=true&parentTag="; + } + + @Override + public List getTabs(Scene scene) { + List tabs = new ArrayList<>(); + tabs.add(createTab("Girls", MessageFormat.format(urlTemplate, "girls"))); + 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); + tabs.add(followedTab); + return tabs; + } + + @Override + public Tab getFollowedTab() { + return followedTab; + } + + private Tab createTab(String title, String url) { + StripchatUpdateService updateService = new StripchatUpdateService(url, false, stripchat); + ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, stripchat); + tab.setRecorder(recorder); + return tab; + } + +} diff --git a/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatUpdateService.java b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatUpdateService.java new file mode 100644 index 00000000..4a74b7a2 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/stripchat/StripchatUpdateService.java @@ -0,0 +1,97 @@ +package ctbrec.ui.sites.stripchat; + +import static ctbrec.io.HttpConstants.*; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.StringUtil; +import ctbrec.io.HttpException; +import ctbrec.sites.stripchat.Stripchat; +import ctbrec.sites.stripchat.StripchatModel; +import ctbrec.ui.SiteUiFactory; +import ctbrec.ui.tabs.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import okhttp3.Response; + +public class StripchatUpdateService extends PaginatedScheduledService { + + private static final Logger LOG = LoggerFactory.getLogger(StripchatUpdateService.class); + + private String url; + private boolean loginRequired; + private Stripchat stripchat; + int modelsPerPage = 60; + + public StripchatUpdateService(String url, boolean loginRequired, Stripchat stripchat) { + this.url = url; + this.loginRequired = loginRequired; + this.stripchat = stripchat; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + List models = new ArrayList<>(); + if(loginRequired && StringUtil.isBlank(ctbrec.Config.getInstance().getSettings().username)) { + return Collections.emptyList(); + } else { + int offset = (getPage() - 1) * modelsPerPage; + int limit = offset + modelsPerPage; + String paginatedUrl = url + "&offset=" + offset + "&limit=" + limit; + LOG.debug("Fetching page {}", paginatedUrl); + if(loginRequired) { + SiteUiFactory.getUi(stripchat).login(); + } + Request 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 (Response response = stripchat.getHttpClient().execute(request)) { + if (response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.has("models")) { + JSONArray jsonModels = json.getJSONArray("models"); + for (int i = 0; i < jsonModels.length(); i++) { + JSONObject jsonModel = jsonModels.getJSONObject(i); + try { + StripchatModel model = stripchat.createModel(jsonModel.getString("username")); + model.setDescription(""); + model.setPreview(jsonModel.optString("snapshotUrl")); + model.setDisplayName(model.getName()); + models.add(model); + } catch (Exception e) { + LOG.warn("Couldn't parse one of the models: {}", jsonModel, e); + } + } + return models; + } else { + LOG.debug("Response was not successful: {}", json); + return Collections.emptyList(); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + } + }; + } + +} diff --git a/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java b/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java index 0d088643..04530e63 100644 --- a/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java +++ b/client/src/main/java/ctbrec/ui/tabs/ThumbCell.java @@ -376,7 +376,7 @@ public class ThumbCell extends StackPane { throw new HttpException(resp.code(), resp.message()); } } catch (IOException e) { - LOG.error("Error loading image", e); + LOG.warn("Error loading thumbnail: {}", e.getLocalizedMessage()); } }); } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 20159a21..2d46c130 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -112,6 +112,8 @@ public class Settings { public String startTab = "Settings"; public String streamatePassword = ""; public String streamateUsername = ""; + public String stripchatUsername = ""; + public String stripchatPassword = ""; public boolean transportLayerSecurity = true; public int thumbWidth = 180; public boolean updateThumbnails = true; diff --git a/common/src/main/java/ctbrec/io/HttpConstants.java b/common/src/main/java/ctbrec/io/HttpConstants.java index 62ab4d0f..92826433 100644 --- a/common/src/main/java/ctbrec/io/HttpConstants.java +++ b/common/src/main/java/ctbrec/io/HttpConstants.java @@ -5,6 +5,7 @@ public class HttpConstants { public static final String ACCEPT = "Accept"; public static final String ACCEPT_LANGUAGE = "Accept-Language"; public static final String CONNECTION = "Connection"; + public static final String CONTENT_TYPE = "Content-Type"; public static final String KEEP_ALIVE = "keep-alive"; public static final String MIMETYPE_APPLICATION_JSON = "application/json"; public static final String ORIGIN = "Origin"; diff --git a/common/src/main/java/ctbrec/sites/stripchat/Stripchat.java b/common/src/main/java/ctbrec/sites/stripchat/Stripchat.java new file mode 100644 index 00000000..143837d9 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/stripchat/Stripchat.java @@ -0,0 +1,183 @@ +package ctbrec.sites.stripchat; + +import static ctbrec.io.HttpConstants.*; + +import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; +import ctbrec.sites.AbstractSite; +import okhttp3.Request; +import okhttp3.Response; + +public class Stripchat extends AbstractSite { + + private static final Logger LOG = LoggerFactory.getLogger(Stripchat.class); + public static final String BASE_URI = "https://stripchat.com"; + private HttpClient httpClient; + + @Override + public String getName() { + return "Stripchat"; + } + + @Override + public String getBaseUrl() { + return BASE_URI; + } + + @Override + public String getAffiliateLink() { + return "https://go.strpjmp.com?creativeId=1&campaignId=1&sourceId=app&path=%2F&userId=cd0bfa6152f1e2b55f8e8218de2f32c7e6c41d9846a390a2113ed4a5edfc95d8"; + } + + @Override + public String getBuyTokensLink() { + return getAffiliateLink(); + } + + @Override + public StripchatModel createModel(String name) { + StripchatModel model = new StripchatModel(); + model.setName(name); + model.setUrl(getBaseUrl() + "/" + name); + model.setSite(this); + return model; + } + + @Override + public Double getTokenBalance() throws IOException { + if (!credentialsAvailable()) { + throw new IOException("Account settings not available"); + } + + String username = Config.getInstance().getSettings().camsodaUsername; + String url = BASE_URI + "/api/v1/user/" + username; + Request request = new Request.Builder().url(url).build(); + try(Response response = getHttpClient().execute(request)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.has("user")) { + JSONObject user = json.getJSONObject("user"); + if(user.has("tokens")) { + return (double) user.getInt("tokens"); + } + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + throw new RuntimeException("Tokens not found in response"); + } + + @Override + public synchronized boolean login() throws IOException { + return credentialsAvailable() && getHttpClient().login(); + } + + @Override + public HttpClient getHttpClient() { + if(httpClient == null) { + httpClient = new StripchatHttpClient(); + } + return httpClient; + } + + @Override + public void init() throws IOException { + // noop + } + + @Override + public void shutdown() { + if(httpClient != null) { + httpClient.shutdown(); + } + } + + @Override + public boolean supportsTips() { + return false; + } + + @Override + public boolean supportsFollow() { + return true; + } + + @Override + public boolean supportsSearch() { + return false; + } + + @Override + public List search(String q) throws IOException, InterruptedException { + String url = BASE_URI + "/api/v1/browse/autocomplete?s=" + URLEncoder.encode(q, "utf-8"); + Request req = new Request.Builder() + .url(url) + .addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .build(); + try(Response response = getHttpClient().execute(req)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.optBoolean("status")) { + List models = new ArrayList<>(); + JSONArray results = json.getJSONArray("results"); + for (int i = 0; i < results.length(); i++) { + JSONObject result = results.getJSONObject(i); + StripchatModel model = createModel(result.getString("username")); + String thumb = result.getString("thumb"); + if(thumb != null) { + model.setPreview("https:" + thumb); + } + if(result.has("display_name")) { + model.setDisplayName(result.getString("display_name")); + } + models.add(model); + } + return models; + } else { + LOG.warn("Search result: {}", json.toString(2)); + return Collections.emptyList(); + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + @Override + public boolean isSiteForModel(Model m) { + return m instanceof StripchatModel; + } + + @Override + public boolean credentialsAvailable() { + String username = Config.getInstance().getSettings().stripchatUsername; + return username != null && !username.trim().isEmpty(); + } + + @Override + public Model createModelFromUrl(String url) { + Matcher m = Pattern.compile("https?://(?:.*?\\.)?stripchat.com/([^/]*?)/?").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/stripchat/StripchatHttpClient.java b/common/src/main/java/ctbrec/sites/stripchat/StripchatHttpClient.java new file mode 100644 index 00000000..e47fb296 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/stripchat/StripchatHttpClient.java @@ -0,0 +1,129 @@ +package ctbrec.sites.stripchat; + +import static ctbrec.io.HttpConstants.*; + +import java.io.IOException; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class StripchatHttpClient extends HttpClient { + + private static final Logger LOG = LoggerFactory.getLogger(StripchatHttpClient.class); + + private long userId; + private String csrfToken; + private String csrfTimestamp; + private String csrfNotifyTimestamp; + + public StripchatHttpClient() { + super("stripchat"); + } + + @Override + public boolean login() throws IOException { + if(loggedIn) { + return true; + } + + // persisted cookies might let us log in + if(checkLoginSuccess()) { + loggedIn = true; + LOG.debug("Logged in with cookies"); + return true; + } + + if (csrfToken == null) { + loadCsrfToken(); + } + + String url = Stripchat.BASE_URI + "/api/front/auth/login"; + JSONObject requestParams = new JSONObject(); + requestParams.put("loginOrEmail", Config.getInstance().getSettings().stripchatUsername); + requestParams.put("password", Config.getInstance().getSettings().stripchatPassword); + requestParams.put("remember", true); + requestParams.put("csrfToken", csrfToken); + requestParams.put("csrfTimestamp", csrfTimestamp); + requestParams.put("csrfNotifyTimestamp", csrfNotifyTimestamp); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), requestParams.toString()); + Request request = new Request.Builder() + .url(url) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(ORIGIN, Stripchat.BASE_URI) + .header(REFERER, Stripchat.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 { + throw new HttpException(response.code(), response.message()); + } + } + } + + private void loadCsrfToken() throws IOException { + String url = Stripchat.BASE_URI + "/api/front/config"; + Request request = new Request.Builder() + .url(url) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(ORIGIN, Stripchat.BASE_URI) + .header(REFERER, Stripchat.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() { + return userId > 0; + } + + public long getUserId() { + 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/stripchat/StripchatModel.java b/common/src/main/java/ctbrec/sites/stripchat/StripchatModel.java new file mode 100644 index 00000000..123bc9c8 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/stripchat/StripchatModel.java @@ -0,0 +1,205 @@ +package ctbrec.sites.stripchat; + +import static ctbrec.io.HttpConstants.*; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import javax.xml.bind.JAXBException; + +import org.json.JSONArray; +import org.json.JSONObject; + +import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.PlaylistException; + +import ctbrec.AbstractModel; +import ctbrec.Config; +import ctbrec.io.HttpException; +import ctbrec.recorder.download.StreamSource; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class StripchatModel 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) { + JSONObject jsonResponse = loadModelInfo(); + if (jsonResponse.has("user")) { + JSONObject user = jsonResponse.getJSONObject("user"); + status = user.optString("status"); + } + } + return Objects.equals(status, "public"); + } + + private JSONObject loadModelInfo() throws IOException { + String url = getSite().getBaseUrl() + "/api/front/users/username/" + URLEncoder.encode(getName(), StandardCharsets.UTF_8.name()); + 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()) { + JSONObject jsonResponse = new JSONObject(response.body().string()); + return jsonResponse; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + @Override + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException { + String name = URLEncoder.encode(getName(), StandardCharsets.UTF_8.name()); + 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()) { + JSONObject jsonResponse = new JSONObject(response.body().string()); + String streamName = jsonResponse.optString("streamName"); + JSONObject viewServers = jsonResponse.getJSONObject("viewServers"); + String serverName = viewServers.optString("flashphoner-hls"); + JSONObject broadcastSettings = jsonResponse.getJSONObject("broadcastSettings"); + List sources = new ArrayList<>(); + StreamSource best = new StreamSource(); + best.height = broadcastSettings.optInt("height"); + best.width = broadcastSettings.optInt("width"); + best.mediaPlaylistUrl = "https://b-" + serverName + ".stripst.com/hls/" + streamName + "/" + streamName + ".m3u8"; + sources.add(best); + Object resolutionObject = broadcastSettings.get("resolutions"); + if (resolutionObject instanceof JSONObject) { + JSONObject resolutions = (JSONObject) resolutionObject; + JSONArray heights = resolutions.names(); + for (int i = 0; i < heights.length(); i++) { + String h = heights.getString(i); + StreamSource streamSource = new StreamSource(); + streamSource.height = Integer.parseInt(h.replace("p", "")); + streamSource.width = streamSource.height * best.getWidth() / best.getHeight(); + String source = streamName + "-" + streamSource.height + "p"; + streamSource.mediaPlaylistUrl = "https://b-" + serverName + ".stripst.com/hls/" + source + "/" + source + ".m3u8"; + sources.add(streamSource); + } + } + return sources.stream().sorted((a, b) -> a.height - b.height).collect(Collectors.toList()); + } 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 { + JSONObject modelInfo = loadModelInfo(); + JSONObject user = modelInfo.getJSONObject("user"); + long modelId = user.optLong("id"); + + StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient(); + String url = Stripchat.BASE_URI + "/api/front/users/" + client.getUserId() + "/favorites/" + modelId; + JSONObject requestParams = new JSONObject(); + requestParams.put("csrfToken", client.getCsrfToken()); + requestParams.put("csrfTimestamp", client.getCsrfTimestamp()); + requestParams.put("csrfNotifyTimestamp", client.getCsrfNotifyTimestamp()); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), requestParams.toString()); + Request request = new Request.Builder() + .url(url) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(ORIGIN, Stripchat.BASE_URI) + .header(REFERER, Stripchat.BASE_URI) + .header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON) + .put(body) + .build(); + try (Response response = client.execute(request)) { + if (response.isSuccessful()) { + return true; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + @Override + public boolean unfollow() throws IOException { + JSONObject modelInfo = loadModelInfo(); + JSONObject user = modelInfo.getJSONObject("user"); + long modelId = user.optLong("id"); + JSONArray favoriteIds = new JSONArray(); + favoriteIds.put(modelId); + + StripchatHttpClient client = (StripchatHttpClient) getSite().getHttpClient(); + String url = Stripchat.BASE_URI + "/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()); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), requestParams.toString()); + Request request = new Request.Builder() + .url(url) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(ORIGIN, Stripchat.BASE_URI) + .header(REFERER, Stripchat.BASE_URI) + .header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON) + .delete(body) + .build(); + try (Response response = client.execute(request)) { + if (response.isSuccessful()) { + return true; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } +}