Implement follow / unfollow
This commit is contained in:
parent
0d47952d3d
commit
18e4a43699
|
@ -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"));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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<String> 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<String> 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<StreamSource> 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
|
||||
|
|
Loading…
Reference in New Issue