From 5a86cfa85e0119732d1284fd9cb0bd0c05c8d130 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sat, 23 Oct 2021 17:19:44 +0200 Subject: [PATCH 01/10] Add initial implementation for cherry.tv --- .../java/ctbrec/ui/CamrecApplication.java | 4 +- .../main/java/ctbrec/ui/SiteUiFactory.java | 10 +- .../ui/sites/cherrytv/CherryTvConfigUI.java | 89 +++++++ .../ui/sites/cherrytv/CherryTvSiteUi.java | 46 ++++ .../sites/cherrytv/CherryTvTabProvider.java | 50 ++++ .../sites/cherrytv/CherryTvUpdateService.java | 115 +++++++++ common/pom.xml | 5 + common/src/main/java/ctbrec/Config.java | 8 +- common/src/main/java/ctbrec/Settings.java | 2 +- .../java/ctbrec/sites/cherrytv/CherryTv.java | 162 +++++++++++++ .../sites/cherrytv/CherryTvHttpClient.java | 18 ++ .../ctbrec/sites/cherrytv/CherryTvModel.java | 225 ++++++++++++++++++ .../ctbrec/recorder/server/HttpServer.java | 213 ++++++++--------- .../src/main/resources/html/static/index.html | 19 +- 14 files changed, 835 insertions(+), 131 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvConfigUI.java create mode 100644 client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvSiteUi.java create mode 100644 client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java create mode 100644 client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java create mode 100644 common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java create mode 100644 common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java create mode 100644 common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index ca40f8d6..84d3d1e9 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.cherrytv.CherryTv; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -174,6 +175,7 @@ public class CamrecApplication extends Application { sites.add(new Cam4()); sites.add(new Camsoda()); sites.add(new Chaturbate()); + sites.add(new CherryTv()); sites.add(new Fc2Live()); sites.add(new Flirt4Free()); sites.add(new LiveJasmin()); @@ -193,7 +195,7 @@ public class CamrecApplication extends Application { } private void initSites() { - sites.stream().forEach(site -> { + sites.forEach(site -> { try { site.setRecorder(recorder); site.setConfig(config); diff --git a/client/src/main/java/ctbrec/ui/SiteUiFactory.java b/client/src/main/java/ctbrec/ui/SiteUiFactory.java index b9056890..a9a64d62 100644 --- a/client/src/main/java/ctbrec/ui/SiteUiFactory.java +++ b/client/src/main/java/ctbrec/ui/SiteUiFactory.java @@ -6,6 +6,7 @@ import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; +import ctbrec.sites.cherrytv.CherryTv; import ctbrec.sites.fc2live.Fc2Live; import ctbrec.sites.flirt4free.Flirt4Free; import ctbrec.sites.jasmin.LiveJasmin; @@ -20,6 +21,7 @@ import ctbrec.ui.sites.bonga.BongaCamsSiteUi; import ctbrec.ui.sites.cam4.Cam4SiteUi; import ctbrec.ui.sites.camsoda.CamsodaSiteUi; import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi; +import ctbrec.ui.sites.cherrytv.CherryTvSiteUi; import ctbrec.ui.sites.fc2live.Fc2LiveSiteUi; import ctbrec.ui.sites.flirt4free.Flirt4FreeSiteUi; import ctbrec.ui.sites.jasmin.LiveJasminSiteUi; @@ -37,6 +39,7 @@ public class SiteUiFactory { private static Cam4SiteUi cam4SiteUi; private static CamsodaSiteUi camsodaSiteUi; private static ChaturbateSiteUi ctbSiteUi; + private static CherryTvSiteUi cherryTvSiteUi; private static Fc2LiveSiteUi fc2SiteUi; private static Flirt4FreeSiteUi flirt4FreeSiteUi; private static LiveJasminSiteUi jasminSiteUi; @@ -49,7 +52,7 @@ public class SiteUiFactory { private SiteUiFactory () {} - public static synchronized SiteUI getUi(Site site) { + public static synchronized SiteUI getUi(Site site) { // NOSONAR if (site instanceof AmateurTv) { if (amateurTvUi == null) { amateurTvUi = new AmateurTvSiteUi((AmateurTv) site); @@ -75,6 +78,11 @@ public class SiteUiFactory { ctbSiteUi = new ChaturbateSiteUi((Chaturbate) site); } return ctbSiteUi; + } else if (site instanceof CherryTv) { + if (cherryTvSiteUi == null) { + cherryTvSiteUi = new CherryTvSiteUi((CherryTv) site); + } + return cherryTvSiteUi; } else if (site instanceof Fc2Live) { if (fc2SiteUi == null) { fc2SiteUi = new Fc2LiveSiteUi((Fc2Live) site); diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvConfigUI.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvConfigUI.java new file mode 100644 index 00000000..9ddfe293 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvConfigUI.java @@ -0,0 +1,89 @@ +package ctbrec.ui.sites.cherrytv; + +import ctbrec.Config; +import ctbrec.sites.cam4.Cam4; +import ctbrec.sites.cherrytv.CherryTv; +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.*; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; + +public class CherryTvConfigUI extends AbstractConfigUI { + private final CherryTv site; + + public CherryTvConfigUI(CherryTv cherryTv) { + this.site = cherryTv; + } + + @Override + public Parent createConfigPanel() { + var 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("Cam4 User"), 0, row); + var username = new TextField(Config.getInstance().getSettings().cam4Username); + username.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().cam4Username)) { + Config.getInstance().getSettings().cam4Username = 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("Cam4 Password"), 0, row); + var password = new PasswordField(); + password.setText(Config.getInstance().getSettings().cam4Password); + password.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().cam4Password)) { + Config.getInstance().getSettings().cam4Password = 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(Cam4.AFFILIATE_LINK)); + 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/cherrytv/CherryTvSiteUi.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvSiteUi.java new file mode 100644 index 00000000..58844da8 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvSiteUi.java @@ -0,0 +1,46 @@ +package ctbrec.ui.sites.cherrytv; + +import ctbrec.sites.cam4.Cam4; +import ctbrec.sites.cam4.Cam4HttpClient; +import ctbrec.sites.cherrytv.CherryTv; +import ctbrec.ui.controls.Dialogs; +import ctbrec.ui.sites.AbstractSiteUi; +import ctbrec.ui.sites.ConfigUI; +import ctbrec.ui.tabs.TabProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public class CherryTvSiteUi extends AbstractSiteUi { + private static final Logger LOG = LoggerFactory.getLogger(CherryTvSiteUi.class); + + private final CherryTv cherryTv; + private CherryTvTabProvider tabProvider; + private CherryTvConfigUI configUi; + + public CherryTvSiteUi(CherryTv cherryTv) { + this.cherryTv = cherryTv; + } + + @Override + public TabProvider getTabProvider() { + if (tabProvider == null) { + tabProvider = new CherryTvTabProvider(cherryTv); + } + return tabProvider; + } + + @Override + public ConfigUI getConfigUI() { + if (configUi == null) { + configUi = new CherryTvConfigUI(cherryTv); + } + return configUi; + } + + @Override + public synchronized boolean login() throws IOException { + return cherryTv.login(); + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java new file mode 100644 index 00000000..3bc63f66 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java @@ -0,0 +1,50 @@ +package ctbrec.ui.sites.cherrytv; + +import ctbrec.recorder.Recorder; +import ctbrec.sites.cherrytv.CherryTv; +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 CherryTvTabProvider implements TabProvider { + + private final CherryTv site; + private final Recorder recorder; + + public CherryTvTabProvider(CherryTv cherryTv) { + this.site = cherryTv; + this.recorder = cherryTv.getRecorder(); + } + + @Override + public List getTabs(Scene scene) { + List tabs = new ArrayList<>(); + + tabs.add(createTab("Female", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"female\",\"tag\":null,\"following\":null,\"limit\":50}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); +// tabs.add(createTab("Male", site.getBaseUrl() + "/directoryResults?online=true&gender=male&orderBy=MOST_VIEWERS")); +// tabs.add(createTab("Couples", site.getBaseUrl() + "/directoryResults?online=true&broadcastType=male_group&broadcastType=female_group&broadcastType=male_female_group&orderBy=MOST_VIEWERS")); +// tabs.add(createTab("HD", site.getBaseUrl() + "/directoryResults?online=true&hd=true&orderBy=MOST_VIEWERS")); +// tabs.add(createTab("New", site.getBaseUrl() + "/directoryResults?online=true&gender=female&orderBy=MOST_VIEWERS&newPerformer=true")); + + return tabs; + } + + @Override + public Tab getFollowedTab() { + return null; + } + + private Tab createTab(String name, String url) { + var updateService = new CherryTvUpdateService(url, site); + var tab = new ThumbOverviewTab(name, updateService, site); + tab.setImageAspectRatio(9.0 / 16.0); + tab.preserveAspectRatioProperty().set(false); + tab.setRecorder(recorder); + return tab; + } + +} diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java new file mode 100644 index 00000000..491337cf --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java @@ -0,0 +1,115 @@ +package ctbrec.ui.sites.cherrytv; + +import com.apollographql.apollo.ApolloClient; +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HtmlParser; +import ctbrec.io.HttpException; +import ctbrec.sites.cherrytv.CherryTv; +import ctbrec.sites.cherrytv.CherryTvModel; +import ctbrec.ui.tabs.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.jsoup.nodes.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static ctbrec.Model.State.*; +import static ctbrec.io.HttpConstants.ACCEPT_LANGUAGE; +import static ctbrec.io.HttpConstants.USER_AGENT; + +public class CherryTvUpdateService extends PaginatedScheduledService { + + private static final Logger LOG = LoggerFactory.getLogger(CherryTvUpdateService.class); + private String url; + private final CherryTv site; + private ApolloClient apolloClient; + + public CherryTvUpdateService(String url, CherryTv site) { + this.site = site; + this.url = url; + + ExecutorService executor = Executors.newSingleThreadExecutor(r -> { + var t = new Thread(r); + t.setDaemon(true); + t.setName("CherryTvUpdateService"); + return t; + }); + setExecutor(executor); + + apolloClient = ApolloClient.builder() + .serverUrl(site.getBaseUrl() + "/graphql") + .build(); + } + + @Override + protected Task> createTask() { + return new Task<>() { + @Override + public List call() throws IOException { + String pageUrl = CherryTvUpdateService.this.url; + LOG.debug("Fetching page {}", pageUrl); + apolloClient. + + var request = new Request.Builder() + .url(pageUrl) + .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .build(); + try (var response = site.getHttpClient().execute(request)) { + if (response.isSuccessful()) { + return parseModels(Objects.requireNonNull(response.body()).string()); + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + }; + } + + private List parseModels(String body) { + var json = new JSONObject(body); + // LOG.debug(json.toString(2)); + List models = new ArrayList<>(); + try { + JSONArray broadcasts = json.getJSONObject("data").getJSONObject("broadcasts").getJSONArray("broadcasts"); + for (int i = 0; i < broadcasts.length(); i++) { + JSONObject broadcast = broadcasts.getJSONObject(i); + CherryTvModel model = site.createModel(broadcast.optString("username")); + model.setDisplayName(broadcast.optString("title")); + model.setDescription(broadcast.optString("description")); + model.setPreview(broadcast.optString("thumbnailUrl")); + var online = broadcast.optString("showStatus").equalsIgnoreCase("Public") + && broadcast.optString("broadcastStatus").equalsIgnoreCase("Live"); + model.setOnline(online); + model.setOnlineState(online ? ONLINE : OFFLINE); + JSONArray tags = broadcast.optJSONArray("tags"); + if (tags != null) { + for (int j = 0; j < tags.length(); j++) { + model.getTags().add(tags.getString(j)); + } + } + models.add(model); + } + } catch (JSONException e) { + LOG.error("Couldn't parse JSON, the structure might have changed", e); + } + return models; + } + + public void setUrl(String url) { + this.url = url; + } + +} diff --git a/common/pom.xml b/common/pom.xml index 24285590..877d8644 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -50,6 +50,11 @@ commons-io commons-io + + com.apollographql.apollo + apollo-runtime + 2.5.9 + javax.servlet javax.servlet-api diff --git a/common/src/main/java/ctbrec/Config.java b/common/src/main/java/ctbrec/Config.java index 73deda30..7470de4f 100644 --- a/common/src/main/java/ctbrec/Config.java +++ b/common/src/main/java/ctbrec/Config.java @@ -49,9 +49,9 @@ public class Config { private static Config instance; private Settings settings; - private String filename; - private List sites; - private File configDir; + private final String filename; + private final List sites; + private final File configDir; /** * to temporarily disable saving of the config * this is useful for the SettingsTab, because setting the initial values of some components causes an immediate save @@ -96,7 +96,7 @@ public class Config { fileContent[2] = ' '; } String json = new String(fileContent, UTF_8).trim(); - settings = adapter.fromJson(json); + settings = Objects.requireNonNull(adapter.fromJson(json)); settings.httpTimeout = Math.max(settings.httpTimeout, 10_000); if (settings.recordingsDir.endsWith("/")) { settings.recordingsDir = settings.recordingsDir.substring(0, settings.recordingsDir.length() - 1); diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 6d22966c..9d46397d 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -19,7 +19,7 @@ public class Settings { ONE_PER_MODEL("one directory for each model"), ONE_PER_RECORDING("one directory for each recording"); - private String description; + private final String description; DirectoryStructure(String description) { this.description = description; } diff --git a/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java b/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java new file mode 100644 index 00000000..b8174de5 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java @@ -0,0 +1,162 @@ +package ctbrec.sites.cherrytv; + +import ctbrec.Model; +import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; +import ctbrec.sites.AbstractSite; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.Response; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static ctbrec.io.HttpConstants.*; + +public class CherryTv extends AbstractSite { + + private static final Logger LOG = LoggerFactory.getLogger(CherryTv.class); + + public static final String BASE_URL = "https://cherry.tv"; + + private CherryTvHttpClient httpClient; + + @Override + public String getName() { + return "CherryTV"; + } + + @Override + public String getBaseUrl() { + return BASE_URL; + } + + @Override + public String getAffiliateLink() { + return getBaseUrl(); + } + + @Override + public CherryTvModel createModel(String name) { + CherryTvModel model = new CherryTvModel(); + model.setName(name); + model.setUrl(getBaseUrl() + '/' + name); + model.setDescription(""); + model.setSite(this); + return model; + } + + @Override + public Double getTokenBalance() throws IOException { + return 0d; + } + + @Override + public String getBuyTokensLink() { + return getAffiliateLink(); + } + + @Override + public synchronized boolean login() throws IOException { + return false; + } + + @Override + public HttpClient getHttpClient() { + if (httpClient == null) { + httpClient = new CherryTvHttpClient(getConfig()); + } + return httpClient; + } + + @Override + public void init() throws IOException { + // nothing to do + } + + @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 boolean searchRequiresLogin() { + return false; + } + + @Override + public List search(String q) throws IOException, InterruptedException { + JSONObject variables = new JSONObject().put("slug", q).put("limit", 10); + JSONObject persistedQuery = new JSONObject().put("persistedQuery", new JSONObject().put("version", 1).put("sha256Hash", "")); + Request req = new Request.Builder() + .url(new HttpUrl.Builder() + .scheme("https") + .host("cherry.tv") + .addPathSegment("qraphql") + .addQueryParameter("operationName", "Search") + .addQueryParameter("variables", variables.toString()) + .addQueryParameter("extensions", persistedQuery.toString()) + .build() + ) + .header(USER_AGENT, getConfig().getSettings().httpUserAgent) + .header(ACCEPT, MIMETYPE_APPLICATION_JSON) + .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) + .header(REFERER, getBaseUrl()) + .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) + .build(); + try (Response response = getHttpClient().execute(req)) { + if (response.isSuccessful()) { + LOG.debug("Response: {}", Objects.requireNonNull(response.body()).string()); + } else { + throw new HttpException(response.code(), response.message()); + } + } + return Collections.emptyList(); + } + + @Override + public boolean isSiteForModel(Model m) { + return m instanceof CherryTvModel; + } + + @Override + public boolean credentialsAvailable() { + return false; + } + + @Override + public Model createModelFromUrl(String url) { + Matcher m = Pattern.compile("https?://.*?cherry\\.tv/([^/]*?)/?").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/cherrytv/CherryTvHttpClient.java b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java new file mode 100644 index 00000000..ff402c32 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java @@ -0,0 +1,18 @@ +package ctbrec.sites.cherrytv; + +import ctbrec.Config; +import ctbrec.io.HttpClient; + +import java.io.IOException; + +public class CherryTvHttpClient extends HttpClient { + + public CherryTvHttpClient(Config config) { + super("cherrytv", config); + } + + @Override + public synchronized boolean login() throws IOException { + return false; + } +} diff --git a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java new file mode 100644 index 00000000..7fcfe3c4 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java @@ -0,0 +1,225 @@ +package ctbrec.sites.cherrytv; + +import com.iheartradio.m3u8.*; +import com.iheartradio.m3u8.data.MasterPlaylist; +import com.iheartradio.m3u8.data.Playlist; +import com.iheartradio.m3u8.data.PlaylistData; +import ctbrec.AbstractModel; +import ctbrec.Config; +import ctbrec.NotImplementedExcetion; +import ctbrec.io.HttpException; +import ctbrec.recorder.download.StreamSource; +import okhttp3.Request; +import okhttp3.Response; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static ctbrec.Model.State.*; +import static ctbrec.io.HttpConstants.*; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class CherryTvModel extends AbstractModel { + + private static final Logger LOG = LoggerFactory.getLogger(CherryTvModel.class); + private static final Pattern NEXT_DATA = Pattern.compile(""); + + private boolean online = false; + private int[] resolution; + private String masterPlaylistUrl; + + @Override + public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { + if (ignoreCache) { + String url = getUrl(); + Request req = new Request.Builder().url(url) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(ACCEPT, "*") + .header(ACCEPT_LANGUAGE, "en") + .header(REFERER, getSite().getBaseUrl()) + .build(); + try (Response resp = site.getHttpClient().execute(req)) { + String body = Objects.requireNonNull(resp.body()).string(); + Files.write(Paths.get("/tmp/mdl.html"), body.getBytes(StandardCharsets.UTF_8)); + Matcher m = NEXT_DATA.matcher(body); + if (m.find()) { + JSONObject json = new JSONObject(m.group(1)); + JSONObject apolloState = json.getJSONObject("props").getJSONObject("pageProps").getJSONObject("apolloState"); + online = false; + onlineState = OFFLINE; + for (Iterator iter = apolloState.keys(); iter.hasNext();) { + String key = iter.next(); + if (key.startsWith("Broadcast:")) { + JSONObject broadcast = apolloState.getJSONObject(key); + setDisplayName(broadcast.optString("title")); + // id = broadcast.getString("id"); + // roomId = broadcast.getString("roomId"); + online = broadcast.optString("showStatus").equalsIgnoreCase("Public") + && broadcast.optString("broadcastStatus").equalsIgnoreCase("Live"); + onlineState = online ? ONLINE : OFFLINE; + masterPlaylistUrl = broadcast.optString("pullUrl", null); + break; + } + } + } else { + LOG.error("NEXT_DATA not found in model page {}", getUrl()); + return false; + } + } catch (JSONException e) { + LOG.error("Unable to determine online state for {}. Probably the JSON structure in NEXT_DATA changed", getName()); + } + } + return online; + } + + public void setOnline(boolean online) { + this.online = online; + } + + @Override + public State getOnlineState(boolean failFast) throws IOException, ExecutionException { + if (!failFast) { + try { + isOnline(true); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + onlineState = OFFLINE; + } catch (IOException | ExecutionException e) { + onlineState = OFFLINE; + } + } + return onlineState; + } + + @Override + public void setOnlineState(State onlineState) { + this.onlineState = onlineState; + } + + @Override + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + try { + isOnline(true); + MasterPlaylist masterPlaylist = getMasterPlaylist(); + List sources = new ArrayList<>(); + for (PlaylistData playlist : masterPlaylist.getPlaylists()) { + if (playlist.hasStreamInfo()) { + StreamSource src = new StreamSource(); + src.bandwidth = playlist.getStreamInfo().getBandwidth(); + src.height = playlist.getStreamInfo().getResolution().height; + String masterUrl = masterPlaylistUrl; + String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); + String segmentUri = baseUrl + playlist.getUri(); + src.mediaPlaylistUrl = segmentUri; + if(src.mediaPlaylistUrl.contains("?")) { + src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?')); + } + LOG.trace("Media playlist {}", src.mediaPlaylistUrl); + sources.add(src); + } + } + return sources; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new ExecutionException(e); + } + } + + private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException { + LOG.trace("Loading master playlist {}", masterPlaylistUrl); + Request req = new Request.Builder() + .url(masterPlaylistUrl) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .build(); + try (Response response = getSite().getHttpClient().execute(req)) { + if (response.isSuccessful()) { + String body = Objects.requireNonNull(response.body()).string(); + LOG.trace(body); + InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8)); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); + Playlist playlist = parser.parse(); + MasterPlaylist master = playlist.getMasterPlaylist(); + return master; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + @Override + public void invalidateCacheEntries() { + resolution = null; + } + + @Override + public void receiveTip(Double tokens) throws IOException { + throw new NotImplementedExcetion(); + } + + @Override + public int[] getStreamResolution(boolean failFast) throws ExecutionException { + if(resolution == null) { + if(failFast) { + return new int[2]; + } + try { + if(!isOnline()) { + return new int[2]; + } + List sources = getStreamSources(); + Collections.sort(sources); + StreamSource best = sources.get(sources.size()-1); + resolution = new int[] {best.width, best.height}; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); + resolution = new int[2]; + } catch (ExecutionException | IOException | ParseException | PlaylistException e) { + LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); + resolution = new int[2]; + } + return resolution; + } else { + return resolution; + } + } + + @Override + public boolean follow() throws IOException { + return false; + } + + @Override + public boolean unfollow() throws IOException { + return false; + } + + public void mapOnlineState(String roomState) { + switch (roomState) { + case "private": + case "fullprivate": + setOnlineState(PRIVATE); + break; + case "group": + case "public": + setOnlineState(ONLINE); + setOnline(true); + break; + default: + LOG.debug(roomState); + setOnlineState(OFFLINE); + } + } +} diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index 1f9d650f..bd46a495 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -1,58 +1,5 @@ package ctbrec.recorder.server; -import static java.nio.charset.StandardCharsets.*; -import static javax.servlet.http.HttpServletResponse.*; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Writer; -import java.net.BindException; -import java.net.URL; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; -import java.util.Optional; - -import javax.servlet.DispatcherType; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.eclipse.jetty.security.ConstraintMapping; -import org.eclipse.jetty.security.ConstraintSecurityHandler; -import org.eclipse.jetty.security.HashLoginService; -import org.eclipse.jetty.security.SecurityHandler; -import org.eclipse.jetty.security.UserStore; -import org.eclipse.jetty.security.authentication.BasicAuthenticator; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.HttpConfiguration; -import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.server.handler.ErrorHandler; -import org.eclipse.jetty.server.handler.HandlerList; -import org.eclipse.jetty.server.handler.SecuredRedirectHandler; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.util.security.Constraint; -import org.eclipse.jetty.util.security.Credential; -import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.json.JSONObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.common.base.Objects; - import ctbrec.Config; import ctbrec.GlobalThreadPool; import ctbrec.NotLoggedInExcetion; @@ -70,6 +17,7 @@ import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; +import ctbrec.sites.cherrytv.CherryTv; import ctbrec.sites.fc2live.Fc2Live; import ctbrec.sites.flirt4free.Flirt4Free; import ctbrec.sites.jasmin.LiveJasmin; @@ -79,17 +27,45 @@ import ctbrec.sites.showup.Showup; import ctbrec.sites.streamate.Streamate; import ctbrec.sites.stripchat.Stripchat; import ctbrec.sites.xlovecam.XloveCam; +import org.eclipse.jetty.security.*; +import org.eclipse.jetty.security.authentication.BasicAuthenticator; +import org.eclipse.jetty.server.*; +import org.eclipse.jetty.server.handler.ErrorHandler; +import org.eclipse.jetty.server.handler.HandlerList; +import org.eclipse.jetty.server.handler.SecuredRedirectHandler; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.security.Constraint; +import org.eclipse.jetty.util.security.Credential; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.*; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; +import java.net.BindException; +import java.net.URL; +import java.util.*; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static javax.servlet.http.HttpServletResponse.SC_MOVED_PERMANENTLY; +import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; public class HttpServer { private static final Logger LOG = LoggerFactory.getLogger(HttpServer.class); - private Recorder recorder; - private OnlineMonitor onlineMonitor; - private Config config; + private final Recorder recorder; + private final OnlineMonitor onlineMonitor; + private final Config config; + private final List sites = new ArrayList<>(); private Server server = new Server(); - private List sites = new ArrayList<>(); - public HttpServer() throws Exception { + public HttpServer() throws IOException { logEnvironment(); createSites(); System.setProperty("ctbrec.server.mode", "1"); @@ -156,6 +132,7 @@ public class HttpServer { sites.add(new Cam4()); sites.add(new Camsoda()); sites.add(new Chaturbate()); + sites.add(new CherryTv()); sites.add(new Fc2Live()); sites.add(new Flirt4Free()); sites.add(new LiveJasmin()); @@ -168,32 +145,29 @@ public class HttpServer { } private void addShutdownHook() { - Runtime.getRuntime().addShutdownHook(new Thread() { - @Override - public void run() { - LOG.info("Shutting down"); - if (onlineMonitor != null) { - onlineMonitor.shutdown(); - } - if (recorder != null) { - recorder.shutdown(false); - } - try { - server.stop(); - } catch (Exception e) { - LOG.error("Couldn't stop HTTP server", e); - } - try { - Config.getInstance().save(); - } catch (IOException e) { - LOG.error("Couldn't save configuration", e); - } - LOG.info("Goodbye!"); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + LOG.info("Shutting down"); + if (onlineMonitor != null) { + onlineMonitor.shutdown(); } - }); + if (recorder != null) { + recorder.shutdown(false); + } + try { + server.stop(); + } catch (Exception e) { + LOG.error("Couldn't stop HTTP server", e); + } + try { + Config.getInstance().save(); + } catch (IOException e) { + LOG.error("Couldn't save configuration", e); + } + LOG.info("Goodbye!"); + })); } - private void startHttpServer() throws Exception { + private void startHttpServer() { server = new Server(); HttpConfiguration httpConfig = new HttpConfiguration(); @@ -204,7 +178,7 @@ public class HttpServer { SslContextFactory sslContextFactory = new SslContextFactory.Server(); URL keyStoreUrl = getClass().getResource("/keystore.pkcs12"); - String keyStoreSrc = System.getProperty("keystore.file", keyStoreUrl.toExternalForm()); + String keyStoreSrc = System.getProperty("keystore.file", Objects.requireNonNull(keyStoreUrl).toExternalForm()); String keyStorePassword = System.getProperty("keystore.password", "ctbrecsucks"); sslContextFactory.setKeyStorePath(keyStoreSrc); sslContextFactory.setKeyStorePassword(keyStorePassword); @@ -244,35 +218,7 @@ public class HttpServer { defaultContext.addServlet(holder, "/hls/*"); if (this.config.getSettings().webinterface) { - StaticFileServlet staticFileServlet = new StaticFileServlet("/html"); - holder = new ServletHolder(staticFileServlet); - String staticFileContext = "/static/*"; - defaultContext.addServlet(holder, staticFileContext); - LOG.info("Register static file servlet under {}", defaultContext.getContextPath()+staticFileContext); - - // servlet to retrieve the HMAC (secured by basic auth if an hmac key is set in the config) - String username = this.config.getSettings().webinterfaceUsername; - String password = this.config.getSettings().webinterfacePassword; - if (config.getSettings().key != null && config.getSettings().key.length > 0) { - basicAuthContext.setSecurityHandler(basicAuth(username, password, "CTB Recorder")); - } - basicAuthContext.addServlet(new ServletHolder(new HttpServlet() { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException { - if (Objects.equal(username, req.getRemoteUser())) { - resp.setStatus(HttpServletResponse.SC_OK); - resp.setContentType("application/json"); - byte[] hmac = Optional.ofNullable(HttpServer.this.config.getSettings().key).orElse(new byte[0]); - try { - JSONObject response = new JSONObject(); - response.put("hmac", new String(hmac, UTF_8)); - resp.getOutputStream().println(response.toString()); - } catch (Exception e) { - resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - } - } - } - }), "/hmac"); + startWebInterface(defaultContext, basicAuthContext); } server.addConnector(http); @@ -291,6 +237,10 @@ public class HttpServer { } catch (BindException e) { LOG.error("Port {} is already in use", http.getPort(), e); System.exit(1); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.error("Server start failed", e); + System.exit(1); } catch (Exception e) { LOG.error("Server start failed", e); System.exit(1); @@ -298,6 +248,38 @@ public class HttpServer { } } + private void startWebInterface(ServletContextHandler defaultContext, ServletContextHandler basicAuthContext) { + StaticFileServlet staticFileServlet = new StaticFileServlet("/html"); + ServletHolder holder = new ServletHolder(staticFileServlet); + String staticFileContext = "/static/*"; + defaultContext.addServlet(holder, staticFileContext); + LOG.info("Register static file servlet under {}", defaultContext.getContextPath()+staticFileContext); + + // servlet to retrieve the HMAC (secured by basic auth if a hmac key is set in the config) + String username = this.config.getSettings().webinterfaceUsername; + String password = this.config.getSettings().webinterfacePassword; + if (config.getSettings().key != null && config.getSettings().key.length > 0) { + basicAuthContext.setSecurityHandler(basicAuth(username, password)); + } + basicAuthContext.addServlet(new ServletHolder(new HttpServlet() { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) { + if (Objects.equals(username, req.getRemoteUser())) { + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/json"); + byte[] hmac = Optional.ofNullable(HttpServer.this.config.getSettings().key).orElse(new byte[0]); + try { + JSONObject response = new JSONObject(); + response.put("hmac", new String(hmac, UTF_8)); + resp.getOutputStream().println(response.toString()); + } catch (Exception e) { + resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + } + }), "/hmac"); + } + private ErrorHandler createErrorHandler(String contextPath) { return new ErrorHandler() { @Override @@ -325,7 +307,7 @@ public class HttpServer { private void addHttpHeaderFilter(ServletContextHandler defaultContext) { FilterHolder httpHeaderFilter = new FilterHolder(new Filter() { @Override - public void init(FilterConfig filterConfig) throws ServletException { + public void init(FilterConfig filterConfig) { // noop } @@ -343,7 +325,8 @@ public class HttpServer { defaultContext.addFilter(httpHeaderFilter, "/*", EnumSet.of(DispatcherType.REQUEST, DispatcherType.INCLUDE)); } - private static final SecurityHandler basicAuth(String username, String password, String realm) { + private static SecurityHandler basicAuth(String username, String password) { + String realm = "CTB Recorder"; UserStore userStore = new UserStore(); userStore.addUser(username, Credential.getCredential(password), new String[] { "user" }); HashLoginService l = new HashLoginService(); @@ -361,7 +344,7 @@ public class HttpServer { ConstraintSecurityHandler csh = new ConstraintSecurityHandler(); csh.setAuthenticator(new BasicAuthenticator()); - csh.setRealmName("myrealm"); + csh.setRealmName(realm); csh.addConstraintMapping(cm); csh.setLoginService(l); @@ -389,7 +372,7 @@ public class HttpServer { private Version getVersion() throws IOException { try (InputStream is = getClass().getClassLoader().getResourceAsStream("version")) { - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull(is))); String versionString = reader.readLine(); Version version = Version.of(versionString); return version; diff --git a/server/src/main/resources/html/static/index.html b/server/src/main/resources/html/static/index.html index a4c18513..51e2262c 100644 --- a/server/src/main/resources/html/static/index.html +++ b/server/src/main/resources/html/static/index.html @@ -162,13 +162,14 @@ - + - + @@ -259,8 +260,8 @@ let observableRecordingsArray = ko.observableArray(); let observableSettingsArray = ko.observableArray(); let space = { - free: ko.observable(0), - total: ko.observable(0), + free: ko.observable(0), + total: ko.observable(0), percent: ko.observable(0), text: ko.observable('') }; @@ -280,8 +281,8 @@ }); } else { $('#addModelByUrl').autocomplete({ - source: ["BongaCams:", "Cam4:", "Camsoda:", "Chaturbate:", "Fc2Live:", "Flirt4Free:", "LiveJasmin:", "MyFreeCams:", "MVLive:", "Showup:", "Streamate:", "Stripchat:"] - }); + source: ["BongaCams:", "Cam4:", "Camsoda:", "Chaturbate:", "CherryTv:", "Fc2Live:", "Flirt4Free:", "LiveJasmin:", "MyFreeCams:", "MVLive:", "Showup:", "Streamate:", "Stripchat:"] + }); } } @@ -291,7 +292,7 @@ let model = { type: null, name: '', - url: input + url: input }; if(console) console.log(model); @@ -442,7 +443,7 @@ $(document).ready(function() { if (localStorage !== undefined && localStorage.hmac !== undefined) { if(console) console.log('using hmac from local storage'); - hmac = localStorage.hmac; + hmac = localStorage.hmac; } else { if(console) console.log('hmac not found in local storage. requesting hmac from server'); $.ajax({ @@ -461,7 +462,7 @@ }) .fail(function(jqXHR, textStatus, errorThrown) { if(console) console.log(textStatus, errorThrown); - $.notify('Couldn\'t get HMAC', 'error'); + $.notify('Could not get HMAC', 'error'); hmac = ''; }); } From c5c2ed12adc44721b74c9a1544530de0ea8c85d7 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sat, 6 Nov 2021 17:41:34 +0100 Subject: [PATCH 02/10] Implement cherry.tv search --- .../java/ctbrec/sites/cherrytv/CherryTv.java | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java b/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java index b8174de5..02ed5af0 100644 --- a/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java +++ b/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java @@ -4,18 +4,16 @@ import ctbrec.Model; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import ctbrec.sites.AbstractSite; -import okhttp3.HttpUrl; import okhttp3.Request; import okhttp3.Response; +import org.json.JSONArray; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Objects; +import java.net.URLEncoder; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -111,32 +109,37 @@ public class CherryTv extends AbstractSite { @Override public List search(String q) throws IOException, InterruptedException { - JSONObject variables = new JSONObject().put("slug", q).put("limit", 10); - JSONObject persistedQuery = new JSONObject().put("persistedQuery", new JSONObject().put("version", 1).put("sha256Hash", "")); + String url = "https://cherry.tv/graphql?operationName=findStreamersBySearch&variables=" + + "{\"limit\":6,\"slug\":\"" + URLEncoder.encode(q, "utf-8") + "\"}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"03d2f017fee32e1b6a1d3f816ce226c464a78d8dab28895c321980fbadffc1ef\"}}"; Request req = new Request.Builder() - .url(new HttpUrl.Builder() - .scheme("https") - .host("cherry.tv") - .addPathSegment("qraphql") - .addQueryParameter("operationName", "Search") - .addQueryParameter("variables", variables.toString()) - .addQueryParameter("extensions", persistedQuery.toString()) - .build() - ) + .url(url) .header(USER_AGENT, getConfig().getSettings().httpUserAgent) .header(ACCEPT, MIMETYPE_APPLICATION_JSON) .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .header(REFERER, getBaseUrl()) .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) .build(); + LOG.debug("Search URL: {}", req.url()); + List result = new LinkedList<>(); try (Response response = getHttpClient().execute(req)) { if (response.isSuccessful()) { - LOG.debug("Response: {}", Objects.requireNonNull(response.body()).string()); + JSONObject json = new JSONObject(Objects.requireNonNull(response.body()).string()); + LOG.debug(json.toString(2)); + JSONObject data = json.getJSONObject("data"); + JSONObject searchResult = data.getJSONObject("searchResult"); + JSONArray streamers = searchResult.getJSONArray("streamers"); + for (int i = 0; i < streamers.length(); i++) { + JSONObject hit = streamers.getJSONObject(i); + CherryTvModel model = createModel(hit.getString("username")); + model.setDescription(hit.getString("description")); + model.setPreview(hit.getString("imageUrl")); + result.add(model); + } } else { throw new HttpException(response.code(), response.message()); } } - return Collections.emptyList(); + return result; } @Override From 8fca82613e7a3864761bffbc7ad3acbcffe2cd7e Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sat, 6 Nov 2021 17:56:14 +0100 Subject: [PATCH 03/10] Add group show and trans tabs for cherry.tv --- .../ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java index 3bc63f66..a0259bf5 100644 --- a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java @@ -23,13 +23,9 @@ public class CherryTvTabProvider implements TabProvider { @Override public List getTabs(Scene scene) { List tabs = new ArrayList<>(); - - tabs.add(createTab("Female", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"female\",\"tag\":null,\"following\":null,\"limit\":50}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); -// tabs.add(createTab("Male", site.getBaseUrl() + "/directoryResults?online=true&gender=male&orderBy=MOST_VIEWERS")); -// tabs.add(createTab("Couples", site.getBaseUrl() + "/directoryResults?online=true&broadcastType=male_group&broadcastType=female_group&broadcastType=male_female_group&orderBy=MOST_VIEWERS")); -// tabs.add(createTab("HD", site.getBaseUrl() + "/directoryResults?online=true&hd=true&orderBy=MOST_VIEWERS")); -// tabs.add(createTab("New", site.getBaseUrl() + "/directoryResults?online=true&gender=female&orderBy=MOST_VIEWERS&newPerformer=true")); - + tabs.add(createTab("Female", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"female\",\"tag\":null,\"following\":null,\"limit\":100}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); + tabs.add(createTab("Trans", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"trans\",\"tag\":null,\"following\":null,\"limit\":100}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); + tabs.add(createTab("Group Show", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"groupshow\",\"tag\":null,\"following\":null,\"limit\":100}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); return tabs; } From ef7e109d7176aee33cc3b9ce58fd05a31e42b36b Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sat, 6 Nov 2021 17:57:29 +0100 Subject: [PATCH 04/10] Remove apollo client again --- .../ui/sites/cherrytv/CherryTvUpdateService.java | 13 ++----------- common/pom.xml | 5 ----- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java index 491337cf..0f09e075 100644 --- a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java @@ -1,9 +1,7 @@ package ctbrec.ui.sites.cherrytv; -import com.apollographql.apollo.ApolloClient; import ctbrec.Config; import ctbrec.Model; -import ctbrec.io.HtmlParser; import ctbrec.io.HttpException; import ctbrec.sites.cherrytv.CherryTv; import ctbrec.sites.cherrytv.CherryTvModel; @@ -13,7 +11,6 @@ import okhttp3.Request; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.jsoup.nodes.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,7 +22,8 @@ import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import static ctbrec.Model.State.*; +import static ctbrec.Model.State.OFFLINE; +import static ctbrec.Model.State.ONLINE; import static ctbrec.io.HttpConstants.ACCEPT_LANGUAGE; import static ctbrec.io.HttpConstants.USER_AGENT; @@ -34,7 +32,6 @@ public class CherryTvUpdateService extends PaginatedScheduledService { private static final Logger LOG = LoggerFactory.getLogger(CherryTvUpdateService.class); private String url; private final CherryTv site; - private ApolloClient apolloClient; public CherryTvUpdateService(String url, CherryTv site) { this.site = site; @@ -47,10 +44,6 @@ public class CherryTvUpdateService extends PaginatedScheduledService { return t; }); setExecutor(executor); - - apolloClient = ApolloClient.builder() - .serverUrl(site.getBaseUrl() + "/graphql") - .build(); } @Override @@ -60,7 +53,6 @@ public class CherryTvUpdateService extends PaginatedScheduledService { public List call() throws IOException { String pageUrl = CherryTvUpdateService.this.url; LOG.debug("Fetching page {}", pageUrl); - apolloClient. var request = new Request.Builder() .url(pageUrl) @@ -80,7 +72,6 @@ public class CherryTvUpdateService extends PaginatedScheduledService { private List parseModels(String body) { var json = new JSONObject(body); - // LOG.debug(json.toString(2)); List models = new ArrayList<>(); try { JSONArray broadcasts = json.getJSONObject("data").getJSONObject("broadcasts").getJSONArray("broadcasts"); diff --git a/common/pom.xml b/common/pom.xml index 877d8644..24285590 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -50,11 +50,6 @@ commons-io commons-io - - com.apollographql.apollo - apollo-runtime - 2.5.9 - javax.servlet javax.servlet-api From 50b2a2a6728ae9e4d2509cb599082f0e0e6aec61 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sat, 6 Nov 2021 19:18:18 +0100 Subject: [PATCH 05/10] Limit to 50 per page --- .../java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java index a0259bf5..52a9395b 100644 --- a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java @@ -23,9 +23,9 @@ public class CherryTvTabProvider implements TabProvider { @Override public List getTabs(Scene scene) { List tabs = new ArrayList<>(); - tabs.add(createTab("Female", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"female\",\"tag\":null,\"following\":null,\"limit\":100}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); - tabs.add(createTab("Trans", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"trans\",\"tag\":null,\"following\":null,\"limit\":100}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); - tabs.add(createTab("Group Show", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"groupshow\",\"tag\":null,\"following\":null,\"limit\":100}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); + tabs.add(createTab("Female", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"female\",\"tag\":null,\"following\":null,\"limit\":50}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); + tabs.add(createTab("Trans", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"trans\",\"tag\":null,\"following\":null,\"limit\":50}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); + tabs.add(createTab("Group Show", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"groupshow\",\"tag\":null,\"following\":null,\"limit\":50}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); return tabs; } From c4c5818496807653607352bc6bb95286dc045281 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sat, 6 Nov 2021 19:38:50 +0100 Subject: [PATCH 06/10] Implement pagination for cherry.tv --- .../java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java | 6 +++--- .../ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java index 52a9395b..09964060 100644 --- a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java @@ -23,9 +23,9 @@ public class CherryTvTabProvider implements TabProvider { @Override public List getTabs(Scene scene) { List tabs = new ArrayList<>(); - tabs.add(createTab("Female", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"female\",\"tag\":null,\"following\":null,\"limit\":50}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); - tabs.add(createTab("Trans", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"trans\",\"tag\":null,\"following\":null,\"limit\":50}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); - tabs.add(createTab("Group Show", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"groupshow\",\"tag\":null,\"following\":null,\"limit\":50}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); + tabs.add(createTab("Female", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"female\",\"tag\":null,\"following\":null,\"limit\":${limit},\"cursor\":${offset}}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); + tabs.add(createTab("Trans", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"trans\",\"tag\":null,\"following\":null,\"limit\":${limit},\"cursor\":${offset}}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); + tabs.add(createTab("Group Show", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"groupshow\",\"tag\":null,\"following\":null,\"limit\":${limit},\"cursor\":${offset}}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); return tabs; } diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java index 0f09e075..cfe7f5ed 100644 --- a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java @@ -30,6 +30,7 @@ import static ctbrec.io.HttpConstants.USER_AGENT; public class CherryTvUpdateService extends PaginatedScheduledService { private static final Logger LOG = LoggerFactory.getLogger(CherryTvUpdateService.class); + private static final int MODELS_PER_PAGE = 100; private String url; private final CherryTv site; @@ -52,6 +53,8 @@ public class CherryTvUpdateService extends PaginatedScheduledService { @Override public List call() throws IOException { String pageUrl = CherryTvUpdateService.this.url; + pageUrl = pageUrl.replace("${limit}", String.valueOf(MODELS_PER_PAGE)); + pageUrl = pageUrl.replace("${offset}", String.valueOf((page - 1) * MODELS_PER_PAGE)); LOG.debug("Fetching page {}", pageUrl); var request = new Request.Builder() From 36cacda106e220ecdf8cc028692902ab072648a4 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sun, 7 Nov 2021 13:35:28 +0100 Subject: [PATCH 07/10] Implement login and followed tab for cherry.tv --- .../ui/sites/cherrytv/CherryTvConfigUI.java | 21 ++-- .../sites/cherrytv/CherryTvFollowedTab.java | 85 ++++++++++++++ .../CherryTvFollowedUpdateService.java | 53 +++++++++ .../ui/sites/cherrytv/CherryTvSiteUi.java | 3 - .../sites/cherrytv/CherryTvTabProvider.java | 17 ++- .../sites/cherrytv/CherryTvUpdateService.java | 37 ++++-- .../main/java/ctbrec/LoggingInterceptor.java | 2 +- common/src/main/java/ctbrec/Settings.java | 2 + .../src/main/java/ctbrec/io/HttpClient.java | 5 +- .../sites/cherrytv/CherryTvHttpClient.java | 107 +++++++++++++++++- .../ctbrec/sites/cherrytv/CherryTvModel.java | 24 ++++ 11 files changed, 325 insertions(+), 31 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvFollowedTab.java create mode 100644 client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvFollowedUpdateService.java diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvConfigUI.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvConfigUI.java index 9ddfe293..82f0c8e4 100644 --- a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvConfigUI.java +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvConfigUI.java @@ -1,7 +1,6 @@ package ctbrec.ui.sites.cherrytv; import ctbrec.Config; -import ctbrec.sites.cam4.Cam4; import ctbrec.sites.cherrytv.CherryTv; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.settings.SettingsTab; @@ -30,7 +29,7 @@ public class CherryTvConfigUI extends AbstractConfigUI { var enabled = new CheckBox(); enabled.setSelected(!settings.disabledSites.contains(site.getName())); enabled.setOnAction(e -> { - if(enabled.isSelected()) { + if (enabled.isSelected()) { settings.disabledSites.remove(site.getName()); } else { settings.disabledSites.add(site.getName()); @@ -40,11 +39,11 @@ public class CherryTvConfigUI extends AbstractConfigUI { GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); layout.add(enabled, 1, row++); - layout.add(new Label("Cam4 User"), 0, row); - var username = new TextField(Config.getInstance().getSettings().cam4Username); + layout.add(new Label(site.getName() + " User"), 0, row); + var username = new TextField(Config.getInstance().getSettings().cherryTvUsername); username.textProperty().addListener((ob, o, n) -> { - if(!n.equals(Config.getInstance().getSettings().cam4Username)) { - Config.getInstance().getSettings().cam4Username = username.getText(); + if (!n.equals(Config.getInstance().getSettings().cherryTvUsername)) { + Config.getInstance().getSettings().cherryTvUsername = username.getText(); site.getHttpClient().logout(); save(); } @@ -54,12 +53,12 @@ public class CherryTvConfigUI extends AbstractConfigUI { GridPane.setColumnSpan(username, 2); layout.add(username, 1, row++); - layout.add(new Label("Cam4 Password"), 0, row); + layout.add(new Label(site.getName() + " Password"), 0, row); var password = new PasswordField(); - password.setText(Config.getInstance().getSettings().cam4Password); + password.setText(Config.getInstance().getSettings().cherryTvPassword); password.textProperty().addListener((ob, o, n) -> { - if(!n.equals(Config.getInstance().getSettings().cam4Password)) { - Config.getInstance().getSettings().cam4Password = password.getText(); + if (!n.equals(Config.getInstance().getSettings().cherryTvPassword)) { + Config.getInstance().getSettings().cherryTvPassword = password.getText(); site.getHttpClient().logout(); save(); } @@ -70,7 +69,7 @@ public class CherryTvConfigUI extends AbstractConfigUI { layout.add(password, 1, row++); var createAccount = new Button("Create new Account"); - createAccount.setOnAction(e -> DesktopIntegration.open(Cam4.AFFILIATE_LINK)); + createAccount.setOnAction(e -> DesktopIntegration.open(site.getAffiliateLink())); layout.add(createAccount, 1, row++); GridPane.setColumnSpan(createAccount, 2); diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvFollowedTab.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvFollowedTab.java new file mode 100644 index 00000000..f6b66200 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvFollowedTab.java @@ -0,0 +1,85 @@ + +package ctbrec.ui.sites.cherrytv; + +import ctbrec.sites.cherrytv.CherryTv; +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 CherryTvFollowedTab extends ThumbOverviewTab implements FollowedTab { + private final Label status; + private ToggleGroup group; + + public CherryTvFollowedTab(String title, CherryTv site) { + super(title, new CherryTvFollowedUpdateService(site), site); + status = new Label("Logging in..."); + grid.getChildren().add(status); + } + + @Override + protected void createGui() { + super.createGui(); + group = new ToggleGroup(); + addOnlineOfflineSelector(); + } + + private void addOnlineOfflineSelector() { + var online = new RadioButton("online"); + online.setToggleGroup(group); + var 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 -> { + ((CherryTvUpdateService) updateService).setFilter(m -> { + try { + return m.isOnline(false) == online.isSelected(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return false; + } catch (Exception ex) { + return false; + } + }); + queue.clear(); + updateService.restart(); + }); + } + + @Override + protected void onSuccess() { + grid.getChildren().remove(status); + super.onSuccess(); + } + + @Override + protected void onFail(WorkerStateEvent event) { + status.setText("Login failed"); + 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/cherrytv/CherryTvFollowedUpdateService.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvFollowedUpdateService.java new file mode 100644 index 00000000..92ce5198 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvFollowedUpdateService.java @@ -0,0 +1,53 @@ +package ctbrec.ui.sites.cherrytv; + +import ctbrec.Model; +import ctbrec.sites.cherrytv.CherryTv; +import ctbrec.sites.cherrytv.CherryTvModel; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static ctbrec.Model.State.OFFLINE; +import static ctbrec.Model.State.ONLINE; + +public class CherryTvFollowedUpdateService extends CherryTvUpdateService { + + private static final Logger LOG = LoggerFactory.getLogger(CherryTvFollowedUpdateService.class); + + public CherryTvFollowedUpdateService(CherryTv site) { + super(site.getBaseUrl() + "/graphql?operationName=FindFollowings&variables={\"cursor\":${offset},\"limit\":${limit}}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"7d2cf16b113dc1d57af02685e249e28df9649ea598717dc2c877294529ae0cb3\"}}", + site,true); + } + + protected List parseModels(String body) throws IOException { + var json = new JSONObject(body); + if (json.has("errors")) { + JSONArray errors = json.getJSONArray("errors"); + JSONObject first = errors.getJSONObject(0); + throw new IOException(first.getString("message")); + } + List models = new ArrayList<>(); + try { + JSONArray followings = json.getJSONObject("data").getJSONObject("followinglist").getJSONArray("followings"); + for (int i = 0; i < followings.length(); i++) { + JSONObject following = followings.getJSONObject(i); + CherryTvModel model = site.createModel(following.optString("username")); + model.setId(following.getString("id")); + model.setPreview(following.optString("img")); + var online = following.optString("status").equalsIgnoreCase("Live"); + model.setOnline(online); + model.setOnlineState(online ? ONLINE : OFFLINE); + models.add(model); + } + } catch (JSONException e) { + LOG.error("Couldn't parse JSON, the structure might have changed", e); + } + return models; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvSiteUi.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvSiteUi.java index 58844da8..33c62dcf 100644 --- a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvSiteUi.java @@ -1,9 +1,6 @@ package ctbrec.ui.sites.cherrytv; -import ctbrec.sites.cam4.Cam4; -import ctbrec.sites.cam4.Cam4HttpClient; import ctbrec.sites.cherrytv.CherryTv; -import ctbrec.ui.controls.Dialogs; import ctbrec.ui.sites.AbstractSiteUi; import ctbrec.ui.sites.ConfigUI; import ctbrec.ui.tabs.TabProvider; diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java index 09964060..6b283ffa 100644 --- a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvTabProvider.java @@ -14,28 +14,35 @@ public class CherryTvTabProvider implements TabProvider { private final CherryTv site; private final Recorder recorder; + private final CherryTvFollowedTab followedTab; public CherryTvTabProvider(CherryTv cherryTv) { this.site = cherryTv; this.recorder = cherryTv.getRecorder(); + + followedTab = new CherryTvFollowedTab("Following", site); + followedTab.setImageAspectRatio(9.0 / 16.0); + followedTab.preserveAspectRatioProperty().set(false); + followedTab.setRecorder(recorder); } @Override public List getTabs(Scene scene) { List tabs = new ArrayList<>(); - tabs.add(createTab("Female", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"female\",\"tag\":null,\"following\":null,\"limit\":${limit},\"cursor\":${offset}}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); - tabs.add(createTab("Trans", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"trans\",\"tag\":null,\"following\":null,\"limit\":${limit},\"cursor\":${offset}}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); - tabs.add(createTab("Group Show", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"groupshow\",\"tag\":null,\"following\":null,\"limit\":${limit},\"cursor\":${offset}}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); + tabs.add(createTab("Female", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"female\",\"tag\":null,\"following\":null,\"limit\":${limit},\"cursor\":${offset}}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); + tabs.add(createTab("Trans", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"trans\",\"tag\":null,\"following\":null,\"limit\":${limit},\"cursor\":${offset}}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); + tabs.add(createTab("Group Show", site.getBaseUrl() + "/graphql?operationName=findBroadcastsByPage&variables={\"slug\":\"groupshow\",\"tag\":null,\"following\":null,\"limit\":${limit},\"cursor\":${offset}}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"f1e214ca901e525301fcc6966cf081ee95ef777974ec184897c4a2cf15e9ac6f\"}}")); + tabs.add(followedTab); return tabs; } @Override public Tab getFollowedTab() { - return null; + return followedTab; } private Tab createTab(String name, String url) { - var updateService = new CherryTvUpdateService(url, site); + var updateService = new CherryTvUpdateService(url, site, false); var tab = new ThumbOverviewTab(name, updateService, site); tab.setImageAspectRatio(9.0 / 16.0); tab.preserveAspectRatioProperty().set(false); diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java index cfe7f5ed..6f39dde8 100644 --- a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java @@ -21,6 +21,9 @@ import java.util.Locale; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static ctbrec.Model.State.OFFLINE; import static ctbrec.Model.State.ONLINE; @@ -31,12 +34,16 @@ public class CherryTvUpdateService extends PaginatedScheduledService { private static final Logger LOG = LoggerFactory.getLogger(CherryTvUpdateService.class); private static final int MODELS_PER_PAGE = 100; - private String url; - private final CherryTv site; - public CherryTvUpdateService(String url, CherryTv site) { + private final String url; + private final boolean loginRequired; + protected final CherryTv site; + private Predicate filter; + + public CherryTvUpdateService(String url, CherryTv site, boolean loginRequired) { this.site = site; this.url = url; + this.loginRequired = loginRequired; ExecutorService executor = Executors.newSingleThreadExecutor(r -> { var t = new Thread(r); @@ -52,6 +59,10 @@ public class CherryTvUpdateService extends PaginatedScheduledService { return new Task<>() { @Override public List call() throws IOException { + if (loginRequired && !site.getHttpClient().login()) { + throw new IOException("Login failed"); + } + String pageUrl = CherryTvUpdateService.this.url; pageUrl = pageUrl.replace("${limit}", String.valueOf(MODELS_PER_PAGE)); pageUrl = pageUrl.replace("${offset}", String.valueOf((page - 1) * MODELS_PER_PAGE)); @@ -64,7 +75,13 @@ public class CherryTvUpdateService extends PaginatedScheduledService { .build(); try (var response = site.getHttpClient().execute(request)) { if (response.isSuccessful()) { - return parseModels(Objects.requireNonNull(response.body()).string()); + String body = Objects.requireNonNull(response.body()).string(); + LOG.debug(body); + Stream stream = parseModels(body).stream(); + if (filter != null) { + stream = stream.filter(filter); + } + return stream.collect(Collectors.toList()); } else { throw new HttpException(response.code(), response.message()); } @@ -73,8 +90,13 @@ public class CherryTvUpdateService extends PaginatedScheduledService { }; } - private List parseModels(String body) { + protected List parseModels(String body) throws IOException { var json = new JSONObject(body); + if (json.has("errors")) { + JSONArray errors = json.getJSONArray("errors"); + JSONObject first = errors.getJSONObject(0); + throw new IOException(first.getString("message")); + } List models = new ArrayList<>(); try { JSONArray broadcasts = json.getJSONObject("data").getJSONObject("broadcasts").getJSONArray("broadcasts"); @@ -102,8 +124,7 @@ public class CherryTvUpdateService extends PaginatedScheduledService { return models; } - public void setUrl(String url) { - this.url = url; + public void setFilter(Predicate filter) { + this.filter = filter; } - } diff --git a/common/src/main/java/ctbrec/LoggingInterceptor.java b/common/src/main/java/ctbrec/LoggingInterceptor.java index b2a4457f..8a8a52dc 100644 --- a/common/src/main/java/ctbrec/LoggingInterceptor.java +++ b/common/src/main/java/ctbrec/LoggingInterceptor.java @@ -23,7 +23,7 @@ public class LoggingInterceptor implements Interceptor { } Response response = chain.proceed(request); long t2 = System.nanoTime(); - LOG.debug("OkHttp Received response for {} in {}\n{}", response.request().url(), (t2 - t1) / 1e6d, response.headers()); + LOG.debug("OkHttp Received {} response for {} in {}ms\n{}", response.code(), response.request().url(), (t2 - t1) / 1e6d, response.headers()); return response; } } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 9d46397d..6c3c4e3a 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -58,6 +58,8 @@ public class Settings { public String chaturbateUsername = ""; public String chaturbateBaseUrl = "https://chaturbate.com"; public int chaturbateMsBetweenRequests = 1000; + public String cherryTvPassword = ""; + public String cherryTvUsername = ""; public boolean chooseStreamQuality = false; public String colorAccent = "#FFFFFF"; public String colorBase = "#FFFFFF"; diff --git a/common/src/main/java/ctbrec/io/HttpClient.java b/common/src/main/java/ctbrec/io/HttpClient.java index 332469a8..da4b4262 100644 --- a/common/src/main/java/ctbrec/io/HttpClient.java +++ b/common/src/main/java/ctbrec/io/HttpClient.java @@ -144,8 +144,9 @@ public abstract class HttpClient { .cookieJar(cookieJar) .connectionPool(GLOBAL_HTTP_CONN_POOL) .connectTimeout(config.getSettings().httpTimeout, TimeUnit.MILLISECONDS) - .readTimeout(config.getSettings().httpTimeout, TimeUnit.MILLISECONDS); - //.addInterceptor(new LoggingInterceptor()); + .readTimeout(config.getSettings().httpTimeout, TimeUnit.MILLISECONDS) + //.addNetworkInterceptor(new LoggingInterceptor()) + ; ProxyType proxyType = config.getSettings().proxyType; if (proxyType == ProxyType.HTTP) { diff --git a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java index ff402c32..a7549921 100644 --- a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java +++ b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java @@ -2,17 +2,122 @@ package ctbrec.sites.cherrytv; import ctbrec.Config; import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; +import okhttp3.*; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Objects; + +import static ctbrec.io.HttpConstants.*; public class CherryTvHttpClient extends HttpClient { + private static final Logger LOG = LoggerFactory.getLogger(CherryTvHttpClient.class); + public CherryTvHttpClient(Config config) { super("cherrytv", config); } @Override public synchronized boolean login() throws IOException { - return false; + if (loggedIn) { + return true; + } + + boolean cookiesWorked = checkLoginSuccess(); + if (cookiesWorked) { + loggedIn = true; + LOG.debug("Logged in with cookies"); + return true; + } + + JSONObject body = new JSONObject() + .put("operationName", "authenticateUser") + .put("variables", new JSONObject() + .put("username", config.getSettings().cherryTvUsername) + .put("password", config.getSettings().cherryTvPassword) + ) + .put("extensions", new JSONObject() + .put("persistedQuery", new JSONObject() + .put("version", 1) + .put("sha256Hash", "9c105878022f9a7d511c12527c70f125606dc25367a4dd56aa63a6af73579087") + ) + ); + + + RequestBody requestBody = RequestBody.create(body.toString(), MediaType.parse("application/json")); + Request request = new Request.Builder() + .url(CherryTv.BASE_URL + "/graphql") + .header(REFERER, CherryTv.BASE_URL) + .header(ORIGIN, CherryTv.BASE_URL) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .post(requestBody) + .build(); + + LOG.debug("Logging in: {}\n{}", request.url(), body.toString(2)); + try (Response response = execute(request)) { + if (response.isSuccessful()) { + JSONObject resp = new JSONObject(Objects.requireNonNull(response.body()).string()); + LOG.info(resp.toString(2)); + JSONObject data = resp.getJSONObject("data"); + JSONObject login = data.getJSONObject("login"); + loggedIn = login.optBoolean("success"); + String jwt = login.optString("token"); + saveAsSessionCookie(jwt); + LOG.debug("Login successful"); + return loggedIn; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private void saveAsSessionCookie(String jwt) { + HttpUrl url = HttpUrl.parse(CherryTv.BASE_URL); + Objects.requireNonNull(url); + long expiresAt = Instant.now().plus(1, ChronoUnit.DAYS).getEpochSecond(); + Cookie sessionCookie = new Cookie.Builder() + .name("session") + .value(jwt) + .expiresAt(expiresAt) + .domain(Objects.requireNonNull(url.topPrivateDomain())) + .path("/") + .secure().httpOnly() + .build(); + getCookieJar().saveFromResponse(url, Collections.singletonList(sessionCookie)); + } + + private boolean checkLoginSuccess() { + String url = CherryTv.BASE_URL + "/graphql?operationName=FindFollowings&variables={\"cursor\":\"0\",\"limit\":20}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"7d2cf16b113dc1d57af02685e249e28df9649ea598717dc2c877294529ae0cb3\"}}"; + Request request = new Request.Builder() + .url(url) + .header(REFERER, CherryTv.BASE_URL) + .header(ORIGIN, CherryTv.BASE_URL) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .build(); + + try (Response response = execute(request)) { + String body = Objects.requireNonNull(response.body()).string(); + LOG.debug("Login body: {}", body); + if (response.isSuccessful()) { + JSONObject json = new JSONObject(body); + if (json.has("errors")) { + LOG.error(json.toString(2)); + return false; + } else { + return json.optString("__typename").equals("FollowingList"); + } + } + return false; + } catch (Exception e) { + LOG.error("Login check failed", e); + return false; + } } } diff --git a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java index 7fcfe3c4..94e194c4 100644 --- a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java +++ b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java @@ -4,6 +4,8 @@ import com.iheartradio.m3u8.*; import com.iheartradio.m3u8.data.MasterPlaylist; import com.iheartradio.m3u8.data.Playlist; import com.iheartradio.m3u8.data.PlaylistData; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.NotImplementedExcetion; @@ -39,6 +41,7 @@ public class CherryTvModel extends AbstractModel { private boolean online = false; private int[] resolution; private String masterPlaylistUrl; + private String id; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { @@ -198,6 +201,8 @@ public class CherryTvModel extends AbstractModel { @Override public boolean follow() throws IOException { + // POST https://cherry.tv/graphql + // {"operationName":"follow","variables":{"userId":"1391"},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"a7a8241014074f5c02ac83863a47f7579e5f03167a7ec424ff65ad045c7fcf6f"}}} return false; } @@ -222,4 +227,23 @@ public class CherryTvModel extends AbstractModel { setOnlineState(OFFLINE); } } + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + @Override + public void readSiteSpecificData(JsonReader reader) throws IOException { + reader.nextName(); + id = reader.nextString(); + } + + @Override + public void writeSiteSpecificData(JsonWriter writer) throws IOException { + writer.name("id").value(id); + } } From 881e8afb4c5b814aeefd16ede397e370a7c5b69c Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sun, 7 Nov 2021 13:43:48 +0100 Subject: [PATCH 08/10] Implement login and followed tab for cherry.tv --- .../sites/cherrytv/CherryTvFollowedTab.java | 25 +++++++++++-------- .../sites/cherrytv/CherryTvUpdateService.java | 1 - .../sites/cherrytv/CherryTvHttpClient.java | 4 +-- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvFollowedTab.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvFollowedTab.java index f6b66200..07bb6684 100644 --- a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvFollowedTab.java +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvFollowedTab.java @@ -29,6 +29,7 @@ public class CherryTvFollowedTab extends ThumbOverviewTab implements FollowedTab super.createGui(); group = new ToggleGroup(); addOnlineOfflineSelector(); + setFilter(true); } private void addOnlineOfflineSelector() { @@ -42,21 +43,25 @@ public class CherryTvFollowedTab extends ThumbOverviewTab implements FollowedTab HBox.setMargin(offline, new Insets(5, 5, 5, 5)); online.setSelected(true); group.selectedToggleProperty().addListener(e -> { - ((CherryTvUpdateService) updateService).setFilter(m -> { - try { - return m.isOnline(false) == online.isSelected(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - return false; - } catch (Exception ex) { - return false; - } - }); + setFilter(online.isSelected()); queue.clear(); updateService.restart(); }); } + private void setFilter(boolean online) { + ((CherryTvUpdateService) updateService).setFilter(m -> { + try { + return m.isOnline(false) == online; + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return false; + } catch (Exception ex) { + return false; + } + }); + } + @Override protected void onSuccess() { grid.getChildren().remove(status); diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java index 6f39dde8..f5af4965 100644 --- a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java @@ -76,7 +76,6 @@ public class CherryTvUpdateService extends PaginatedScheduledService { try (var response = site.getHttpClient().execute(request)) { if (response.isSuccessful()) { String body = Objects.requireNonNull(response.body()).string(); - LOG.debug(body); Stream stream = parseModels(body).stream(); if (filter != null) { stream = stream.filter(filter); diff --git a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java index a7549921..503bc951 100644 --- a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java +++ b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java @@ -60,11 +60,11 @@ public class CherryTvHttpClient extends HttpClient { .post(requestBody) .build(); - LOG.debug("Logging in: {}\n{}", request.url(), body.toString(2)); + LOG.debug("Logging in: {}", request.url()); try (Response response = execute(request)) { if (response.isSuccessful()) { JSONObject resp = new JSONObject(Objects.requireNonNull(response.body()).string()); - LOG.info(resp.toString(2)); + LOG.trace(resp.toString(2)); JSONObject data = resp.getJSONObject("data"); JSONObject login = data.getJSONObject("login"); loggedIn = login.optBoolean("success"); From 0d47952d3d79fd646ce060cd5635c20676b6c3a4 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sun, 7 Nov 2021 14:01:58 +0100 Subject: [PATCH 09/10] Make sure, that the model id is always set --- .../sites/cherrytv/CherryTvUpdateService.java | 1 + .../java/ctbrec/sites/cherrytv/CherryTv.java | 22 +++++++++++++++++-- .../ctbrec/sites/cherrytv/CherryTvModel.java | 3 +-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java index f5af4965..c0442391 100644 --- a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java @@ -102,6 +102,7 @@ public class CherryTvUpdateService extends PaginatedScheduledService { for (int i = 0; i < broadcasts.length(); i++) { JSONObject broadcast = broadcasts.getJSONObject(i); CherryTvModel model = site.createModel(broadcast.optString("username")); + model.setId(broadcast.getString("id")); model.setDisplayName(broadcast.optString("title")); model.setDescription(broadcast.optString("description")); model.setPreview(broadcast.optString("thumbnailUrl")); diff --git a/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java b/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java index 02ed5af0..87c5811f 100644 --- a/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java +++ b/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java @@ -13,10 +13,15 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URLEncoder; -import java.util.*; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static ctbrec.Model.State.OFFLINE; +import static ctbrec.Model.State.ONLINE; import static ctbrec.io.HttpConstants.*; public class CherryTv extends AbstractSite { @@ -131,6 +136,11 @@ public class CherryTv extends AbstractSite { for (int i = 0; i < streamers.length(); i++) { JSONObject hit = streamers.getJSONObject(i); CherryTvModel model = createModel(hit.getString("username")); + model.setId(hit.getString("id")); + boolean online = hit.optString("showStatus").equalsIgnoreCase("Public") + && hit.optString("broadcastStatus").equalsIgnoreCase("Live"); + model.setOnline(online); + model.setOnlineState(online ? ONLINE : OFFLINE); model.setDescription(hit.getString("description")); model.setPreview(hit.getString("imageUrl")); result.add(model); @@ -157,7 +167,15 @@ public class CherryTv extends AbstractSite { Matcher m = Pattern.compile("https?://.*?cherry\\.tv/([^/]*?)/?").matcher(url); if (m.matches()) { String modelName = m.group(1); - return createModel(modelName); + CherryTvModel model = createModel(modelName); + try { + model.isOnline(true); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (Exception e) { + LOG.warn("Couldn't determine model id. This could cause problems in the future", e); + } + return model; } else { return super.createModelFromUrl(url); } diff --git a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java index 94e194c4..f0cf72dc 100644 --- a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java +++ b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java @@ -67,8 +67,7 @@ public class CherryTvModel extends AbstractModel { if (key.startsWith("Broadcast:")) { JSONObject broadcast = apolloState.getJSONObject(key); setDisplayName(broadcast.optString("title")); - // id = broadcast.getString("id"); - // roomId = broadcast.getString("roomId"); + id = broadcast.getString("id"); online = broadcast.optString("showStatus").equalsIgnoreCase("Public") && broadcast.optString("broadcastStatus").equalsIgnoreCase("Live"); onlineState = online ? ONLINE : OFFLINE; From 18e4a4369905983bcd4909828bb083a764c1958d Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Sun, 7 Nov 2021 16:10:03 +0100 Subject: [PATCH 10/10] Implement follow / unfollow --- .../sites/cherrytv/CherryTvUpdateService.java | 1 - .../src/main/java/ctbrec/io/HttpClient.java | 80 +++------ .../java/ctbrec/sites/cherrytv/CherryTv.java | 9 +- .../sites/cherrytv/CherryTvHttpClient.java | 20 ++- .../ctbrec/sites/cherrytv/CherryTvModel.java | 160 +++++++++++++----- 5 files changed, 158 insertions(+), 112 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java index c0442391..f5af4965 100644 --- a/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/cherrytv/CherryTvUpdateService.java @@ -102,7 +102,6 @@ public class CherryTvUpdateService extends PaginatedScheduledService { for (int i = 0; i < broadcasts.length(); i++) { JSONObject broadcast = broadcasts.getJSONObject(i); CherryTvModel model = site.createModel(broadcast.optString("username")); - model.setId(broadcast.getString("id")); model.setDisplayName(broadcast.optString("title")); model.setDescription(broadcast.optString("description")); model.setPreview(broadcast.optString("thumbnailUrl")); diff --git a/common/src/main/java/ctbrec/io/HttpClient.java b/common/src/main/java/ctbrec/io/HttpClient.java index da4b4262..bbfc4bd2 100644 --- a/common/src/main/java/ctbrec/io/HttpClient.java +++ b/common/src/main/java/ctbrec/io/HttpClient.java @@ -1,8 +1,16 @@ package ctbrec.io; -import static ctbrec.io.HttpConstants.*; -import static java.nio.charset.StandardCharsets.*; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; +import ctbrec.Config; +import ctbrec.LoggingInterceptor; +import ctbrec.Settings.ProxyType; +import okhttp3.*; +import okhttp3.OkHttpClient.Builder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.net.ssl.*; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -14,44 +22,16 @@ import java.nio.file.Files; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; -import javax.net.ssl.KeyManager; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; - -import ctbrec.Config; -import ctbrec.Settings.ProxyType; -import okhttp3.ConnectionPool; -import okhttp3.Cookie; -import okhttp3.Credentials; -import okhttp3.OkHttpClient; -import okhttp3.OkHttpClient.Builder; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.Route; -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; +import static ctbrec.io.HttpConstants.ACCEPT_ENCODING_GZIP; +import static ctbrec.io.HttpConstants.CONTENT_ENCODING; +import static java.nio.charset.StandardCharsets.UTF_8; public abstract class HttpClient { private static final Logger LOG = LoggerFactory.getLogger(HttpClient.class); @@ -59,11 +39,11 @@ public abstract class HttpClient { private static final ConnectionPool GLOBAL_HTTP_CONN_POOL = new ConnectionPool(10, 2, TimeUnit.MINUTES); protected OkHttpClient client; - protected CookieJarImpl cookieJar = new CookieJarImpl(); + protected CookieJarImpl cookieJar; protected Config config; protected boolean loggedIn = false; protected int loginTries = 0; - private String name; + private final String name; protected HttpClient(String name, Config config) { this.name = name; @@ -145,8 +125,7 @@ public abstract class HttpClient { .connectionPool(GLOBAL_HTTP_CONN_POOL) .connectTimeout(config.getSettings().httpTimeout, TimeUnit.MILLISECONDS) .readTimeout(config.getSettings().httpTimeout, TimeUnit.MILLISECONDS) - //.addNetworkInterceptor(new LoggingInterceptor()) - ; + .addNetworkInterceptor(new LoggingInterceptor()); ProxyType proxyType = config.getSettings().proxyType; if (proxyType == ProxyType.HTTP) { @@ -157,7 +136,7 @@ public abstract class HttpClient { } } - // if transport layer security (TLS) is switched on, accept the self signed cert from the server + // if transport layer security (TLS) is switched on, accept the self-signed cert from the server if (config.getSettings().transportLayerSecurity) { acceptAllTlsCerts(builder); } @@ -177,8 +156,8 @@ public abstract class HttpClient { X509Certificate[] x509Certificates = new X509Certificate[0]; return x509Certificates; } - @Override public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {} - @Override public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {} + @Override public void checkServerTrusted(final X509Certificate[] chain, final String authType) { /* noop*/ } + @Override public void checkClientTrusted(final X509Certificate[] chain, final String authType) { /* noop*/ } }; try { @@ -190,7 +169,7 @@ public abstract class HttpClient { sslContext.init(keyManagers, trustManagers, secureRandom); SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); builder.sslSocketFactory(sslSocketFactory, x509TrustManager); - builder.hostnameVerifier((name, sslSession) -> true); + builder.hostnameVerifier((hostname, sslSession) -> true); } catch (KeyManagementException | NoSuchAlgorithmException e) { LOG.error("Couldn't install trust managers for TLS connections"); } @@ -254,17 +233,14 @@ public abstract class HttpClient { } private okhttp3.Authenticator createHttpProxyAuthenticator(String username, String password) { - return new okhttp3.Authenticator() { - @Override - public Request authenticate(Route route, Response response) throws IOException { - String credential = Credentials.basic(username, password); - return response.request().newBuilder().header("Proxy-Authorization", credential).build(); - } + return (route, response) -> { + String credential = Credentials.basic(username, password); + return response.request().newBuilder().header("Proxy-Authorization", credential).build(); }; } public static class SocksProxyAuth extends Authenticator { - private PasswordAuthentication auth; + private final PasswordAuthentication auth; private SocksProxyAuth(String user, String password) { auth = new PasswordAuthentication(user, password == null ? new char[]{} : password.toCharArray()); @@ -327,16 +303,16 @@ public abstract class HttpClient { public static String gunzipBody(Response response) throws IOException { if (Objects.equals(response.header(CONTENT_ENCODING), ACCEPT_ENCODING_GZIP)) { - GZIPInputStream gzipIn = new GZIPInputStream(response.body().byteStream()); + GZIPInputStream gzipIn = new GZIPInputStream(Objects.requireNonNull(response.body()).byteStream()); ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] b = new byte[1024]; - int len = -1; + int len; while ((len = gzipIn.read(b)) >= 0) { bos.write(b, 0, len); } return bos.toString(StandardCharsets.UTF_8.toString()); } else { - return response.body().string(); + return Objects.requireNonNull(response.body()).string(); } } diff --git a/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java b/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java index 87c5811f..3cdcda7a 100644 --- a/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java +++ b/common/src/main/java/ctbrec/sites/cherrytv/CherryTv.java @@ -69,7 +69,7 @@ public class CherryTv extends AbstractSite { @Override public synchronized boolean login() throws IOException { - return false; + return getHttpClient().login(); } @Override @@ -99,7 +99,7 @@ public class CherryTv extends AbstractSite { @Override public boolean supportsFollow() { - return false; + return true; } @Override @@ -129,7 +129,7 @@ public class CherryTv extends AbstractSite { try (Response response = getHttpClient().execute(req)) { if (response.isSuccessful()) { JSONObject json = new JSONObject(Objects.requireNonNull(response.body()).string()); - LOG.debug(json.toString(2)); + LOG.trace(json.toString(2)); JSONObject data = json.getJSONObject("data"); JSONObject searchResult = data.getJSONObject("searchResult"); JSONArray streamers = searchResult.getJSONArray("streamers"); @@ -159,7 +159,8 @@ public class CherryTv extends AbstractSite { @Override public boolean credentialsAvailable() { - return false; + String username = getConfig().getSettings().cherryTvUsername; + return username != null && !username.trim().isEmpty(); } @Override diff --git a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java index 503bc951..387cf1f9 100644 --- a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java +++ b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvHttpClient.java @@ -64,14 +64,18 @@ public class CherryTvHttpClient extends HttpClient { try (Response response = execute(request)) { if (response.isSuccessful()) { JSONObject resp = new JSONObject(Objects.requireNonNull(response.body()).string()); - LOG.trace(resp.toString(2)); - JSONObject data = resp.getJSONObject("data"); - JSONObject login = data.getJSONObject("login"); - loggedIn = login.optBoolean("success"); - String jwt = login.optString("token"); - saveAsSessionCookie(jwt); - LOG.debug("Login successful"); - return loggedIn; + if (resp.has("data")) { + JSONObject data = resp.getJSONObject("data"); + JSONObject login = data.getJSONObject("login"); + loggedIn = login.optBoolean("success"); + String jwt = login.optString("token"); + saveAsSessionCookie(jwt); + LOG.debug("Login successful"); + return loggedIn; + } else { + LOG.error(resp.toString(2)); + return false; + } } else { throw new HttpException(response.code(), response.message()); } diff --git a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java index f0cf72dc..ddb08ceb 100644 --- a/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java +++ b/common/src/main/java/ctbrec/sites/cherrytv/CherryTvModel.java @@ -9,9 +9,12 @@ import com.squareup.moshi.JsonWriter; import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.NotImplementedExcetion; +import ctbrec.StringUtil; import ctbrec.io.HttpException; import ctbrec.recorder.download.StreamSource; +import okhttp3.MediaType; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; import org.json.JSONException; import org.json.JSONObject; @@ -21,9 +24,6 @@ import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; @@ -55,26 +55,10 @@ public class CherryTvModel extends AbstractModel { .build(); try (Response resp = site.getHttpClient().execute(req)) { String body = Objects.requireNonNull(resp.body()).string(); - Files.write(Paths.get("/tmp/mdl.html"), body.getBytes(StandardCharsets.UTF_8)); Matcher m = NEXT_DATA.matcher(body); if (m.find()) { JSONObject json = new JSONObject(m.group(1)); - JSONObject apolloState = json.getJSONObject("props").getJSONObject("pageProps").getJSONObject("apolloState"); - online = false; - onlineState = OFFLINE; - for (Iterator iter = apolloState.keys(); iter.hasNext();) { - String key = iter.next(); - if (key.startsWith("Broadcast:")) { - JSONObject broadcast = apolloState.getJSONObject(key); - setDisplayName(broadcast.optString("title")); - id = broadcast.getString("id"); - online = broadcast.optString("showStatus").equalsIgnoreCase("Public") - && broadcast.optString("broadcastStatus").equalsIgnoreCase("Live"); - onlineState = online ? ONLINE : OFFLINE; - masterPlaylistUrl = broadcast.optString("pullUrl", null); - break; - } - } + updateModelProperties(json); } else { LOG.error("NEXT_DATA not found in model page {}", getUrl()); return false; @@ -86,6 +70,27 @@ public class CherryTvModel extends AbstractModel { return online; } + private void updateModelProperties(JSONObject json) { + LOG.trace(json.toString(2)); + JSONObject apolloState = json.getJSONObject("props").getJSONObject("pageProps").getJSONObject("apolloState"); + online = false; + onlineState = OFFLINE; + for (Iterator iter = apolloState.keys(); iter.hasNext(); ) { + String key = iter.next(); + if (key.startsWith("Broadcast:")) { + JSONObject broadcast = apolloState.getJSONObject(key); + setDisplayName(broadcast.optString("title")); + online = broadcast.optString("showStatus").equalsIgnoreCase("Public") + && broadcast.optString("broadcastStatus").equalsIgnoreCase("Live"); + onlineState = online ? ONLINE : OFFLINE; + masterPlaylistUrl = broadcast.optString("pullUrl", null); + } else if (key.startsWith("Streamer:")) { + JSONObject streamer = apolloState.getJSONObject(key); + id = streamer.getString("id"); + } + } + } + public void setOnline(boolean online) { this.online = online; } @@ -125,7 +130,7 @@ public class CherryTvModel extends AbstractModel { String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); String segmentUri = baseUrl + playlist.getUri(); src.mediaPlaylistUrl = segmentUri; - if(src.mediaPlaylistUrl.contains("?")) { + if (src.mediaPlaylistUrl.contains("?")) { src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?')); } LOG.trace("Media playlist {}", src.mediaPlaylistUrl); @@ -172,18 +177,18 @@ public class CherryTvModel extends AbstractModel { @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { - if(resolution == null) { - if(failFast) { + if (resolution == null) { + if (failFast) { return new int[2]; } try { - if(!isOnline()) { + if (!isOnline()) { return new int[2]; } List sources = getStreamSources(); Collections.sort(sources); - StreamSource best = sources.get(sources.size()-1); - resolution = new int[] {best.width, best.height}; + StreamSource best = sources.get(sources.size() - 1); + resolution = new int[]{best.width, best.height}; } catch (InterruptedException e) { Thread.currentThread().interrupt(); LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); @@ -192,38 +197,97 @@ public class CherryTvModel extends AbstractModel { LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage()); resolution = new int[2]; } - return resolution; - } else { - return resolution; } + return resolution; } @Override public boolean follow() throws IOException { - // POST https://cherry.tv/graphql - // {"operationName":"follow","variables":{"userId":"1391"},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"a7a8241014074f5c02ac83863a47f7579e5f03167a7ec424ff65ad045c7fcf6f"}}} - return false; + return followUnfollow("follow", "a7a8241014074f5c02ac83863a47f7579e5f03167a7ec424ff65ad045c7fcf6f"); } @Override public boolean unfollow() throws IOException { - return false; + return followUnfollow("unfollow", "e91f8f5a60d33efb2dfb3348b977b78358862d3a5cd5ef0011a6aa6bb65d0bd4"); + } + + private boolean followUnfollow(String action, String persistedQueryHash) throws IOException { + Request request = createFollowUnfollowRequest(action, persistedQueryHash); + LOG.debug("Sending follow request for model {} with ID {}", getName(), getId()); + try (Response response = getSite().getHttpClient().execute(request)) { + if (response.isSuccessful()) { + String responseBody = Objects.requireNonNull(response.body(), "HTTP response body is null").string(); + LOG.debug(responseBody); + JSONObject resp = new JSONObject(responseBody); + if (resp.has("data") && !resp.isNull("data")) { + JSONObject data = resp.getJSONObject("data"); + if (data.has(action + "User")) { + return data.getJSONObject(action + "User").optBoolean("success"); + } + } else if (resp.has("errors")) { + JSONObject first = resp.getJSONArray("errors").getJSONObject(0); + if (first.optString("message").matches("You have .*? the user")) { + return true; + } + } + LOG.debug(resp.toString(2)); + return false; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private Request createFollowUnfollowRequest(String action, String persistedQueryHash) throws IOException { + if (StringUtil.isBlank(id)) { + try { + // if the id is not set yet, we call isOnline(true), where it gets set + isOnline(true); + } catch (ExecutionException e) { + throw new IOException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e); + } + } + + JSONObject body = new JSONObject() + .put("operationName", action) + .put("variables", new JSONObject() + .put("userId", Objects.requireNonNull(id, "Model ID is null")) + ) + .put("query", "mutation " + action + "($userId: ID!) {\n " + action + "User(userId: $userId) {\n success\n __typename\n }\n}\n") + .put("extensions", new JSONObject() + .put("persistedQuery", new JSONObject() + .put("version", 1) + .put("sha256Hash", persistedQueryHash) + ) + ); + + RequestBody requestBody = RequestBody.create(body.toString(), MediaType.parse("application/json")); + return new Request.Builder() + .url(CherryTv.BASE_URL + "/graphql") + .header(REFERER, CherryTv.BASE_URL) + .header(ORIGIN, CherryTv.BASE_URL) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .post(requestBody) + .build(); } public void mapOnlineState(String roomState) { switch (roomState) { - case "private": - case "fullprivate": - setOnlineState(PRIVATE); - break; - case "group": - case "public": - setOnlineState(ONLINE); - setOnline(true); - break; - default: - LOG.debug(roomState); - setOnlineState(OFFLINE); + case "private": + case "fullprivate": + setOnlineState(PRIVATE); + break; + case "group": + case "public": + setOnlineState(ONLINE); + setOnline(true); + break; + default: + LOG.debug(roomState); + setOnlineState(OFFLINE); } } @@ -237,8 +301,10 @@ public class CherryTvModel extends AbstractModel { @Override public void readSiteSpecificData(JsonReader reader) throws IOException { - reader.nextName(); - id = reader.nextString(); + if (reader.hasNext()) { + reader.nextName(); + id = reader.nextString(); + } } @Override