Implement login and followed tab for cherry.tv

This commit is contained in:
0xb00bface 2021-11-07 13:35:28 +01:00
parent c4c5818496
commit 36cacda106
11 changed files with 325 additions and 31 deletions

View File

@ -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);

View File

@ -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);
}
});
}
}

View File

@ -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<Model> 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<Model> 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;
}
}

View File

@ -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;

View File

@ -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<Tab> getTabs(Scene scene) {
List<Tab> 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);

View File

@ -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<Model> 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<Model> 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<Model> 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<Model> parseModels(String body) {
protected List<Model> 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<Model> 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<Model> filter) {
this.filter = filter;
}
}

View File

@ -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;
}
}

View File

@ -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";

View File

@ -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) {

View File

@ -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;
}
}
}

View File

@ -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);
}
}