Merge branch 'cherrytv' into dev
This commit is contained in:
commit
cc2875911e
|
@ -23,6 +23,7 @@ import java.util.concurrent.ScheduledExecutorService;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import ctbrec.sites.secretfriends.SecretFriends;
|
||||
import ctbrec.sites.cherrytv.CherryTv;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -175,6 +176,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());
|
||||
|
@ -195,7 +197,7 @@ public class CamrecApplication extends Application {
|
|||
}
|
||||
|
||||
private void initSites() {
|
||||
sites.stream().forEach(site -> {
|
||||
sites.forEach(site -> {
|
||||
try {
|
||||
site.setRecorder(recorder);
|
||||
site.setConfig(config);
|
||||
|
|
|
@ -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;
|
||||
|
@ -21,6 +22,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;
|
||||
|
@ -39,6 +41,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;
|
||||
|
@ -52,7 +55,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);
|
||||
|
@ -78,6 +81,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);
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package ctbrec.ui.sites.cherrytv;
|
||||
|
||||
import ctbrec.Config;
|
||||
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(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().cherryTvUsername)) {
|
||||
Config.getInstance().getSettings().cherryTvUsername = 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(site.getName() + " Password"), 0, row);
|
||||
var password = new PasswordField();
|
||||
password.setText(Config.getInstance().getSettings().cherryTvPassword);
|
||||
password.textProperty().addListener((ob, o, n) -> {
|
||||
if (!n.equals(Config.getInstance().getSettings().cherryTvPassword)) {
|
||||
Config.getInstance().getSettings().cherryTvPassword = 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(site.getAffiliateLink()));
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
|
||||
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();
|
||||
setFilter(true);
|
||||
}
|
||||
|
||||
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 -> {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package ctbrec.ui.sites.cherrytv;
|
||||
|
||||
import ctbrec.sites.cherrytv.CherryTv;
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
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;
|
||||
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(followedTab);
|
||||
return tabs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Tab getFollowedTab() {
|
||||
return followedTab;
|
||||
}
|
||||
|
||||
private Tab createTab(String name, String url) {
|
||||
var updateService = new CherryTvUpdateService(url, site, false);
|
||||
var tab = new ThumbOverviewTab(name, updateService, site);
|
||||
tab.setImageAspectRatio(9.0 / 16.0);
|
||||
tab.preserveAspectRatioProperty().set(false);
|
||||
tab.setRecorder(recorder);
|
||||
return tab;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
package ctbrec.ui.sites.cherrytv;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
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.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 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;
|
||||
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 static final int MODELS_PER_PAGE = 100;
|
||||
|
||||
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);
|
||||
t.setDaemon(true);
|
||||
t.setName("CherryTvUpdateService");
|
||||
return t;
|
||||
});
|
||||
setExecutor(executor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<List<Model>> createTask() {
|
||||
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));
|
||||
LOG.debug("Fetching page {}", pageUrl);
|
||||
|
||||
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()) {
|
||||
String body = Objects.requireNonNull(response.body()).string();
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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");
|
||||
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 setFilter(Predicate<Model> filter) {
|
||||
this.filter = filter;
|
||||
}
|
||||
}
|
|
@ -49,9 +49,9 @@ public class Config {
|
|||
|
||||
private static Config instance;
|
||||
private Settings settings;
|
||||
private String filename;
|
||||
private List<Site> sites;
|
||||
private File configDir;
|
||||
private final String filename;
|
||||
private final List<Site> 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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
@ -144,8 +124,8 @@ 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) {
|
||||
|
@ -156,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);
|
||||
}
|
||||
|
@ -176,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 {
|
||||
|
@ -189,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");
|
||||
}
|
||||
|
@ -253,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());
|
||||
|
@ -326,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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
package ctbrec.sites.cherrytv;
|
||||
|
||||
import ctbrec.Model;
|
||||
import ctbrec.io.HttpClient;
|
||||
import ctbrec.io.HttpException;
|
||||
import ctbrec.sites.AbstractSite;
|
||||
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.net.URLEncoder;
|
||||
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 {
|
||||
|
||||
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 getHttpClient().login();
|
||||
}
|
||||
|
||||
@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 true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSearch() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean searchRequiresLogin() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Model> search(String q) throws IOException, InterruptedException {
|
||||
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(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<Model> result = new LinkedList<>();
|
||||
try (Response response = getHttpClient().execute(req)) {
|
||||
if (response.isSuccessful()) {
|
||||
JSONObject json = new JSONObject(Objects.requireNonNull(response.body()).string());
|
||||
LOG.trace(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.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);
|
||||
}
|
||||
} else {
|
||||
throw new HttpException(response.code(), response.message());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSiteForModel(Model m) {
|
||||
return m instanceof CherryTvModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean credentialsAvailable() {
|
||||
String username = getConfig().getSettings().cherryTvUsername;
|
||||
return username != null && !username.trim().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Model createModelFromUrl(String url) {
|
||||
Matcher m = Pattern.compile("https?://.*?cherry\\.tv/([^/]*?)/?").matcher(url);
|
||||
if (m.matches()) {
|
||||
String modelName = m.group(1);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
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 {
|
||||
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: {}", request.url());
|
||||
try (Response response = execute(request)) {
|
||||
if (response.isSuccessful()) {
|
||||
JSONObject resp = new JSONObject(Objects.requireNonNull(response.body()).string());
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,314 @@
|
|||
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 com.squareup.moshi.JsonReader;
|
||||
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;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
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("<script id=\"__NEXT_DATA__\" type=\"application/json\">(.*?)</script>");
|
||||
|
||||
private boolean online = false;
|
||||
private int[] resolution;
|
||||
private String masterPlaylistUrl;
|
||||
private String id;
|
||||
|
||||
@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();
|
||||
Matcher m = NEXT_DATA.matcher(body);
|
||||
if (m.find()) {
|
||||
JSONObject json = new JSONObject(m.group(1));
|
||||
updateModelProperties(json);
|
||||
} 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@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<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
try {
|
||||
isOnline(true);
|
||||
MasterPlaylist masterPlaylist = getMasterPlaylist();
|
||||
List<StreamSource> 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<StreamSource> 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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean follow() throws IOException {
|
||||
return followUnfollow("follow", "a7a8241014074f5c02ac83863a47f7579e5f03167a7ec424ff65ad045c7fcf6f");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean unfollow() throws IOException {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readSiteSpecificData(JsonReader reader) throws IOException {
|
||||
if (reader.hasNext()) {
|
||||
reader.nextName();
|
||||
id = reader.nextString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
|
||||
writer.name("id").value(id);
|
||||
}
|
||||
}
|
|
@ -1,59 +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 ctbrec.sites.secretfriends.SecretFriends;
|
||||
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;
|
||||
|
@ -71,26 +17,56 @@ 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;
|
||||
import ctbrec.sites.manyvids.MVLive;
|
||||
import ctbrec.sites.mfc.MyFreeCams;
|
||||
import ctbrec.sites.secretfriends.SecretFriends;
|
||||
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<Site> sites = new ArrayList<>();
|
||||
private Server server = new Server();
|
||||
private List<Site> sites = new ArrayList<>();
|
||||
|
||||
public HttpServer() throws Exception {
|
||||
public HttpServer() throws IOException {
|
||||
logEnvironment();
|
||||
createSites();
|
||||
System.setProperty("ctbrec.server.mode", "1");
|
||||
|
@ -157,6 +133,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());
|
||||
|
@ -170,32 +147,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();
|
||||
|
@ -206,7 +180,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);
|
||||
|
@ -246,35 +220,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);
|
||||
|
@ -293,6 +239,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);
|
||||
|
@ -300,6 +250,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
|
||||
|
@ -327,7 +309,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
|
||||
}
|
||||
|
||||
|
@ -345,7 +327,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();
|
||||
|
@ -363,7 +346,7 @@ public class HttpServer {
|
|||
|
||||
ConstraintSecurityHandler csh = new ConstraintSecurityHandler();
|
||||
csh.setAuthenticator(new BasicAuthenticator());
|
||||
csh.setRealmName("myrealm");
|
||||
csh.setRealmName(realm);
|
||||
csh.addConstraintMapping(cm);
|
||||
csh.setLoginService(l);
|
||||
|
||||
|
@ -391,7 +374,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;
|
||||
|
|
|
@ -162,13 +162,14 @@
|
|||
<td data-bind="text: ko_progressString"></td>
|
||||
<td data-bind="text: ko_size"></td>
|
||||
<td>
|
||||
<button class="btn btn-secondary fa fa-play" title="Play recording" data-bind="enable: ko_status() == 'FINISHED', click: play"></button>
|
||||
<button class="btn btn-secondary fa fa-play" title="Play recording" data-bind="click: play"></button>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-secondary fa fa-download" title="Download recording" data-bind="enable: ko_status() == 'FINISHED' && singleFile, click: download"></button>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-secondary fa fa-trash" title="Delete recording" data-bind="enable: (ko_status() == 'FINISHED' || ko_status() == 'WAITING'), click: ctbrec.deleteRecording"></button>
|
||||
<button class="btn btn-secondary fa fa-trash" title="Delete recording" data-bind="enable: (ko_status() == 'FINISHED' || ko_status() == 'WAITING' || ko_status() == 'FAILED'),
|
||||
click: ctbrec.deleteRecording"></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -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 = '';
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue