Add login, follow and unfollow and FollowedTab for Amateur.TV

This commit is contained in:
0xb00bface 2021-05-29 17:39:05 +02:00
parent 600f1941a5
commit 3c71624f38
12 changed files with 368 additions and 25 deletions

View File

@ -0,0 +1,99 @@
package ctbrec.ui.sites.amateurtv;
import ctbrec.Config;
import ctbrec.sites.amateurtv.AmateurTv;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.settings.SettingsTab;
import ctbrec.ui.sites.AbstractConfigUI;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
public class AmateurTvConfigUI extends AbstractConfigUI {
private AmateurTv site;
public AmateurTvConfigUI(AmateurTv site) {
this.site = site;
}
@Override
public Parent createConfigPanel() {
GridPane layout = SettingsTab.createGridLayout();
var settings = Config.getInstance().getSettings();
var row = 0;
var l = new Label("Active");
layout.add(l, 0, row);
var enabled = new CheckBox();
enabled.setSelected(!settings.disabledSites.contains(site.getName()));
enabled.setOnAction(e -> {
if(enabled.isSelected()) {
settings.disabledSites.remove(site.getName());
} else {
settings.disabledSites.add(site.getName());
}
save();
});
GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
layout.add(enabled, 1, row++);
layout.add(new Label("Amateur.TV User"), 0, row);
var username = new TextField(settings.amateurTvUsername);
username.setPrefWidth(300);
username.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().amateurTvUsername)) {
Config.getInstance().getSettings().amateurTvUsername = 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("Amateur.TV Password"), 0, row);
var password = new PasswordField();
password.setText(settings.amateurTvPassword);
password.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().amateurTvPassword)) {
Config.getInstance().getSettings().amateurTvPassword = password.getText();
site.getHttpClient().logout();
save();
}
});
GridPane.setFillWidth(password, true);
GridPane.setHgrow(password, Priority.ALWAYS);
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, row++);
// layout.add(new Label("Bongacams Base URL"), 0, row);
// var baseUrl = new TextField();
// baseUrl.setText(Config.getInstance().getSettings().bongacamsBaseUrl);
// baseUrl.textProperty().addListener((ob, o, n) -> {
// Config.getInstance().getSettings().bongacamsBaseUrl = baseUrl.getText();
// save();
// });
// GridPane.setFillWidth(baseUrl, true);
// GridPane.setHgrow(baseUrl, Priority.ALWAYS);
// GridPane.setColumnSpan(baseUrl, 2);
// layout.add(baseUrl, 1, row++);
var createAccount = new Button("Create new Account");
createAccount.setOnAction(e -> DesktopIntegration.open(site.getAffiliateLink()));
layout.add(createAccount, 1, row);
GridPane.setColumnSpan(createAccount, 2);
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
// GridPane.setMargin(baseUrl, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
return layout;
}
}

View File

@ -0,0 +1,108 @@
package ctbrec.ui.sites.amateurtv;
import java.io.IOException;
import java.util.Collections;
import java.util.Objects;
import java.util.function.Consumer;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.sites.amateurtv.AmateurTv;
import ctbrec.ui.ExternalBrowser;
import okhttp3.Cookie;
import okhttp3.Cookie.Builder;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
public class AmateurTvElectronLoginDialog {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTvElectronLoginDialog.class);
public static final String DOMAIN = "amateur.tv";
public static final String URL = AmateurTv.baseUrl;
private CookieJar cookieJar;
private ExternalBrowser browser;
public AmateurTvElectronLoginDialog(CookieJar cookieJar) throws IOException {
this.cookieJar = cookieJar;
browser = ExternalBrowser.getInstance();
try {
var config = new JSONObject();
config.put("url", URL);
config.put("w", 640);
config.put("h", 480);
var msg = new JSONObject();
msg.put("config", config);
browser.run(msg, msgHandler);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Couldn't wait for login dialog", e);
} finally {
browser.close();
}
}
private Consumer<String> msgHandler = line -> {
if (!line.startsWith("{")) {
LOG.error("Didn't received a JSON object {}", line);
} else {
var json = new JSONObject(line);
try {
browser.executeJavaScript("let loginDialogVisible = document.querySelectorAll('div[class~=\"MuiDialog-container\"]').length > 1");
browser.executeJavaScript("if (!loginDialogVisible) { document.querySelector('button').innerHTML.indexOf('I agree') >= 0 && document.querySelector('button').click(); }");
browser.executeJavaScript("if (!loginDialogVisible) { document.querySelector('button[aria-label=\"open drawer\"]').click(); }"); // open the burger menu to get to the login button
browser.executeJavaScript("if (!loginDialogVisible) { document.querySelectorAll('button').forEach(function(b) { if (b.textContent === 'Log in') b.click(); }); }"); // click the login button to open the login dialog
browser.executeJavaScript("loginDialogVisible = document.querySelectorAll('div[class~=\"MuiDialog-container\"]').length > 1");
browser.executeJavaScript("if (loginDialogVisible) throw new Error(\"Stop execution right here\")");
// String username = Config.getInstance().getSettings().amateurTvUsername;
// String password = Config.getInstance().getSettings().amateurTvPassword;
// browser.executeJavaScript("if (loginDialogVisible) { document.querySelectorAll('div[class~=\"MuiDialog-container\"] input').item(0).value = '" + username + "' }"); // enter username
// browser.executeJavaScript("if (loginDialogVisible) { document.querySelectorAll('div[class~=\"MuiDialog-container\"] input').item(1).value = '" + password + "' }"); // enter password
//browser.executeJavaScript("console.log('submit')");
// browser.executeJavaScript("if(loginDialogVisible) { document.querySelector('div[class~=\"MuiDialog-container\"] button[type=\"submit\"]').click() }"); // click the submit button
} catch(Exception e) {
LOG.warn("Couldn't auto fill username and password for Amateur.TV", e);
}
var loginSuccessful = false;
if (json.has("cookies")) {
var cookiesFromBrowser = json.getJSONArray("cookies");
for (var i = 0; i < cookiesFromBrowser.length(); i++) {
var cookie = cookiesFromBrowser.getJSONObject(i);
if (cookie.getString("domain").contains(DOMAIN)) {
Builder b = new Cookie.Builder()
.path(cookie.getString("path"))
.domain(DOMAIN)
.name(cookie.getString("name"))
.value(cookie.getString("value"))
.expiresAt((long) cookie.optDouble("expirationDate") * 1000l);
if (cookie.optBoolean("hostOnly")) {
b.hostOnlyDomain(DOMAIN);
}
if (cookie.optBoolean("httpOnly")) {
b.httpOnly();
}
if (cookie.optBoolean("secure")) {
b.secure();
}
Cookie c = b.build();
cookieJar.saveFromResponse(HttpUrl.parse(AmateurTv.baseUrl), Collections.singletonList(c));
LOG.debug("{}={}", c.name(), c.value());
if (Objects.equals(c.name(), "userType") && Objects.equals(c.value(), "registered")) {
loginSuccessful = true;
}
}
}
}
if (loginSuccessful) {
try {
browser.close();
} catch (IOException e) {
LOG.error("Couldn't send shutdown request to external browser", e);
}
}
}
};
}

View File

@ -0,0 +1,13 @@
package ctbrec.ui.sites.amateurtv;
import ctbrec.sites.Site;
import ctbrec.ui.tabs.FollowedTab;
import ctbrec.ui.tabs.PaginatedScheduledService;
import ctbrec.ui.tabs.ThumbOverviewTab;
public class AmateurTvFollowedTab extends ThumbOverviewTab implements FollowedTab {
public AmateurTvFollowedTab(String title, PaginatedScheduledService updateService, Site site) {
super(title, updateService, site);
}
}

View File

@ -2,19 +2,28 @@ package ctbrec.ui.sites.amateurtv;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.sites.amateurtv.AmateurTv;
import ctbrec.sites.amateurtv.AmateurTvHttpClient;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.sites.AbstractSiteUi;
import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabProvider;
public class AmateurTvSiteUi extends AbstractSiteUi {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTvSiteUi.class);
private final AmateurTvTabProvider tabProvider;
private final AmateurTv amateurTv;
private final AmateurTv site;
private final AmateurTvConfigUI configUi;
public AmateurTvSiteUi(AmateurTv amateurTv) {
this.amateurTv = amateurTv;
this.site = amateurTv;
tabProvider = new AmateurTvTabProvider(amateurTv);
configUi = new AmateurTvConfigUI(amateurTv);
}
@Override
@ -24,11 +33,29 @@ public class AmateurTvSiteUi extends AbstractSiteUi {
@Override
public ConfigUI getConfigUI() {
return null;
return configUi;
}
@Override
public synchronized boolean login() throws IOException {
return amateurTv.login();
if (!site.credentialsAvailable()) {
return false;
}
boolean automaticLogin = site.login();
if (automaticLogin) {
return true;
} else {
// login with external browser window
try {
new AmateurTvElectronLoginDialog(site.getHttpClient().getCookieJar());
} catch (Exception e1) {
LOG.error("Error logging in with external browser", e1);
Dialogs.showError("Login error", "Couldn't login to " + site.getName(), e1);
}
AmateurTvHttpClient httpClient = (AmateurTvHttpClient) site.getHttpClient();
return httpClient.checkLoginSuccess();
}
}
}

View File

@ -15,6 +15,7 @@ public class AmateurTvTabProvider implements TabProvider {
private AmateurTv amateurTv;
private Recorder recorder;
private AmateurTvFollowedTab followedTab;
public AmateurTvTabProvider(AmateurTv amateurTv) {
this.amateurTv = amateurTv;
@ -50,6 +51,14 @@ public class AmateurTvTabProvider implements TabProvider {
updateService = new AmateurTvUpdateService(amateurTv, url);
tabs.add(createTab("Trans", updateService));
// followed
url = AmateurTv.baseUrl + "/v3/readmodel/cache/cams/F";
updateService = new AmateurTvUpdateService(amateurTv, url);
updateService.requiresLogin(true);
followedTab = new AmateurTvFollowedTab("Followed", updateService, amateurTv);
followedTab.setRecorder(recorder);
tabs.add(followedTab);
return tabs;
}
@ -61,7 +70,7 @@ public class AmateurTvTabProvider implements TabProvider {
@Override
public Tab getFollowedTab() {
return null;
return followedTab;
}
}

View File

@ -16,6 +16,7 @@ import ctbrec.Config;
import ctbrec.Model;
import ctbrec.sites.amateurtv.AmateurTv;
import ctbrec.sites.amateurtv.AmateurTvModel;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.tabs.PaginatedScheduledService;
import javafx.concurrent.Task;
import okhttp3.Request;
@ -27,6 +28,7 @@ public class AmateurTvUpdateService extends PaginatedScheduledService {
private AmateurTv site;
private String url;
private boolean requiresLogin = false;
public AmateurTvUpdateService(AmateurTv site, String url) {
this.site = site;
@ -38,26 +40,29 @@ public class AmateurTvUpdateService extends PaginatedScheduledService {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException {
if (requiresLogin) {
SiteUiFactory.getUi(site).login();
}
return loadModelList();
}
};
}
private List<Model> loadModelList() throws IOException {
int offset = page - 1;
String pageUrl = url + '/' + offset * ITEMS_PER_PAGE + '/' + ITEMS_PER_PAGE + "/es";
String pageUrl = url + '/' + offset * ITEMS_PER_PAGE + '/' + ITEMS_PER_PAGE + "/en";
LOG.debug("Fetching page {}", pageUrl);
var request = new Request.Builder()
.url(pageUrl)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT, Locale.ENGLISH.getLanguage())
.header(REFERER, site.getBaseUrl())
.header(REFERER, site.getBaseUrl() + "/following")
.build();
try (var response = site.getHttpClient().execute(request)) {
if (response.isSuccessful()) {
var content = response.body().string();
LOG.debug(content);
List<Model> models = new ArrayList<>();
var json = new JSONObject(content);
var modelNodes = json.getJSONObject("cams").getJSONArray("nodes");
@ -81,4 +86,8 @@ public class AmateurTvUpdateService extends PaginatedScheduledService {
models.add(model);
}
}
public void requiresLogin(boolean requiresLogin) {
this.requiresLogin = requiresLogin;
}
}

View File

@ -43,6 +43,8 @@ public class Settings {
TIME_OR_SIZE
}
public String amateurTvUsername = "";
public String amateurTvPassword = "";
public String bongacamsBaseUrl = "https://bongacams.com";
public String bongaPassword = "";
public String bongaUsername = "";

View File

@ -25,6 +25,7 @@ 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;
@ -217,7 +218,6 @@ public abstract class HttpClient {
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void loadCookies() {
try {
File cookieFile = new File(Config.getInstance().getConfigDir(), "cookies-" + name + ".json");
@ -233,10 +233,12 @@ public abstract class HttpClient {
.build();
JsonAdapter<CookieContainer> adapter = moshi.adapter(CookieContainer.class).indent(" ");
CookieContainer fromJson = adapter.fromJson(json);
Set entries = fromJson.entrySet();
for (Object _entry : entries) {
Entry entry = (Entry) _entry;
cookies.put((String)entry.getKey(), (List<Cookie>)entry.getValue());
Set<Entry<String, List<Cookie>>> entries = fromJson.entrySet();
for (Entry<String, List<Cookie>> entry : entries) {
List<Cookie> filteredCookies = entry.getValue().stream()
.filter(c -> !Objects.equals("deleted", c.value()))
.collect(Collectors.toList());
cookies.put(entry.getKey(), filteredCookies);
}
} catch (Exception e) {

View File

@ -13,6 +13,7 @@ public class HttpConstants {
public static final String COOKIE = "Cookie";
public static final String KEEP_ALIVE = "keep-alive";
public static final String MIMETYPE_APPLICATION_JSON = "application/json";
public static final String MIMETYPE_TEXT_HTML = "text/html";
public static final String NO_CACHE = "no-cache";
public static final String ORIGIN = "Origin";
public static final String PRAGMA = "Pragma";

View File

@ -6,13 +6,14 @@ import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.io.HttpClient;
import ctbrec.sites.AbstractSite;
public class AmateurTv extends AbstractSite {
public static String baseUrl = "https://www.amateur.tv";
public static String baseUrl = "https://en.amateur.tv";
private AmateurTvHttpClient httpClient;
@ -79,7 +80,7 @@ public class AmateurTv extends AbstractSite {
@Override
public boolean supportsFollow() {
return false;
return true;
}
@Override
@ -104,9 +105,8 @@ public class AmateurTv extends AbstractSite {
@Override
public boolean credentialsAvailable() {
//String username = Config.getInstance().getSettings().bongaUsername;
//return username != null && !username.trim().isEmpty();
return false;
String username = Config.getInstance().getSettings().amateurTvUsername;
return username != null && !username.trim().isEmpty();
}
@Override

View File

@ -1,22 +1,50 @@
package ctbrec.sites.amateurtv;
import static ctbrec.io.HttpConstants.*;
import java.io.IOException;
import java.util.Locale;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.json.JSONObject;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import okhttp3.Request;
import okhttp3.Response;
public class AmateurTvHttpClient extends HttpClient {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTvHttpClient.class);
public AmateurTvHttpClient() {
super("amateurtv");
}
@Override
public boolean login() throws IOException {
return false;
return checkLoginSuccess();
}
/**
* Check, if the login worked by requesting the user profile
*
* @throws IOException
*/
public boolean checkLoginSuccess() throws IOException {
String url = AmateurTv.baseUrl + "/v3/readmodel/user/me";
Request request = new Request.Builder()
.url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(REFERER, AmateurTv.baseUrl)
.build();
try (Response response = execute(request)) {
if (response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
return json.has("username") && !json.getString("username").equalsIgnoreCase("guest");
} else {
throw new HttpException(response.code(), response.message());
}
}
}
}

View File

@ -7,12 +7,16 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import javax.xml.bind.JAXBException;
import org.json.JSONObject;
import org.jsoup.nodes.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
@ -30,11 +34,15 @@ import ctbrec.Config;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class AmateurTvModel extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTvModel.class);
private boolean online = false;
@Override
@ -127,12 +135,49 @@ public class AmateurTvModel extends AbstractModel {
@Override
public boolean follow() throws IOException {
return false;
String url = getSite().getBaseUrl() + "/v3/user/follow";
return followUnfollow(url);
}
@Override
public boolean unfollow() throws IOException {
return false;
String url = getSite().getBaseUrl() + "/v3/user/unfollow";
return followUnfollow(url);
}
private boolean followUnfollow(String url) throws IOException {
if(!getSite().login()) {
throw new IOException("Not logged in");
}
LOG.debug("Calling {}", url);
RequestBody body = new FormBody.Builder()
.add("username", getName())
.build();
Request req = new Request.Builder()
.url(url)
.method("POST", body)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(REFERER, getUrl())
.header(ORIGIN, getSite().getBaseUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
try (Response resp = site.getHttpClient().execute(req)) {
if (resp.isSuccessful()) {
String msg = resp.body().string();
JSONObject json = new JSONObject(msg);
if (Objects.equals(json.getString("result"), "OK")) {
LOG.debug("Follow/Unfollow -> {}", msg);
return true;
} else {
LOG.debug(msg);
throw new IOException("Response was " + msg);
}
} else {
throw new HttpException(resp.code(), resp.message());
}
}
}
private JSONObject getModelInfo() throws IOException {