Remove non-working sites

This commit is contained in:
Jafea7 2025-04-07 12:54:39 +10:00
parent c91b410307
commit 7bee404ec1
68 changed files with 9 additions and 6640 deletions

View File

@ -24,19 +24,14 @@ import ctbrec.recorder.Recorder;
import ctbrec.recorder.RemoteRecorder;
import ctbrec.recorder.SimplifiedLocalRecorder;
import ctbrec.sites.Site;
import ctbrec.sites.amateurtv.AmateurTv;
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.dreamcam.Dreamcam;
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.streamray.Streamray;
@ -184,19 +179,14 @@ public class CamrecApplication extends Application {
}
private void createSites() {
sites.add(new AmateurTv());
sites.add(new BongaCams());
sites.add(new Cam4());
sites.add(new Camsoda());
sites.add(new Chaturbate());
sites.add(new CherryTv());
sites.add(new Dreamcam());
sites.add(new Fc2Live());
sites.add(new Flirt4Free());
sites.add(new LiveJasmin());
sites.add(new MVLive());
sites.add(new MyFreeCams());
sites.add(new SecretFriends());
sites.add(new Showup());
sites.add(new Streamate());
sites.add(new Stripchat());

View File

@ -1,38 +1,28 @@
package ctbrec.ui;
import ctbrec.sites.Site;
import ctbrec.sites.amateurtv.AmateurTv;
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.dreamcam.Dreamcam;
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.streamray.Streamray;
import ctbrec.sites.stripchat.Stripchat;
import ctbrec.sites.winktv.WinkTv;
import ctbrec.sites.xlovecam.XloveCam;
import ctbrec.ui.sites.amateurtv.AmateurTvSiteUi;
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.dreamcam.DreamcamSiteUi;
import ctbrec.ui.sites.fc2live.Fc2LiveSiteUi;
import ctbrec.ui.sites.flirt4free.Flirt4FreeSiteUi;
import ctbrec.ui.sites.jasmin.LiveJasminSiteUi;
import ctbrec.ui.sites.manyvids.MVLiveSiteUi;
import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi;
import ctbrec.ui.sites.secretfriends.SecretFriendsSiteUi;
import ctbrec.ui.sites.showup.ShowupSiteUi;
import ctbrec.ui.sites.streamate.StreamateSiteUi;
import ctbrec.ui.sites.streamray.StreamraySiteUi;
@ -42,18 +32,13 @@ import ctbrec.ui.sites.xlovecam.XloveCamSiteUi;
public class SiteUiFactory {
private static AmateurTvSiteUi amateurTvUi;
private static BongaCamsSiteUi bongaSiteUi;
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;
private static MVLiveSiteUi mvLiveSiteUi;
private static MyFreeCamsSiteUi mfcSiteUi;
private static SecretFriendsSiteUi secretFriendsSiteUi;
private static ShowupSiteUi showupSiteUi;
private static StreamateSiteUi streamateSiteUi;
private static StripchatSiteUi stripchatSiteUi;
@ -66,12 +51,7 @@ public class SiteUiFactory {
}
public static synchronized SiteUI getUi(Site site) { // NOSONAR
if (site instanceof AmateurTv) {
if (amateurTvUi == null) {
amateurTvUi = new AmateurTvSiteUi((AmateurTv) site);
}
return amateurTvUi;
} else if (site instanceof BongaCams) {
if (site instanceof BongaCams) {
if (bongaSiteUi == null) {
bongaSiteUi = new BongaCamsSiteUi((BongaCams) site);
}
@ -91,11 +71,6 @@ 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 Dreamcam) {
if (dreamcamSiteUi == null) {
dreamcamSiteUi = new DreamcamSiteUi((Dreamcam) site);
@ -111,21 +86,11 @@ public class SiteUiFactory {
flirt4FreeSiteUi = new Flirt4FreeSiteUi((Flirt4Free) site);
}
return flirt4FreeSiteUi;
} else if (site instanceof MVLive) {
if (mvLiveSiteUi == null) {
mvLiveSiteUi = new MVLiveSiteUi((MVLive) site);
}
return mvLiveSiteUi;
} else if (site instanceof MyFreeCams) {
if (mfcSiteUi == null) {
mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site);
}
return mfcSiteUi;
} else if (site instanceof SecretFriends) {
if (secretFriendsSiteUi == null) {
secretFriendsSiteUi = new SecretFriendsSiteUi((SecretFriends) site);
}
return secretFriendsSiteUi;
} else if (site instanceof Showup) {
if (showupSiteUi == null) {
showupSiteUi = new ShowupSiteUi((Showup) site);
@ -136,11 +101,6 @@ public class SiteUiFactory {
streamateSiteUi = new StreamateSiteUi((Streamate) site);
}
return streamateSiteUi;
} else if (site instanceof LiveJasmin) {
if (jasminSiteUi == null) {
jasminSiteUi = new LiveJasminSiteUi((LiveJasmin) site);
}
return jasminSiteUi;
} else if (site instanceof Stripchat) {
if (stripchatSiteUi == null) {
stripchatSiteUi = new StripchatSiteUi((Stripchat) site);

View File

@ -1,93 +0,0 @@
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++);
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;
}
}

View File

@ -1,126 +0,0 @@
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.BASE_URL;
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
.onReady(this::onReady)
.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);
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.BASE_URL), 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();
return;
} catch (IOException e) {
LOG.error("Couldn't send shutdown request to external browser", e);
}
}
try {
browser.executeJavaScript("document.querySelector('div[class~=\"cy_ubCoins\"]') != null")
.thenAccept(b -> {
LOG.debug("Result: {}", b);
if (Boolean.TRUE.equals(b)) {
try {
browser.close();
} catch (IOException e) {
e.printStackTrace();
}
}
})
.exceptionally(ex -> {LOG.error("Error", ex); return null;});
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\")");
} catch(Exception e) {
LOG.warn("Couldn't auto fill username and password for Amateur.TV", e);
}
}
};
private void onReady() {
try {
browser.executeJavaScript("let loginDialogVisible = document.querySelectorAll('div[class~=\"MuiDialog-container\"]').length > 1");
} catch(Exception e) {
LOG.warn("Couldn't auto fill username and password for Amateur.TV", e);
}
}
}

View File

@ -1,13 +0,0 @@
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

@ -1,65 +0,0 @@
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 AmateurTv site;
private AmateurTvTabProvider tabProvider;
private AmateurTvConfigUI configUi;
public AmateurTvSiteUi(AmateurTv amateurTv) {
this.site = amateurTv;
}
@Override
public TabProvider getTabProvider() {
if (tabProvider == null) {
tabProvider = new AmateurTvTabProvider(site);
}
return tabProvider;
}
@Override
public ConfigUI getConfigUI() {
if (configUi == null) {
configUi = new AmateurTvConfigUI(site);
}
return configUi;
}
@Override
public synchronized boolean login() throws IOException {
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

@ -1,73 +0,0 @@
package ctbrec.ui.sites.amateurtv;
import ctbrec.sites.amateurtv.AmateurTv;
import ctbrec.ui.sites.AbstractTabProvider;
import ctbrec.ui.tabs.PaginatedScheduledService;
import ctbrec.ui.tabs.ThumbOverviewTab;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
import java.util.ArrayList;
import java.util.List;
public class AmateurTvTabProvider extends AbstractTabProvider {
private AmateurTvFollowedTab followedTab;
public AmateurTvTabProvider(AmateurTv amateurTv) {
super(amateurTv);
}
@Override
protected List<Tab> getSiteTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>();
// all
var url = AmateurTv.BASE_URL + "/v3/readmodel/cache/onlinecamlist";
var updateService = new AmateurTvUpdateService((AmateurTv) site, url);
tabs.add(createTab("All", updateService));
// female
url = AmateurTv.BASE_URL + "/v3/readmodel/cache/sectioncamlist?genre=[%22w%22]";
updateService = new AmateurTvUpdateService((AmateurTv) site, url);
tabs.add(createTab("Female", updateService));
// male
url = AmateurTv.BASE_URL + "/v3/readmodel/cache/sectioncamlist?genre=[%22m%22]";
updateService = new AmateurTvUpdateService((AmateurTv) site, url);
tabs.add(createTab("Male", updateService));
// couples
url = AmateurTv.BASE_URL + "/v3/readmodel/cache/sectioncamlist?genre=[%22c%22]";
updateService = new AmateurTvUpdateService((AmateurTv) site, url);
tabs.add(createTab("Couples", updateService));
// trans
url = AmateurTv.BASE_URL + "/v3/readmodel/cache/sectioncamlist?genre=[%22t%22]";
updateService = new AmateurTvUpdateService((AmateurTv) site, url);
tabs.add(createTab("Trans", updateService));
// followed
url = AmateurTv.BASE_URL + "/v3/readmodel/cache/favorites";
updateService = new AmateurTvUpdateService((AmateurTv) site, url);
updateService.requiresLogin(true);
followedTab = new AmateurTvFollowedTab("Followed", updateService, site);
followedTab.setRecorder(recorder);
tabs.add(followedTab);
return tabs;
}
private Tab createTab(String title, PaginatedScheduledService updateService) {
var tab = new ThumbOverviewTab(title, updateService, site);
tab.setRecorder(recorder);
return tab;
}
@Override
public Tab getFollowedTab() {
return followedTab;
}
}

View File

@ -1,132 +0,0 @@
package ctbrec.ui.sites.amateurtv;
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;
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.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import static ctbrec.io.HttpConstants.*;
public class AmateurTvUpdateService extends PaginatedScheduledService {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTvUpdateService.class);
private static final int ITEMS_PER_PAGE = 48;
private AmateurTv site;
private String url;
private boolean requiresLogin = false;
private List<Model> modelsList;
private Instant lastListInfoRequest = Instant.EPOCH;
public AmateurTvUpdateService(AmateurTv site, String url) {
this.site = site;
this.url = url;
}
@Override
protected Task<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException {
if (requiresLogin) {
if (!SiteUiFactory.getUi(site).login()) {
throw new IOException("- Login is required");
}
;
}
return getModelList().stream()
.skip((page - 1) * (long) ITEMS_PER_PAGE)
.limit(ITEMS_PER_PAGE)
.collect(Collectors.toList()); // NOSONAR
}
};
}
private List<Model> getModelList() throws IOException {
if (Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 30) {
return modelsList;
}
lastListInfoRequest = Instant.now();
modelsList = loadModelList();
if (modelsList == null) {
modelsList = Collections.emptyList();
}
return modelsList;
}
private List<Model> loadModelList() throws IOException {
LOG.debug("Fetching page {}", url);
Request request = new Request.Builder()
.url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT, Locale.ENGLISH.getLanguage())
.header(REFERER, site.getBaseUrl() + "/following")
.build();
try (Response response = site.getHttpClient().execute(request)) {
if (response.isSuccessful()) {
String content = response.body().string();
List<Model> models = new ArrayList<>();
JSONObject json = new JSONObject(content);
if (json.has("body")) {
JSONObject body = json.getJSONObject("body");
if (body.has("cams")) {
JSONArray cams = body.getJSONArray("cams");
parseModels(cams, models);
}
if (body.has("list") && body.has("total")) {
if (body.optInt("total") > 0) {
JSONArray list = body.getJSONArray("list");
parseModels(list, models);
}
}
}
if (json.has("cams")) {
JSONArray cams = json.getJSONArray("cams");
parseModels(cams, models);
}
return models;
} else {
int code = response.code();
throw new IOException("HTTP status " + code);
}
}
}
private void parseModels(JSONArray jsonModels, List<Model> models) {
for (var i = 0; i < jsonModels.length(); i++) {
JSONObject m = jsonModels.getJSONObject(i);
String name = m.optString("username");
AmateurTvModel model = (AmateurTvModel) site.createModel(name);
if (m.optBoolean("capturesEnabled", true) && m.has("capture")) {
model.setPreview(m.optString("capture"));
} else {
model.setPreview(site.getBaseUrl() + m.optString("avatar"));
}
model.setDescription(m.optString("topic"));
models.add(model);
}
}
public void requiresLogin(boolean requiresLogin) {
this.requiresLogin = requiresLogin;
}
}

View File

@ -1,88 +0,0 @@
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;
}
}

View File

@ -1,90 +0,0 @@
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);
}
});
}
}

View File

@ -1,55 +0,0 @@
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("following", site, true);
url = "https://api.cherry.tv/graphql?operationName=findFollowingBroadcastsByPage&variables={\"limit\":1000}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"241ae6ae3c2bd62e78432b4d51a92a1baa59d9e94d173867a2a45586704465d1\"}}";
}
@Override
protected List<Model> parseModels(String body) throws IOException {
var json = new JSONObject(body);
//LOG.debug(json.toString(2));
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("broadcasts").getJSONArray("broadcasts");
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("imageUrl"));
var online = following.optString("broadcastStatus").equalsIgnoreCase("Live");
model.setOnline(online);
model.setOnlineState(online ? ONLINE : OFFLINE);
models.add(model);
}
} catch (JSONException e) {
LOG.error("Couldn't parse JSON, the structure might have changed", e);
}
return models;
}
}

View File

@ -1,43 +0,0 @@
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();
}
}

View File

@ -1,50 +0,0 @@
package ctbrec.ui.sites.cherrytv;
import ctbrec.sites.cherrytv.CherryTv;
import ctbrec.ui.sites.AbstractTabProvider;
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 extends AbstractTabProvider {
private final CherryTvFollowedTab followedTab;
public CherryTvTabProvider(CherryTv cherryTv) {
super(cherryTv);
followedTab = new CherryTvFollowedTab("Following", (CherryTv) site);
followedTab.setImageAspectRatio(1);
followedTab.preserveAspectRatioProperty().set(false);
followedTab.setRecorder(recorder);
}
@Override
protected List<Tab> getSiteTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>();
tabs.add(createTab("Female", "girls"));
tabs.add(createTab("Trans", "trans"));
tabs.add(createTab("Group Show", "groupshow"));
tabs.add(followedTab);
return tabs;
}
@Override
public Tab getFollowedTab() {
return followedTab;
}
private Tab createTab(String name, String url) {
var updateService = new CherryTvUpdateService(url, (CherryTv) site, false);
var tab = new ThumbOverviewTab(name, updateService, site);
tab.setImageAspectRatio(9.0 / 16.0);
tab.preserveAspectRatioProperty().set(false);
tab.setRecorder(recorder);
return tab;
}
}

View File

@ -1,151 +0,0 @@
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.net.URLEncoder;
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;
import static java.nio.charset.StandardCharsets.UTF_8;
public class CherryTvUpdateService extends PaginatedScheduledService {
private static final Logger LOG = LoggerFactory.getLogger(CherryTvUpdateService.class);
protected static final long MODELS_PER_PAGE = 50;
protected String url;
private final boolean loginRequired;
protected final CherryTv site;
private Predicate<Model> filter;
public CherryTvUpdateService(String slug, CherryTv site, boolean loginRequired) {
this.site = site;
this.url = "https://api.cherry.tv/graphql?query=" + URLEncoder.encode(BROADCASTS_QUERY
.replace(" ", "")
.replace("${slug}", slug), UTF_8);
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");
}
LOG.debug("Fetching page {}", url);
var request = new Request.Builder()
.url(url)
.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.skip((page - 1) * MODELS_PER_PAGE)
.limit(MODELS_PER_PAGE)
.collect(Collectors.toList());
} else {
LOG.debug(Objects.requireNonNull(response.body()).string());
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("broadcastsPaged").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;
}
private static final String BROADCASTS_QUERY = """
{
broadcastsPaged(query: {limit:1000,slug:"${slug}"}) {
broadcasts {
id
title
username
description
thumbnailUrl
tags
broadcastStatus
showStatus
}
totalCount
}
}
""";
}

View File

@ -1,107 +0,0 @@
package ctbrec.ui.sites.jasmin;
import ctbrec.Config;
import ctbrec.sites.jasmin.LiveJasmin;
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 LiveJasminConfigUi extends AbstractConfigUI {
private LiveJasmin liveJasmin;
public LiveJasminConfigUi(LiveJasmin liveJasmin) {
this.liveJasmin = liveJasmin;
}
@Override
public Parent createConfigPanel() {
var settings = Config.getInstance().getSettings();
var layout = SettingsTab.createGridLayout();
var row = 0;
var l = new Label("Active");
layout.add(l, 0, row);
var enabled = new CheckBox();
enabled.setSelected(!settings.disabledSites.contains(liveJasmin.getName()));
enabled.setOnAction(e -> {
if(enabled.isSelected()) {
settings.disabledSites.remove(liveJasmin.getName());
} else {
settings.disabledSites.add(liveJasmin.getName());
}
save();
});
GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
layout.add(enabled, 1, row++);
layout.add(new Label("LiveJasmin User"), 0, row);
var username = new TextField(Config.getInstance().getSettings().livejasminUsername);
username.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().livejasminUsername)) {
Config.getInstance().getSettings().livejasminUsername = n;
liveJasmin.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("LiveJasmin Password"), 0, row);
var password = new PasswordField();
password.setText(Config.getInstance().getSettings().livejasminPassword);
password.textProperty().addListener((ob, o, n) -> {
if(!n.equals(Config.getInstance().getSettings().livejasminPassword)) {
Config.getInstance().getSettings().livejasminPassword = n;
liveJasmin.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("LiveJasmin Base URL"), 0, row);
var baseUrl = new TextField();
baseUrl.setText(Config.getInstance().getSettings().livejasminBaseUrl);
baseUrl.textProperty().addListener((ob, o, n) -> {
Config.getInstance().getSettings().livejasminBaseUrl = 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(liveJasmin.getAffiliateLink()));
layout.add(createAccount, 1, row++);
GridPane.setColumnSpan(createAccount, 2);
var deleteCookies = new Button("Delete Cookies");
deleteCookies.setOnAction(e -> liveJasmin.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(baseUrl, 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));
username.setPrefWidth(300);
return layout;
}
}

View File

@ -1,98 +0,0 @@
package ctbrec.ui.sites.jasmin;
import java.io.IOException;
import java.util.Collections;
import java.util.function.Consumer;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.ui.ExternalBrowser;
import okhttp3.Cookie;
import okhttp3.Cookie.Builder;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
public class LiveJasminElectronLoginDialog {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminElectronLoginDialog.class);
public static final String URL = LiveJasmin.baseUrl + "/en/auth/login";
private CookieJar cookieJar;
private ExternalBrowser browser;
public LiveJasminElectronLoginDialog(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", 720);
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);
} catch (IOException e) {
LOG.debug("Error while starting the browser or communication to it", e);
} finally {
browser.close();
}
}
private Consumer<String> msgHandler = line -> {
if(!line.startsWith("{")) {
System.err.println(line); // NOSONAR
} else {
var json = new JSONObject(line);
if(json.has("url")) {
var url = json.getString("url");
if(url.endsWith("/auth/login")) {
try {
String username = Config.getInstance().getSettings().livejasminUsername;
if (username != null && !username.trim().isEmpty()) {
browser.executeJavaScript("document.querySelector('#login_form input[name=\"username\"]').value = '" + username + "';");
}
String password = Config.getInstance().getSettings().livejasminPassword;
if (password != null && !password.trim().isEmpty()) {
password = password.replace("'", "\\'");
browser.executeJavaScript("document.querySelector('#login_form input[name=\"password\"]').value = '" + password + "';");
}
browser.executeJavaScript("document.getElementById('header_container').setAttribute('style', 'display:none');");
browser.executeJavaScript("document.getElementById('footer').setAttribute('style', 'display:none');");
browser.executeJavaScript("document.getElementById('react-container').setAttribute('style', 'display:none');");
browser.executeJavaScript("document.getElementById('inner_container').setAttribute('style', 'padding: 0; margin: 1em');");
browser.executeJavaScript("document.querySelector('div[class~=\"content_box\"]').setAttribute('style', 'margin: 1em');");
} catch(Exception e) {
LOG.warn("Couldn't auto fill username and password", e);
}
}
if(json.has("cookies")) {
var cookiesFromBrowser = json.getJSONArray("cookies");
for (var i = 0; i < cookiesFromBrowser.length(); i++) {
var cookie = cookiesFromBrowser.getJSONObject(i);
Builder b = new Cookie.Builder()
.path("/")
.domain(LiveJasmin.baseDomain)
.name(cookie.getString("name"))
.value(cookie.getString("value"))
.expiresAt(0);
Cookie c = b.build();
cookieJar.saveFromResponse(HttpUrl.parse(LiveJasmin.baseUrl), Collections.singletonList(c));
}
}
if(url.contains("/member/")) {
try {
browser.close();
} catch(IOException e) {
LOG.error("Couldn't send close request to browser", e);
}
}
}
}
};
}

View File

@ -1,51 +0,0 @@
package ctbrec.ui.sites.jasmin;
import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.ui.tabs.FollowedTab;
import javafx.geometry.Insets;
import javafx.scene.Scene;
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 LiveJasminFollowedTab extends LiveJasminTab implements FollowedTab {
public LiveJasminFollowedTab(LiveJasmin liveJasmin) {
super("Followed", new LiveJasminFollowedUpdateService(liveJasmin), liveJasmin);
}
@Override
public void setScene(Scene scene) {
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (this.isSelected() && event.getCode() == KeyCode.DELETE) {
follow(selectedThumbCells, false);
}
});
}
@Override
protected void createGui() {
super.createGui();
addOnlineOfflineSelector();
}
private void addOnlineOfflineSelector() {
var group = new ToggleGroup();
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 -> {
((LiveJasminFollowedUpdateService)updateService).setShowOnline(online.isSelected());
queue.clear();
updateService.restart();
});
}
}

View File

@ -1,140 +0,0 @@
package ctbrec.ui.sites.jasmin;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.io.HttpException;
import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.jasmin.LiveJasminModel;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.tabs.PaginatedScheduledService;
import javafx.concurrent.Task;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Future;
import static ctbrec.io.HttpConstants.*;
public class LiveJasminFollowedUpdateService extends PaginatedScheduledService {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminFollowedUpdateService.class);
private final LiveJasmin liveJasmin;
private final String url;
private boolean showOnline = true;
public LiveJasminFollowedUpdateService(LiveJasmin liveJasmin) {
this.liveJasmin = liveJasmin;
this.url = liveJasmin.getBaseUrl() + "/en/free/favourite/get-favourite-list";
}
@Override
protected Task<List<Model>> createTask() {
return new Task<>() {
@Override
public List<Model> call() throws IOException {
if (!liveJasmin.credentialsAvailable()) {
throw new RuntimeException("Credentials missing");
}
boolean loggedIn = SiteUiFactory.getUi(liveJasmin).login();
if (!loggedIn) {
throw new RuntimeException("Couldn't login to livejasmin");
}
Request request = new Request.Builder()
.url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(REFERER, liveJasmin.getBaseUrl() + "/en/free/favorite")
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response response = liveJasmin.getHttpClient().execute(request)) {
if (response.isSuccessful()) {
String body = response.body().string();
List<Model> models = new ArrayList<>();
JSONObject json = new JSONObject(body);
if (json.has("success")) {
JSONObject data = json.getJSONObject("data");
JSONArray performers = data.getJSONArray("performers");
List<Future<?>> loadDetailsFutures = new LinkedList<>();
for (int i = 0; i < performers.length(); i++) {
JSONObject m = performers.getJSONObject(i);
String name = m.optString("pid");
if (name.isEmpty()) {
continue;
}
LiveJasminModel model = (LiveJasminModel) liveJasmin.createModel(name);
model.setId(String.valueOf(m.get("id")));
model.setDisplayName(m.getString("display_name"));
Model.State onlineState = LiveJasminModel.mapStatus(m.getInt("status"));
boolean online = onlineState == Model.State.ONLINE;
model.setOnlineState(onlineState);
if (online == showOnline) {
models.add(model);
}
loadDetailsFutures.add(GlobalThreadPool.submit(() -> loadModelDetails(model)));
}
for (Future<?> future : loadDetailsFutures) {
try {
future.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
// details couldn't be loaded, but that doesn't matter
}
}
LOG.debug("done");
} else {
LOG.error("Request failed:\n{}", body);
throw new IOException("Response was not successful");
}
return models;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
private void loadModelDetails(LiveJasminModel model) {
try {
String sessionId = liveJasmin.getHttpClient().getCookieJar().getCookie(HttpUrl.parse(liveJasmin.getBaseUrl()), "session").value();
String detailsUrl = liveJasmin.getBaseUrl() + "/en/member/flash/get-performer-details/" + model.getName() + "?appletType=html5&noFlash=0&session=" + sessionId;
Request request = new Request.Builder()
.url(detailsUrl)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(REFERER, liveJasmin.getBaseUrl() + "/en/member/chat-html5/" + model.getName())
.build();
try (Response response = liveJasmin.getHttpClient().execute(request)) {
if (response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
if (json.optBoolean("success")) {
JSONObject data = json.getJSONObject("data");
model.setPreview(data.getString("profile_picture_url"));
}
}
}
} catch (IOException e) {
// details couldn't be loaded, but that doesn't matter
}
}
};
}
public void setShowOnline(boolean showOnline) {
this.showOnline = showOnline;
}
}

View File

@ -1,78 +0,0 @@
package ctbrec.ui.sites.jasmin;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.jasmin.LiveJasminHttpClient;
import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.sites.AbstractSiteUi;
import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabProvider;
public class LiveJasminSiteUi extends AbstractSiteUi {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminSiteUi.class);
private final LiveJasmin liveJasmin;
private LiveJasminTabProvider tabProvider;
private LiveJasminConfigUi configUi;
private long lastLoginTime = 0;
public LiveJasminSiteUi(LiveJasmin liveJasmin) {
this.liveJasmin = liveJasmin;
}
@Override
public TabProvider getTabProvider() {
if (tabProvider == null) {
tabProvider = new LiveJasminTabProvider(liveJasmin);
}
return tabProvider;
}
@Override
public ConfigUI getConfigUI() {
if (configUi == null) {
configUi = new LiveJasminConfigUi(liveJasmin);
}
return configUi;
}
@Override
public synchronized boolean login() throws IOException {
// renew login every 30 min
long now = System.currentTimeMillis();
var renew = false;
if ((now - lastLoginTime) > TimeUnit.MINUTES.toMillis(30)) {
renew = true;
}
boolean automaticLogin = liveJasmin.login(renew);
if (automaticLogin) {
lastLoginTime = System.currentTimeMillis();
return true;
} else {
lastLoginTime = System.currentTimeMillis();
// login with external browser window
try {
new LiveJasminElectronLoginDialog(liveJasmin.getHttpClient().getCookieJar());
} catch (Exception e1) {
LOG.error("Error logging in with external browser", e1);
Dialogs.showError("Login error", "Couldn't login to " + liveJasmin.getName(), e1);
}
LiveJasminHttpClient httpClient = (LiveJasminHttpClient) liveJasmin.getHttpClient();
boolean loggedIn = httpClient.checkLoginSuccess();
if (loggedIn) {
LOG.info("Logged in");
} else {
LOG.info("Login failed");
}
return loggedIn;
}
}
}

View File

@ -1,80 +0,0 @@
package ctbrec.ui.sites.jasmin;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.sites.Site;
import ctbrec.ui.tabs.PaginatedScheduledService;
import ctbrec.ui.tabs.ThumbOverviewTab;
import javafx.concurrent.WorkerStateEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class LiveJasminTab extends ThumbOverviewTab {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminTab.class);
protected Label status;
protected Button acknowledge = new Button("That's alright");
private boolean betaAcknowledged = Config.getInstance().getSettings().livejasminBetaAcknowledged;
public LiveJasminTab(String title, PaginatedScheduledService updateService, Site site) {
super(title, updateService, site);
if(!betaAcknowledged) {
status = new Label("LiveJasmin is not fully functional. Live previews do not work.\n"
+ "If you get errors while loading the tabs, try to create an account and open the Followed tab first.");
grid.getChildren().add(status);
grid.getChildren().add(acknowledge);
} else {
status = new Label("Loading...");
grid.getChildren().add(status);
}
acknowledge.setOnAction(e -> {
betaAcknowledged = true;
Config.getInstance().getSettings().livejasminBetaAcknowledged = true;
try {
Config.getInstance().save();
} catch (IOException e1) {
LOG.error("Couldn't save config", e1);
}
status.setText("Loading...");
grid.getChildren().remove(acknowledge);
if(updateService != null) {
updateService.cancel();
updateService.reset();
updateService.restart();
}
});
}
@Override
protected void onSuccess() {
if(Config.getInstance().getSettings().livejasminBetaAcknowledged) {
grid.getChildren().remove(status);
grid.getChildren().remove(acknowledge);
super.onSuccess();
}
}
@Override
protected void onFail(WorkerStateEvent event) {
if(Config.getInstance().getSettings().livejasminBetaAcknowledged) {
status.setText("Error");
grid.getChildren().remove(acknowledge);
super.onFail(event);
}
}
public void setScene(Scene scene) {
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (this.isSelected() && event.getCode() == KeyCode.DELETE) {
follow(selectedThumbCells, false);
}
});
}
}

View File

@ -1,51 +0,0 @@
package ctbrec.ui.sites.jasmin;
import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.ui.sites.AbstractTabProvider;
import ctbrec.ui.tabs.ThumbOverviewTab;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
import javafx.util.Duration;
import java.util.ArrayList;
import java.util.List;
public class LiveJasminTabProvider extends AbstractTabProvider {
private final LiveJasminFollowedTab followedTab;
public LiveJasminTabProvider(LiveJasmin site) {
super(site);
followedTab = new LiveJasminFollowedTab(site);
followedTab.setRecorder(recorder);
followedTab.setImageAspectRatio(9.0 / 16.0);
}
@Override
protected List<Tab> getSiteTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>();
tabs.add(createTab("Girls", site.getBaseUrl() + "/en/girls/?listPageOrderType=most_popular"));
tabs.add(createTab("New Girls", site.getBaseUrl() + "/en/girls/new-models/?listPageOrderType=most_popular"));
tabs.add(createTab("Boys", site.getBaseUrl() + "/en/boys/?listPageOrderType=most_popular"));
tabs.add(createTab("New Boys", site.getBaseUrl() + "/en/boys/new-models/?listPageOrderType=most_popular"));
tabs.add(createTab("Couples", site.getBaseUrl() + "/en/girls/couple/?listPageOrderType=most_popular"));
tabs.add(createTab("Trans", site.getBaseUrl() + "/en/boys/transboy/?listPageOrderType=most_popular"));
tabs.add(followedTab);
return tabs;
}
@Override
public Tab getFollowedTab() {
return followedTab;
}
private ThumbOverviewTab createTab(String title, String url) {
var s = new LiveJasminUpdateService((LiveJasmin) site, url);
s.setPeriod(Duration.seconds(60));
ThumbOverviewTab tab = new ThumbOverviewTab(title, s, site);
tab.setRecorder(recorder);
tab.setImageAspectRatio(9.0 / 16.0);
return tab;
}
}

View File

@ -1,189 +0,0 @@
package ctbrec.ui.sites.jasmin;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.io.HttpException;
import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.jasmin.LiveJasminModel;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.tabs.PaginatedScheduledService;
import javafx.concurrent.Task;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.text.MessageFormat;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import static ctbrec.io.HttpConstants.*;
public class LiveJasminUpdateService extends PaginatedScheduledService {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminUpdateService.class);
private final String url;
private final LiveJasmin liveJasmin;
private final int modelsPerPage = 60;
private String listPageId = "";
private List<Model> modelsList;
private int lastPageLoaded;
private transient Instant lastListInfoRequest = Instant.EPOCH;
public LiveJasminUpdateService(LiveJasmin liveJasmin, String url) {
this.liveJasmin = liveJasmin;
this.url = url;
this.lastPageLoaded = 0;
}
@Override
protected Task<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException {
return getModelList().stream()
.skip((page - 1) * (long) modelsPerPage)
.limit(modelsPerPage)
.collect(Collectors.toList()); // NOSONAR
}
};
}
private List<Model> getModelList() throws IOException {
page = Math.min(page, 99);
if ((lastPageLoaded > 0) && Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 60) {
while (page > lastPageLoaded) {
lastPageLoaded++;
modelsList.addAll(loadMore());
}
return modelsList;
}
lastPageLoaded = 1;
modelsList = loadModelList();
while (page > lastPageLoaded) {
lastPageLoaded++;
modelsList.addAll(loadMore());
}
if (modelsList == null) {
return Collections.emptyList();
}
return modelsList;
}
private List<Model> loadModelList() throws IOException {
lastListInfoRequest = Instant.now();
var cookieJar = liveJasmin.getHttpClient().getCookieJar();
var sortCookie = new Cookie.Builder().domain(LiveJasmin.baseDomain).name("listPageOrderType").value("most_popular").build();
cookieJar.saveFromResponse(HttpUrl.parse(liveJasmin.getBaseUrl()), Collections.singletonList(sortCookie));
String category = (url.contains("boys")) ? "boys" : "girls";
var categoryCookie = new Cookie.Builder().domain(LiveJasmin.baseDomain).name("category").value(category).build();
cookieJar.saveFromResponse(HttpUrl.parse(liveJasmin.getBaseUrl()), Collections.singletonList(categoryCookie));
LOG.debug("Fetching page {}", url);
Request req = new Request.Builder()
.url(url)
.addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.addHeader(ACCEPT, MIMETYPE_APPLICATION_JSON)
.addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.addHeader(REFERER, liveJasmin.getBaseUrl())
.addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response response = liveJasmin.getHttpClient().execute(req)) {
LOG.debug("Response {} {}", response.code(), response.message());
if (response.isSuccessful()) {
String body = response.body().string();
List<Model> models = new ArrayList<>();
JSONObject json = new JSONObject(body);
if (json.optBoolean("success")) {
parseModels(models, json);
} else if (json.optString("error").equals("Please login.")) {
var siteUI = SiteUiFactory.getUi(liveJasmin);
if (siteUI.login()) {
return loadModelList();
} else {
LOG.error("Request failed:\n{}", body);
throw new IOException("Response was not successful");
}
} else {
LOG.error("Request failed:\n{}", body);
throw new IOException("Response was not successful");
}
return models;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
private List<Model> loadMore() throws IOException {
lastListInfoRequest = Instant.now();
String moreURL = liveJasmin.getBaseUrl() + MessageFormat.format("/en/list-page-ajax/show-more-json/{0}?wide=true&layout=layout-big&_dc={1}", listPageId, String.valueOf(System.currentTimeMillis()));
LOG.debug("Fetching page {}", moreURL);
Request req = new Request.Builder()
.url(moreURL)
.addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.addHeader(ACCEPT, MIMETYPE_APPLICATION_JSON)
.addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.addHeader(REFERER, liveJasmin.getBaseUrl())
.addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response response = liveJasmin.getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = response.body().string();
List<Model> models = new ArrayList<>();
JSONObject json = new JSONObject(body);
if (json.optBoolean("success")) {
parseModels(models, json);
}
return models;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
private void parseModels(List<Model> models, JSONObject json) {
if (json.has("data")) {
JSONObject data = json.getJSONObject("data");
if (data.optInt("isLast") > 0) {
lastPageLoaded = 999;
}
if (data.has("content")) {
JSONObject content = data.getJSONObject("content");
if (content.optInt("isLastPage") > 0) {
lastPageLoaded = 999;
}
listPageId = content.optString("listPageId");
JSONArray performers = content.getJSONArray("performers");
for (var i = 0; i < performers.length(); i++) {
var m = performers.getJSONObject(i);
var name = m.optString("pid");
if (name.isEmpty()) {
continue;
}
LiveJasminModel model = (LiveJasminModel) liveJasmin.createModel(name);
model.setId(m.getString("id"));
model.setPreview(m.optString("profilePictureUrl"));
model.setOnlineState(LiveJasminModel.mapStatus(m.optInt("status")));
model.setDisplayName(m.optString("display_name", null));
models.add(model);
}
} // if content
} // if data
}
}

View File

@ -1,50 +0,0 @@
package ctbrec.ui.sites.manyvids;
import ctbrec.Config;
import ctbrec.sites.manyvids.MVLive;
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.layout.GridPane;
public class MVLiveConfigUi extends AbstractConfigUI {
private MVLive site;
public MVLiveConfigUi(MVLive site) {
this.site = site;
}
@Override
public Parent createConfigPanel() {
var settings = Config.getInstance().getSettings();
GridPane layout = SettingsTab.createGridLayout();
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();
});
layout.add(enabled, 1, row++);
var deleteCookies = new Button("Delete Cookies");
deleteCookies.setOnAction(e -> site.getHttpClient().clearCookies());
layout.add(deleteCookies, 1, row);
GridPane.setColumnSpan(deleteCookies, 2);
GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
GridPane.setMargin(deleteCookies, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
return layout;
}
}

View File

@ -1,41 +0,0 @@
package ctbrec.ui.sites.manyvids;
import java.io.IOException;
import ctbrec.sites.manyvids.MVLive;
import ctbrec.ui.sites.AbstractSiteUi;
import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabProvider;
public class MVLiveSiteUi extends AbstractSiteUi {
private final MVLive mvlive;
private MVLiveTabProvider tabProvider;
private MVLiveConfigUi configUi;
public MVLiveSiteUi(MVLive mvlive) {
this.mvlive = mvlive;
}
@Override
public TabProvider getTabProvider() {
if (tabProvider == null) {
tabProvider = new MVLiveTabProvider(mvlive);
}
return tabProvider;
}
@Override
public ConfigUI getConfigUI() {
if (configUi == null) {
configUi = new MVLiveConfigUi(mvlive);
}
return configUi;
}
@Override
public boolean login() throws IOException {
return false;
}
}

View File

@ -1,32 +0,0 @@
package ctbrec.ui.sites.manyvids;
import ctbrec.sites.manyvids.MVLive;
import ctbrec.ui.sites.AbstractTabProvider;
import ctbrec.ui.tabs.ThumbOverviewTab;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
import java.util.ArrayList;
import java.util.List;
public class MVLiveTabProvider extends AbstractTabProvider {
public MVLiveTabProvider(MVLive mvlive) {
super(mvlive);
}
@Override
protected List<Tab> getSiteTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>();
tabs.add(createTab("Online"));
return tabs;
}
private Tab createTab(String title) {
var updateService = new MVLiveUpdateService((MVLive) site, "https://api.vidchat.manyvids.com/creator/live");
var tab = new ThumbOverviewTab(title, updateService, site);
tab.setRecorder(site.getRecorder());
tab.setImageAspectRatio(1);
return tab;
}
}

View File

@ -1,103 +0,0 @@
package ctbrec.ui.sites.manyvids;
import ctbrec.Model;
import ctbrec.io.HttpException;
import ctbrec.sites.manyvids.MVLive;
import ctbrec.sites.manyvids.MVLiveModel;
import ctbrec.ui.tabs.PaginatedScheduledService;
import javafx.concurrent.Task;
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.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import static ctbrec.io.HttpConstants.*;
public class MVLiveUpdateService extends PaginatedScheduledService {
private final MVLive mvlive;
private final String url;
private final int modelsPerPage = 48;
private static List<Model> modelsList;
private static Instant lastListInfoRequest = Instant.EPOCH;
private static final Logger LOG = LoggerFactory.getLogger(MVLiveUpdateService.class);
public MVLiveUpdateService(MVLive site, String url) {
this.mvlive = site;
this.url = url;
}
@Override
protected Task<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException {
return getModelList().stream()
.skip((page - 1) * (long) modelsPerPage)
.limit(modelsPerPage)
.collect(Collectors.toList()); // NOSONAR
}
};
}
private List<Model> getModelList() throws IOException {
if (Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 30) {
return modelsList;
}
lastListInfoRequest = Instant.now();
modelsList = loadModels(url);
if (modelsList == null) {
return Collections.emptyList();
}
return modelsList;
}
protected List<Model> loadModels(String url) throws IOException {
List<Model> models = new ArrayList<>();
LOG.debug("Loading live models from {}", url);
Request req = new Request.Builder()
.url(url)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(ORIGIN, MVLive.BASE_URL)
.header(REFERER, MVLive.BASE_URL)
.header(AUTHORIZATION, "GUEST")
.build();
try (Response response = mvlive.getHttpClient().execute(req)) {
String body = response.body().string();
LOG.trace("response body: {}", body);
if (response.isSuccessful()) {
JSONObject json = new JSONObject(body);
if (!json.has("live_creators")) {
LOG.debug("Unexpected response:\n{}", json.toString(2));
return Collections.emptyList();
}
JSONArray creators = json.getJSONArray("live_creators");
for (int i = 0; i < creators.length(); i++) {
JSONObject creator = creators.getJSONObject(i);
MVLiveModel model = mvlive.createModel(creator.getString("url_handle"));
model.updateStateFromJson(creator);
models.add(model);
}
if (!json.optString("next_token").isBlank()) {
models.addAll(loadModels(url + "?next_token=" + json.optString("next_token")));
}
} else {
throw new HttpException(response.code(), body);
}
}
return models;
}
}

View File

@ -1,89 +0,0 @@
package ctbrec.ui.sites.secretfriends;
import ctbrec.Config;
import ctbrec.sites.secretfriends.SecretFriends;
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.layout.GridPane;
public class SecretFriendsConfigUI extends AbstractConfigUI {
private final SecretFriends site;
public SecretFriendsConfigUI(SecretFriends secretFriends) {
this.site = secretFriends;
}
@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(site.getName() + " User"), 0, row);
// var username = new TextField(Config.getInstance().getSettings().stripchatUsername);
// username.textProperty().addListener((ob, o, n) -> {
// if(!n.equals(Config.getInstance().getSettings().stripchatUsername)) {
// Config.getInstance().getSettings().stripchatUsername = 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().stripchatPassword);
// password.textProperty().addListener((ob, o, n) -> {
// if(!n.equals(Config.getInstance().getSettings().stripchatPassword)) {
// Config.getInstance().getSettings().stripchatPassword = 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;
}
}

View File

@ -1,40 +0,0 @@
package ctbrec.ui.sites.secretfriends;
import ctbrec.sites.secretfriends.SecretFriends;
import ctbrec.ui.sites.AbstractSiteUi;
import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabProvider;
import java.io.IOException;
public class SecretFriendsSiteUi extends AbstractSiteUi {
private SecretFriendsTabProvider tabProvider;
private SecretFriendsConfigUI configUi;
private final SecretFriends site;
public SecretFriendsSiteUi(SecretFriends site) {
this.site = site;
}
@Override
public TabProvider getTabProvider() {
if (tabProvider == null) {
tabProvider = new SecretFriendsTabProvider(site);
}
return tabProvider;
}
@Override
public ConfigUI getConfigUI() {
if (configUi == null) {
configUi = new SecretFriendsConfigUI(site);
}
return configUi;
}
@Override
public synchronized boolean login() throws IOException {
return site.login();
}
}

View File

@ -1,34 +0,0 @@
package ctbrec.ui.sites.secretfriends;
import ctbrec.sites.secretfriends.SecretFriends;
import ctbrec.ui.sites.AbstractTabProvider;
import ctbrec.ui.tabs.ThumbOverviewTab;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
import java.util.ArrayList;
import java.util.List;
public class SecretFriendsTabProvider extends AbstractTabProvider {
public SecretFriendsTabProvider(SecretFriends site) {
super(site);
}
@Override
protected List<Tab> getSiteTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>();
tabs.add(createTab("Girls", SecretFriends.BASE_URI + "/users"));
tabs.add(createTab("New", SecretFriends.BASE_URI + "/newgirls"));
tabs.add(createTab("Couples", SecretFriends.BASE_URI + "/site/couple"));
return tabs;
}
private Tab createTab(String title, String url) {
var updateService = new SecretFriendsUpdateService(url, false, (SecretFriends) site);
var tab = new ThumbOverviewTab(title, updateService, site);
tab.setRecorder(recorder);
return tab;
}
}

View File

@ -1,92 +0,0 @@
package ctbrec.ui.sites.secretfriends;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.io.HtmlParser;
import ctbrec.io.HttpException;
import ctbrec.sites.secretfriends.SecretFriends;
import ctbrec.sites.secretfriends.SecretFriendsModelParser;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.tabs.PaginatedScheduledService;
import javafx.concurrent.Task;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.*;
import static ctbrec.ErrorMessages.HTTP_RESPONSE_BODY_IS_NULL;
import static ctbrec.io.HttpConstants.*;
public class SecretFriendsUpdateService extends PaginatedScheduledService {
private static final Logger LOG = LoggerFactory.getLogger(SecretFriendsUpdateService.class);
private final String url;
private final boolean loginRequired;
private final SecretFriends site;
public SecretFriendsUpdateService(String url, boolean loginRequired, SecretFriends site) {
this.url = url;
this.loginRequired = loginRequired;
this.site = site;
}
@Override
protected Task<List<Model>> createTask() {
return new Task<>() {
@Override
public List<Model> call() throws IOException {
if (loginRequired && !site.credentialsAvailable()) {
return Collections.emptyList();
} else {
String paginatedUrl = url;
if (page > 1) {
String pager = (url.indexOf("/users") > 0) ? "Friend_page" : "AModel_page";
paginatedUrl = HttpUrl.parse(url).newBuilder().addQueryParameter(pager, String.valueOf(page)).build().toString();
}
LOG.debug("Fetching page {}", paginatedUrl);
if (loginRequired) {
SiteUiFactory.getUi(site).login();
}
Request request = new Request.Builder()
.url(paginatedUrl)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(REFERER, SecretFriends.BASE_URI)
.build();
try (Response response = site.getHttpClient().execute(request)) {
if (response.isSuccessful()) {
return parseModels(Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string());
} else {
throw new HttpException(response.code(), response.message());
}
}
}
}
};
}
private List<Model> parseModels(String body) {
List<Model> models = new ArrayList<>();
Elements modelDivs = HtmlParser.getTags(body, "div[class~=model-wrapper]");
LOG.debug("Found {} models", modelDivs.size());
for (Element div : modelDivs) {
try {
models.add(SecretFriendsModelParser.parse(site, div));
} catch (Exception e) {
LOG.warn("Couldn't parse one of the models: {}", div.html(), e);
}
}
return models;
}
}

View File

@ -50,8 +50,6 @@ public class Settings {
};
public FlaresolverrSettings flaresolverr = new FlaresolverrSettings();
public String amateurTvUsername = "";
public String amateurTvPassword = "";
public String bongacamsBaseUrl = "https://bongacams.com";
public String bongaPassword = "";
public String bongaUsername = "";
@ -65,8 +63,6 @@ public class Settings {
@Deprecated
public boolean chaturbateUseFlaresolverr = false;
public int chaturbateMsBetweenRequests = 1000;
public String cherryTvPassword = "";
public String cherryTvUsername = "";
public boolean chooseStreamQuality = false;
public String colorAccent = "#FFFFFF";
public String colorBase = "#FFFFFF";
@ -100,10 +96,6 @@ public class Settings {
public byte[] key = null;
public List<String> ignoredModels = new ArrayList<>();
public String lastDownloadDir = "";
public String livejasminBaseUrl = "https://www.livejasmin.com";
public boolean livejasminBetaAcknowledged = false;
public String livejasminPassword = "";
public String livejasminUsername = "";
public boolean livePreviews = false;
public boolean localRecording = true;
public boolean logFFmpegOutput = false;

View File

@ -1,161 +0,0 @@
package ctbrec.sites.amateurtv;
import ctbrec.Model;
import ctbrec.StringUtil;
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 java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8;
public class AmateurTv extends AbstractSite {
public static final String BASE_URL = "https://en.amateur.tv";
private AmateurTvHttpClient httpClient;
@Override
public void init() throws IOException {
// nothing to do
}
@Override
public String getName() {
return "Amateur.tv";
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
public AmateurTvModel createModel(String name) {
AmateurTvModel model = new AmateurTvModel();
model.setName(name);
model.setUrl(BASE_URL + '/' + 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 credentialsAvailable() && getHttpClient().login();
}
@Override
public HttpClient getHttpClient() {
if (httpClient == null) {
httpClient = new AmateurTvHttpClient(getConfig());
}
return httpClient;
}
@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 List<Model> search(String q) throws IOException, InterruptedException {
if (StringUtil.isBlank(q)) {
return Collections.emptyList();
}
String url = getBaseUrl() + "/v3/readmodel/cache/filterbyusername/" + URLEncoder.encode(q, UTF_8);
var req = new Request.Builder()
.url(url)
.header(USER_AGENT, getConfig().getSettings().httpUserAgent)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT, Locale.ENGLISH.getLanguage())
.header(REFERER, getBaseUrl())
.build();
try (Response response = getHttpClient().execute(req)) {
if (response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
List<Model> models = new ArrayList<>();
JSONArray results = json.getJSONObject("cams").getJSONArray("nodes");
int maxResults = Math.min(30, results.length());
for (int i = 0; i < maxResults; i++) {
JSONObject result = results.getJSONObject(i);
var user = result.getJSONObject("user");
AmateurTvModel model = createModel(user.optString("username"));
model.setPreview(result.optString("imageURL"));
models.add(model);
}
return models;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
@Override
public boolean isSiteForModel(Model m) {
return m instanceof AmateurTvModel;
}
@Override
public boolean credentialsAvailable() {
String username = getConfig().getSettings().amateurTvUsername;
return username != null && !username.trim().isEmpty();
}
@Override
public Model createModelFromUrl(String url) {
Matcher m = Pattern.compile("https?://.*?amateur.tv/(.*)").matcher(url);
if (m.matches()) {
String modelName = m.group(1);
return createModel(modelName);
} else {
return super.createModelFromUrl(url);
}
}
@Override
public String getAffiliateLink() {
return BASE_URL;
}
}

View File

@ -1,171 +0,0 @@
package ctbrec.sites.amateurtv;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.AbstractDownload;
import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern;
import static ctbrec.io.HttpConstants.*;
public class AmateurTvDownload extends AbstractDownload {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTvDownload.class);
private static final int MAX_SECONDS_WITHOUT_TRANSFER = 20;
private final HttpClient httpClient;
private FileOutputStream fout;
private Instant timeOfLastTransfer = Instant.MAX;
private volatile boolean running;
private volatile boolean started;
private File targetFile;
public AmateurTvDownload(HttpClient httpClient) {
this.httpClient = httpClient;
}
@Override
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
this.config = config;
this.model = model;
this.startTime = startTime;
this.downloadExecutor = executorService;
splittingStrategy = initSplittingStrategy(config.getSettings());
targetFile = config.getFileForRecording(model, "mp4", startTime);
timeOfLastTransfer = Instant.now();
}
@Override
public void stop() {
running = false;
}
@Override
public void finalizeDownload() {
if (fout != null) {
try {
LOG.debug("Closing recording file {}", targetFile);
fout.close();
} catch (IOException e) {
LOG.error("Error while closing recording file {}", targetFile, e);
}
}
}
@Override
public boolean isRunning() {
return running;
}
@Override
public void postProcess(Recording recording) {
// nothing to do
}
@Override
public File getTarget() {
return targetFile;
}
@Override
public String getPath(Model model) {
String absolutePath = targetFile.getAbsolutePath();
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
String relativePath = absolutePath.replaceFirst(Pattern.quote(recordingsDir), "");
return relativePath;
}
@Override
public boolean isSingleFile() {
return true;
}
@Override
public long getSizeInByte() {
return getTarget().length();
}
@Override
public RecordingProcess call() throws Exception {
if (!started) {
started = true;
startDownload();
}
if (splittingStrategy.splitNecessary(this)) {
stop();
rescheduleTime = Instant.now();
} else {
rescheduleTime = Instant.now().plusSeconds(5);
}
if (!model.isOnline(true)) {
stop();
}
if (Duration.between(timeOfLastTransfer, Instant.now()).getSeconds() > MAX_SECONDS_WITHOUT_TRANSFER) {
LOG.info("No video data received for {} seconds. Stopping recording for model {}", MAX_SECONDS_WITHOUT_TRANSFER, model);
stop();
}
return this;
}
private void startDownload() {
downloadExecutor.submit(() -> {
running = true;
try {
StreamSource src = model.getStreamSources().get(0);
LOG.debug("Loading video from {}", src.getMediaPlaylistUrl());
Request request = new Request.Builder()
.url(src.getMediaPlaylistUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "en")
.header(ORIGIN, AmateurTv.BASE_URL)
.build();
try (Response resp = httpClient.execute(request)) {
if (resp.isSuccessful()) {
LOG.debug("Recording video stream to {}", targetFile);
Files.createDirectories(targetFile.getParentFile().toPath());
fout = new FileOutputStream(targetFile);
InputStream in = Objects.requireNonNull(resp.body()).byteStream();
byte[] b = new byte[1024];
int len;
while (running && !Thread.currentThread().isInterrupted() && (len = in.read(b)) >= 0) {
fout.write(b, 0, len);
timeOfLastTransfer = Instant.now();
getDownloadedBytes().addAndGet(len);
BandwidthMeter.add(len);
}
} else {
throw new HttpException(resp.code(), resp.message());
}
}
} catch (Exception e) {
LOG.error("Error while downloading MP4", e);
}
running = false;
});
}
}

View File

@ -1,50 +0,0 @@
package ctbrec.sites.amateurtv;
import static ctbrec.io.HttpConstants.*;
import java.io.IOException;
import java.util.Locale;
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 {
public AmateurTvHttpClient(Config config) {
super("amateurtv", config);
}
@Override
public boolean login() throws IOException {
return checkLoginSuccess();
}
/**
* Check, if the login worked by requesting the user profile
*
* @throws IOException
*/
public boolean checkLoginSuccess() throws IOException {
String url = AmateurTv.BASE_URL + "/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.BASE_URL)
.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

@ -1,190 +0,0 @@
package ctbrec.sites.amateurtv;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource;
import ctbrec.recorder.download.hls.FfmpegHlsDownload;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*;
public class AmateurTvModel extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTvModel.class);
private transient JSONArray qualities = new JSONArray();
private int[] resolution = new int[2];
@Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
if (ignoreCache) {
JSONObject json = getModelInfo();
setOnlineState(OFFLINE);
boolean online = json.optString("status").equalsIgnoreCase("online");
if (online) setOnlineState(ONLINE);
boolean brb = json.optBoolean("brb");
if (brb) setOnlineState(AWAY);
boolean privateChat = json.optString("privateChatStatus").equalsIgnoreCase("exclusive_private");
if (privateChat) setOnlineState(PRIVATE);
}
return onlineState == ONLINE;
}
@Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if (!failFast || onlineState == UNKNOWN || onlineState == UNCHECKED) {
try {
onlineState = isOnline(true) ? ONLINE : OFFLINE;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
onlineState = OFFLINE;
} catch (IOException | ExecutionException e) {
onlineState = OFFLINE;
}
}
return onlineState;
}
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
List<StreamSource> streamSources = new ArrayList<>();
String mediaPlaylistUrl = getMasterPlaylistUrl();
qualities.forEach(item -> {
String value = (String) item;
String[] res = value.split("x");
StreamSource src = new StreamSource();
src.setMediaPlaylistUrl(MessageFormat.format("{0}&variant={1}", mediaPlaylistUrl, res[1]));
src.setWidth(Integer.parseInt(res[0]));
src.setHeight(Integer.parseInt(res[1]));
src.setBandwidth(0);
streamSources.add(src);
});
return streamSources;
}
private String getMasterPlaylistUrl() throws IOException {
JSONObject json = getModelInfo();
JSONObject videoTech = json.getJSONObject("videoTechnologies");
qualities = json.getJSONArray("qualities");
return videoTech.getString("fmp4-hls");
}
@Override
public void invalidateCacheEntries() {
resolution = new int[2];
}
@Override
public void receiveTip(Double tokens) throws IOException {
// not supported
}
@Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
if (!failFast) {
try {
List<StreamSource> sources = getStreamSources();
if (!sources.isEmpty()) {
StreamSource best = sources.get(0);
resolution = new int[]{best.getWidth(), best.getHeight()};
}
} catch (IOException | ParseException | PlaylistException | JAXBException e) {
throw new ExecutionException(e);
}
}
return resolution;
}
@Override
public boolean follow() throws IOException {
String url = getSite().getBaseUrl() + "/v3/user/follow";
return followUnfollow(url);
}
@Override
public boolean unfollow() throws IOException {
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 = Objects.requireNonNull(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 {
String url = AmateurTv.BASE_URL + "/v3/readmodel/show/" + getName() + "/es";
Request req = new Request.Builder().url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, "en")
.header(REFERER, getSite().getBaseUrl() + '/' + getName())
.build();
try (Response response = site.getHttpClient().execute(req)) {
if (response.isSuccessful()) {
JSONObject jsonResponse = new JSONObject(response.body().string());
return jsonResponse;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
@Override
public RecordingProcess createDownload() {
return new FfmpegHlsDownload(getSite().getHttpClient());
}
}

View File

@ -1,178 +0,0 @@
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.*;
import static java.nio.charset.StandardCharsets.UTF_8;
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 List<Model> search(String q) throws IOException, InterruptedException {
String url = "https://api.cherry.tv/graphql?operationName=Search&variables="
+ "{\"limit\":6,\"slug\":\"" + URLEncoder.encode(q, UTF_8) + "\"}&extensions={\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"b6b001b46111c5634b5d3f48caa5ad38e747d74a5c841f447f0094e7f2bc2fb1\"}}";
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");
JSONArray streamers = data.getJSONArray("search");
for (int i = 0; i < streamers.length(); i++) {
JSONObject hit = streamers.getJSONObject(i);
CherryTvModel model = createModel(hit.getString("id"));
model.setId(hit.getString("id"));
boolean online = hit.optString("broadcastStatus").equalsIgnoreCase("Live");
model.setOnline(online);
model.setOnlineState(online ? ONLINE : OFFLINE);
model.setDescription(hit.getString("description"));
model.setPreview(hit.getString("image"));
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);
}
}
}

View File

@ -1,128 +0,0 @@
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("accountName", config.getSettings().cherryTvUsername)
.put("password", config.getSettings().cherryTvPassword)
.put("sourceDomain", "cherry.tv")
)
.put("extensions", new JSONObject()
.put("persistedQuery", new JSONObject()
.put("version", 1)
.put("sha256Hash", "dc7c4f9040e6bbbd9168e3e10867115117c52689beac68c2066c6e695759911a")
)
);
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\":\"b4e8b260e7ab5958011b7a242ecbc13158f05b90380c0dbcafd6a8e3b4c96414\"}}";
Request request = new Request.Builder()
.url(url)
.header(REFERER, CherryTv.BASE_URL)
.header(ORIGIN, CherryTv.BASE_URL)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
try (Response response = execute(request)) {
String body = Objects.requireNonNull(response.body()).string();
LOG.debug("Login body: {}", body);
if (response.isSuccessful()) {
JSONObject json = new JSONObject(body);
if (json.has("errors")) {
LOG.error(json.toString(2));
return false;
} else {
return json.optString("__typename").equals("FollowingList");
}
}
return false;
} catch (Exception e) {
LOG.error("Login check failed", e);
return false;
}
}
}

View File

@ -1,285 +0,0 @@
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.iheartradio.m3u8.data.StreamInfo;
import ctbrec.*;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONException;
import org.json.JSONObject;
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.ErrorMessages.HTTP_RESPONSE_BODY_IS_NULL;
import static ctbrec.Model.State.OFFLINE;
import static ctbrec.Model.State.ONLINE;
import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j
public class CherryTvModel extends AbstractModel {
private static final Pattern NEXT_DATA = Pattern.compile("<script id=\"__NEXT_DATA__\" type=\"application/json\">(.*?)</script>");
@Setter
private boolean online = false;
private int[] resolution;
private String masterPlaylistUrl;
@Getter
@Setter
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:")) {
log.trace("Model properties:\n{}", apolloState.toString(2));
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");
}
}
}
@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 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.setBandwidth(playlist.getStreamInfo().getBandwidth());
src.setHeight(Optional.ofNullable(playlist.getStreamInfo()).map(StreamInfo::getResolution).map(res -> res.height).orElse(0));
src.setWidth(Optional.ofNullable(playlist.getStreamInfo()).map(StreamInfo::getResolution).map(res -> res.width).orElse(0));
String masterUrl = masterPlaylistUrl;
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
String segmentUri = baseUrl + playlist.getUri();
src.setMediaPlaylistUrl(segmentUri);
if (src.getMediaPlaylistUrl().contains("?")) {
src.setMediaPlaylistUrl(src.getMediaPlaylistUrl().substring(0, src.getMediaPlaylistUrl().lastIndexOf('?')));
}
log.trace("Media playlist {}", src.getMediaPlaylistUrl());
sources.add(src);
}
}
return sources;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ExecutionException(e);
}
}
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
if (masterPlaylistUrl == null) {
log.info("Master playlist not found for {}:{}. This probably is webrtc stream", getSite().getName(), getName());
throw new StreamNotFoundException("Webrtc streams are not supported for " + getSite().getName());
}
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.getWidth(), best.getHeight()};
} 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();
}
@Override
public void readSiteSpecificData(Map<String, String> data) {
id = data.get("id");
}
@Override
public void writeSiteSpecificData(Map<String, String> data) {
data.put("id", id);
}
}

View File

@ -1,217 +0,0 @@
package ctbrec.sites.jasmin;
import ctbrec.Model;
import ctbrec.NotLoggedInExcetion;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.sites.AbstractSite;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static ctbrec.io.HttpConstants.*;
public class LiveJasmin extends AbstractSite {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasmin.class);
public static String baseUrl = "https://www.livejasmin.com";
public static String baseDomain = "www.livejasmin.com";
private HttpClient httpClient;
@Override
public String getName() {
return "LiveJasmin";
}
@Override
public String getBaseUrl() {
return baseUrl;
}
@Override
public String getAffiliateLink() {
return "https://awejmp.com/?siteId=jasmin&categoryName=girl&pageName=listpage&performerName=&prm[psid]=0xb00bface&prm[pstool]=205_1&prm[psprogram]=revs&prm[campaign_id]=&subAffId={SUBAFFID}&filters=";
}
@Override
public Model createModel(String name) {
LiveJasminModel model = new LiveJasminModel();
model.setName(name);
model.setDescription("");
model.setSite(this);
model.setUrl(getBaseUrl() + "/en/chat/" + name);
return model;
}
@Override
public Double getTokenBalance() throws IOException {
if (getLiveJasminHttpClient().login()) {
String sessionId = getLiveJasminHttpClient().getSessionId();
String url = getBaseUrl() + "/en/offline-surprise/get-member-balance?session=" + sessionId;
Request request = new Request.Builder().url(url)
.addHeader(USER_AGENT, getConfig().getSettings().httpUserAgent)
.addHeader(ACCEPT, "*/*")
.addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.addHeader(REFERER, getBaseUrl())
.addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response response = getHttpClient().execute(request)) {
if (response.isSuccessful()) {
String body = Objects.requireNonNull(response.body()).string();
JSONObject json = new JSONObject(body);
if (json.optBoolean("success")) {
return json.optDouble("result");
} else {
throw new IOException("Response was not successful: " + url + "\n" + body);
}
} else {
throw new HttpException(response.code(), response.message());
}
}
} else {
throw new IOException(new NotLoggedInExcetion());
}
}
@Override
public String getBuyTokensLink() {
return getAffiliateLink();
}
@Override
public boolean login() throws IOException {
return getHttpClient().login();
}
@Override
public HttpClient getHttpClient() {
if (httpClient == null) {
httpClient = new LiveJasminHttpClient(getConfig());
}
return httpClient;
}
@Override
public void init() throws IOException {
baseUrl = getConfig().getSettings().livejasminBaseUrl;
HttpUrl url = HttpUrl.parse(baseUrl);
baseDomain = url.topPrivateDomain();
}
@Override
public void shutdown() {
if (httpClient != null) {
httpClient.shutdown();
}
}
@Override
public boolean supportsTips() {
return true;
}
@Override
public boolean supportsFollow() {
return true;
}
@Override
public boolean supportsSearch() {
return true;
}
@Override
public List<Model> search(String q) throws IOException, InterruptedException {
String sessionId = getLiveJasminHttpClient().getSessionId();
String pathSegment = sessionId.charAt(0) == 'm' ? "member" : "guest";
String base = "https://api-gateway.dditsadn.com";
String url = base + "/v2/" + pathSegment + "/auto-suggest/suggestions?"
+ "product=livejasmin"
+ "&session=" + sessionId
+ "&limit=5"
+ "&embed[]=data.performers.*"
+ "&version=v2"
+ "&criteria[]=searchText,%3D," + URLEncoder.encode(q, "utf-8")
+ "&criteria[]=language,%3D,en"
+ "&criteria[]=gender,%3D,female";
Request request = new Request.Builder().url(url)
.addHeader(USER_AGENT, getConfig().getSettings().httpUserAgent)
.addHeader(ACCEPT, "*/*")
.addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.addHeader(REFERER, getBaseUrl())
.addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
LOG.debug("Search URL: {}", url);
try (Response response = getHttpClient().execute(request)) {
if (response.isSuccessful()) {
String body = Objects.requireNonNull(response.body()).string();
JSONObject json = new JSONObject(body);
List<Model> models = new ArrayList<>();
JSONObject data = json.getJSONObject("data");
JSONArray performers = data.getJSONArray("performers");
for (int i = 0; i < performers.length(); i++) {
JSONObject performer = performers.getJSONObject(i);
LiveJasminModel model = (LiveJasminModel) createModel(performer.getString("performerNick"));
model.setDisplayName(performer.optString("displayName", model.getName()));
model.setId(String.valueOf(performer.getLong("performerId")));
JSONArray profilePics = performer.optJSONArray("profilePictures");
if (profilePics != null && profilePics.length() > 0) {
JSONObject profilePic = profilePics.getJSONObject(profilePics.length() - 1);
model.setPreview("https:" + profilePic.getString("url"));
}
models.add(model);
}
return models;
} else {
throw new HttpException(response.code(), Objects.requireNonNull(response.body()).string());
}
}
}
@Override
public boolean isSiteForModel(Model m) {
return m instanceof LiveJasminModel;
}
@Override
public boolean credentialsAvailable() {
return !getConfig().getSettings().livejasminUsername.isEmpty();
}
private LiveJasminHttpClient getLiveJasminHttpClient() {
return (LiveJasminHttpClient) getHttpClient();
}
@Override
public Model createModelFromUrl(String url) {
Matcher m = Pattern.compile("http.*?livejasmin\\.com.*?#!chat/(.*)").matcher(url);
if (m.find()) {
String name = m.group(1);
return createModel(name);
}
m = Pattern.compile("http.*?livejasmin\\.com.*?/chat(?:-html5)?/(.*)").matcher(url);
if (m.find()) {
String name = m.group(1);
return createModel(name);
}
return super.createModelFromUrl(url);
}
public boolean login(boolean renew) throws IOException {
return getLiveJasminHttpClient().login(renew);
}
}

View File

@ -1,98 +0,0 @@
package ctbrec.sites.jasmin;
import static ctbrec.io.HttpConstants.*;
import java.io.IOException;
import java.util.Locale;
import java.util.NoSuchElementException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import okhttp3.Cookie;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class LiveJasminHttpClient extends HttpClient {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminHttpClient.class);
protected LiveJasminHttpClient(Config config) {
super("livejasmin", config);
// delete all cookies, if we are guests, because old guest sessions cause
// endless redirects
if(Config.getInstance().getSettings().livejasminUsername.isEmpty()) {
getCookieJar().clear();
}
}
@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;
}
return false;
}
public synchronized boolean login(boolean renew) throws IOException {
if (loggedIn && !renew) {
return true;
}
boolean cookiesWorked = checkLoginSuccess();
if (cookiesWorked) {
loggedIn = true;
LOG.debug("Logged in with cookies");
return true;
}
return false;
}
public boolean checkLoginSuccess() throws IOException {
OkHttpClient temp = client.newBuilder()
.followRedirects(false)
.followSslRedirects(false)
.build();
String url = "https://m." + LiveJasmin.baseDomain + "/en/member/favourite/get-favourite-list?ajax=1";
Request request = new Request.Builder()
.url(url)
.addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgentMobile)
.addHeader(ACCEPT, MIMETYPE_APPLICATION_JSON)
.addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.addHeader(REFERER, LiveJasmin.baseUrl)
.addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try(Response response = temp.newCall(request).execute()) {
LOG.debug("Login Check {}: {} - {}", url, response.code(), response.message());
if(response.isSuccessful()) {
return true;
} else {
return false;
}
}
}
public String getSessionId() {
Cookie sessionCookie = getCookieJar().getCookie(HttpUrl.parse(LiveJasmin.baseUrl), "session");
if(sessionCookie != null) {
return sessionCookie.value();
} else {
throw new NoSuchElementException("session cookie not found");
}
}
}

View File

@ -1,255 +0,0 @@
package ctbrec.sites.jasmin;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.StringUtil;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource;
import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ExecutionException;
import static ctbrec.io.HttpConstants.*;
public class LiveJasminModel extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminModel.class);
private String id;
private boolean online = false;
private int[] resolution;
private final Random rng = new Random();
private transient LiveJasminModelInfo modelInfo;
@Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
if (ignoreCache) {
loadModelInfo();
}
return online;
}
protected void loadModelInfo() throws IOException {
Request req = new Request.Builder().url(LiveJasmin.baseUrl)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, "*/*")
.header(ACCEPT_ENCODING, "deflate")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(REFERER, getSite().getBaseUrl() + "/")
.header(ORIGIN, getSite().getBaseUrl())
.build();
try (Response response = getSite().getHttpClient().execute(req)) {
// do nothing we just want the cookies
LOG.debug("Initial request succeeded: {} - {}", response.isSuccessful(), response.code());
}
String url = LiveJasmin.baseUrl + "/en/flash/get-performer-details/" + getName();
req = new Request.Builder().url(url)
.header(USER_AGENT, "Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15")
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_ENCODING, "deflate")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(REFERER, getSite().getBaseUrl() + "/")
.header(ORIGIN, getSite().getBaseUrl())
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = response.body().string();
JSONObject json = new JSONObject(body);
LOG.trace(json.toString(2));
if (json.optBoolean("success")) {
JSONObject data = json.getJSONObject("data");
modelInfo = new LiveJasminModelInfo.LiveJasminModelInfoBuilder()
.sbIp(data.optString("sb_ip", null))
.sbHash(data.optString("sb_hash", null))
.sessionId("m12345678901234567890123456789012")
.jsm2session(getSite().getHttpClient().getCookiesByName("session").get(0).value())
.performerId(data.optString("performer_id", getName()))
.displayName(data.optString("display_name", getName()))
.clientInstanceId(randomClientInstanceId())
.status(data.optInt("status", -1))
.build();
if (data.has("channelsiteurl")) {
setUrl(LiveJasmin.baseUrl + data.getString("channelsiteurl"));
}
onlineState = mapStatus(modelInfo.getStatus());
online = onlineState == State.ONLINE
&& StringUtil.isNotBlank(modelInfo.getSbIp())
&& StringUtil.isNotBlank(modelInfo.getSbHash());
LOG.debug("{} - status:{} {} {} {} {}", getName(), online, onlineState, Arrays.toString(resolution), getUrl(), id);
} else {
throw new IOException("Response was not successful: " + body);
}
} else {
throw new HttpException(response.code(), response.message());
}
}
}
private String randomClientInstanceId() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 32; i++) {
sb.append(rng.nextInt(9) + 1);
}
return sb.toString();
}
public static State mapStatus(int status) {
switch (status) {
case 0 -> {
return State.OFFLINE;
}
case 1 -> {
return State.ONLINE;
}
case 2, 3 -> {
return State.PRIVATE;
}
default -> {
LOG.debug("Unkown state {}", status);
return State.UNKNOWN;
}
}
}
@Override
public void setOnlineState(State status) {
super.setOnlineState(status);
online = status == State.ONLINE;
}
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
loadModelInfo();
String websocketUrlTemplate = "wss://dss-relay-{ipWithDashes}.dditscdn.com/memberChat/jasmin{modelName}{sb_hash}?random={clientInstanceId}";
String websocketUrl = websocketUrlTemplate
.replace("{ipWithDashes}", modelInfo.getSbIp().replace('.', '-'))
.replace("{modelName}", getName())
.replace("{sb_hash}", modelInfo.getSbHash())
.replace("{clientInstanceId}", modelInfo.getClientInstanceId());
modelInfo.setWebsocketUrl(websocketUrl);
LiveJasminStreamRegistration liveJasminStreamRegistration = new LiveJasminStreamRegistration(site, modelInfo);
List<StreamSource> streamSources = liveJasminStreamRegistration.getStreamSources();
Collections.sort(streamSources);
return streamSources;
}
@Override
public void invalidateCacheEntries() {
// noop
}
@Override
public void receiveTip(Double tokens) throws IOException {
LiveJasminTippingWebSocket tippingSocket = new LiveJasminTippingWebSocket(site.getHttpClient());
try {
tippingSocket.sendTip(this, Config.getInstance(), tokens);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
}
}
@Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
if (resolution == null) {
if (failFast) {
return new int[2];
}
try {
loadModelInfo();
} catch (IOException e) {
throw new ExecutionException(e);
}
}
return resolution;
}
@Override
public boolean follow() throws IOException {
return follow(true);
}
@Override
public boolean unfollow() throws IOException {
return follow(false);
}
private boolean follow(boolean follow) throws IOException {
if (id == null) {
loadModelInfo();
}
String sessionId = ((LiveJasminHttpClient) site.getHttpClient()).getSessionId();
String url;
if (follow) {
url = site.getBaseUrl() + "/en/free/favourite/add-favourite?session=" + sessionId + "&performerId=" + id;
} else {
url = site.getBaseUrl() + "/en/free/favourite/delete-favourite?session=" + sessionId + "&performerId=" + id;
}
Request request = new Request.Builder()
.url(url)
.addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.addHeader(ACCEPT, "*/*")
.addHeader(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.addHeader(REFERER, getUrl())
.addHeader(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response response = site.getHttpClient().execute(request)) {
if (response.isSuccessful()) {
String body = response.body().string();
JSONObject json = new JSONObject(body);
return json.optString("status").equalsIgnoreCase("ok");
} else {
throw new HttpException(response.code(), response.message());
}
}
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public void readSiteSpecificData(Map<String, String> data) {
id = data.get("id");
}
@Override
public void writeSiteSpecificData(Map<String, String> data) {
if (id == null) {
try {
loadModelInfo();
} catch (IOException e) {
LOG.error("Couldn't load model ID for {}. This can cause problems with saving / loading the model", getName());
}
}
data.put("id", id);
}
public void setOnline(boolean online) {
this.online = online;
}
@Override
public RecordingProcess createDownload() {
return new LiveJasminWebrtcDownload(getSite().getHttpClient());
}
}

View File

@ -1,18 +0,0 @@
package ctbrec.sites.jasmin;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class LiveJasminModelInfo {
private String websocketUrl;
private String sbIp;
private String sbHash;
private String sessionId;
private String jsm2session;
private String performerId;
private String displayName;
private String clientInstanceId;
private int status;
}

View File

@ -1,258 +0,0 @@
package ctbrec.sites.jasmin;
import ctbrec.Config;
import ctbrec.recorder.download.StreamSource;
import ctbrec.sites.Site;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import java.net.URLEncoder;
import java.util.*;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import static ctbrec.io.HttpConstants.USER_AGENT;
import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j
public class LiveJasminStreamRegistration {
private static final String KEY_EVENT = "event";
private static final String KEY_FUNC_NAME = "funcName";
private final Site site;
private final LiveJasminModelInfo modelInfo;
private final CyclicBarrier barrier = new CyclicBarrier(2);
private int streamCount = 0;
private WebSocket webSocket;
public LiveJasminStreamRegistration(Site site, LiveJasminModelInfo modelInfo) {
this.site = site;
this.modelInfo = modelInfo;
}
List<StreamSource> getStreamSources() {
var streamSources = new LinkedList<LiveJasminStreamSource>();
try {
Request webSocketRequest = new Request.Builder()
.url(modelInfo.getWebsocketUrl())
.addHeader(USER_AGENT, Config.getInstance().getSettings().httpUserAgentMobile)
.build();
log.debug("Websocket: {}", modelInfo.getWebsocketUrl());
webSocket = site.getHttpClient().newWebSocket(webSocketRequest, new WebSocketListener() {
@Override
public void onOpen(@NotNull WebSocket webSocket, @NotNull Response response) {
Thread.currentThread().setName("Stream registration for " + modelInfo.getPerformerId());
log.debug("onOpen");
JSONObject register = new JSONObject()
.put(KEY_EVENT, "register")
.put("applicationId", "memberChat/jasmin" + modelInfo.getPerformerId() + modelInfo.getSbHash())
.put("connectionData", new JSONObject()
.put("sessionID", modelInfo.getSessionId())
.put("jasmin2App", true)
.put("isMobileClient", false)
.put("platform", "desktop")
.put("chatID", "freechat")
.put("jsm2SessionId", modelInfo.getJsm2session())
.put("userType", "user")
.put("performerId", modelInfo.getPerformerId())
.put("clientRevision", "")
.put("livejasminTvmember", false)
.put("newApplet", true)
.put("livefeedtype", JSONObject.NULL)
.put("gravityCookieId", "")
.put("passparam", "")
.put("brandID", "jasmin")
.put("cobrandId", "livejasmin")
.put("subbrand", "livejasmin")
.put("siteName", "LiveJasmin")
.put("siteUrl", "https://www.livejasmin.com")
.put("clientInstanceId", modelInfo.getClientInstanceId())
.put("armaVersion", "38.32.1-LIVEJASMIN-44016-1")
.put("isPassive", false)
.put("peekPatternJsm2", true)
.put("chatHistoryRequired", true)
);
log.trace("Stream registration\n{}", register.toString(2));
send(register.toString());
send(new JSONObject().put(KEY_EVENT, "ping").toString());
send(new JSONObject()
.put(KEY_EVENT, "call")
.put(KEY_FUNC_NAME, "makeActive")
.put("data", new JSONArray())
.toString());
send(new JSONObject()
.put(KEY_EVENT, "call")
.put(KEY_FUNC_NAME, "setVideo")
.put("data", new JSONArray()
.put(JSONObject.NULL)
.put(false)
.put(true)
.put(modelInfo.getJsm2session())
)
.toString());
send(new JSONObject()
.put(KEY_EVENT, "connectSharedObject")
.put("name", "data/chat_so")
.toString());
}
@Override
public void onFailure(@NotNull WebSocket webSocket, @NotNull Throwable t, @Nullable Response response) {
log.error("onFailure", t);
awaitBarrier();
webSocket.close(1000, "");
}
@Override
public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) {
log.trace("< {}", text);
JSONObject message = new JSONObject(text);
if (message.opt(KEY_EVENT).equals("pong")) {
new Thread(() -> {
try {
Thread.sleep(message.optInt("nextPing"));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
send(new JSONObject().put(KEY_EVENT, "ping").toString());
}).start();
} else if (message.optString(KEY_EVENT).equals("updateSharedObject") && message.optString("name").equals("data/chat_so")) {
log.trace(message.toString(2));
JSONArray list = message.getJSONArray("list");
for (int i = 0; i < list.length(); i++) {
JSONObject attribute = list.getJSONObject(i);
if (attribute.optString("name").equals("streamList")) {
JSONObject value = attribute.getJSONObject("newValue");
JSONObject patterns = value.getJSONObject("patterns");
String freePattern = patterns.getString("free");
JSONArray streams = value.getJSONArray("streams");
for (int j = 0; j < streams.length(); j++) {
JSONObject stream = streams.getJSONObject(j);
addStreamSource(streamSources, freePattern, stream);
}
Collections.sort(streamSources);
Collections.reverse(streamSources);
for (LiveJasminStreamSource src : streamSources) {
JSONObject getVideoData = new JSONObject()
.put(KEY_EVENT, "call")
.put(KEY_FUNC_NAME, "getVideoData")
.put("data", new JSONArray()
.put(new JSONObject()
.put("protocols", new JSONArray()
.put("h5live")
)
.put("streamId", src.getStreamId())
.put("correlationId", UUID.randomUUID().toString().replace("-", "").substring(0, 16))
)
);
streamCount++;
send(getVideoData.toString());
}
}
}
} else if (message.optString(KEY_FUNC_NAME).equals("setVideoData")) {
JSONObject data = message.getJSONArray("data").getJSONArray(0).getJSONObject(0);
String streamId = data.getString("streamId");
String wssUrl = data.getJSONObject("protocol").getJSONObject("h5live").getString("wssUrl");
streamSources.stream().filter(src -> Objects.equals(src.getStreamId(), streamId)).findAny().ifPresent(src -> src.setMediaPlaylistUrl(wssUrl));
if (--streamCount == 0) {
awaitBarrier();
}
} else if (!message.optString(KEY_FUNC_NAME).equals("chatHistory")) {
log.trace("onMessageT {}", new JSONObject(text).toString(2));
}
}
@Override
public void onMessage(@NotNull WebSocket webSocket, @NotNull ByteString bytes) {
log.trace("onMessageB");
super.onMessage(webSocket, bytes);
}
@Override
public void onClosed(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
log.trace("onClosed {} {}", code, reason);
super.onClosed(webSocket, code, reason);
}
@Override
public void onClosing(@NotNull WebSocket webSocket, int code, @NotNull String reason) {
log.trace("onClosing {} {}", code, reason);
awaitBarrier();
}
private void send(String msg) {
log.debug(" > {}", msg);
webSocket.send(msg);
}
});
log.debug("Waiting for websocket to return");
awaitBarrier();
log.debug("Websocket is done. Stream sources {}", streamSources);
} catch (Exception e) {
log.error("Couldn't determine stream sources", e);
}
return streamSources.stream().map(StreamSource.class::cast).collect(Collectors.toList()); // NOSONAR
}
private void addStreamSource(LinkedList<LiveJasminStreamSource> streamSources, String pattern, JSONObject stream) {
int w = stream.getInt("width");
int h = stream.getInt("height");
int bitrate = stream.getInt("bitrate") * 1024;
String name = stream.getString("name");
String streamName = pattern.replace("{$streamname}", name);
String streamId = stream.getString("streamId");
String rtmpUrl = "rtmp://{ip}/memberChat/jasmin{modelName}{sb_hash}?sessionId-{sessionId}|clientInstanceId-{clientInstanceId}"
.replace("{ip}", modelInfo.getSbIp())
.replace("{modelName}", modelInfo.getPerformerId())
.replace("{sb_hash}", modelInfo.getSbHash())
.replace("{sessionId}", modelInfo.getSessionId())
.replace("{clientInstanceId}", modelInfo.getClientInstanceId());
String hlsUrl = "https://dss-hls-{ipWithDashes}.dditscdn.com/h5live/http/playlist.m3u8?url={rtmpUrl}&stream={streamName}"
.replace("{ipWithDashes}", modelInfo.getSbIp().replace('.', '-'))
.replace("{rtmpUrl}", URLEncoder.encode(rtmpUrl, UTF_8))
.replace("{streamName}", URLEncoder.encode(streamName, UTF_8));
LiveJasminStreamSource streamSource = new LiveJasminStreamSource();
streamSource.setMediaPlaylistUrl(hlsUrl);
streamSource.setWidth(w);
streamSource.setHeight(h);
streamSource.setBandwidth(bitrate);
streamSource.setRtmpUrl(rtmpUrl);
streamSource.setStreamName(streamName);
streamSource.setStreamId(streamId);
streamSource.setStreamRegistration(this);
streamSources.add(streamSource);
}
private void awaitBarrier() {
try {
barrier.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error(e.getLocalizedMessage(), e);
} catch (TimeoutException | BrokenBarrierException e) {
log.error(e.getLocalizedMessage(), e);
}
}
void close() {
webSocket.close(1000, "");
}
}

View File

@ -1,14 +0,0 @@
package ctbrec.sites.jasmin;
import ctbrec.recorder.download.StreamSource;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LiveJasminStreamSource extends StreamSource {
private String rtmpUrl;
private String streamName;
private String streamId;
private LiveJasminStreamRegistration streamRegistration;
}

View File

@ -1,176 +0,0 @@
package ctbrec.sites.jasmin;
import static ctbrec.io.HttpConstants.*;
import java.io.IOException;
import java.util.Locale;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.io.HttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
public class LiveJasminTippingWebSocket {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminTippingWebSocket.class);
private String applicationId;
private String sessionId;
private String jsm2SessionId;
private String sb_ip;
private String sb_hash;
private String relayHost;
private String streamHost;
private String clientInstanceId = "01234567890123456789012345678901"; // TODO where to get or generate a random id?
private WebSocket relay;
private Throwable exception;
private HttpClient client;
private Model model;
public LiveJasminTippingWebSocket(HttpClient client) {
this.client = client;
}
public void sendTip(Model model, Config config, double amount) throws IOException, InterruptedException {
this.model = model;
getPerformerDetails(model.getName());
LOG.debug("appid: {}", applicationId);
LOG.debug("sessionid: {}",sessionId);
LOG.debug("jsm2sessionid: {}",jsm2SessionId);
LOG.debug("sb_ip: {}",sb_ip);
LOG.debug("sb_hash: {}",sb_hash);
LOG.debug("relay host: {}",relayHost);
LOG.debug("stream host: {}",streamHost);
LOG.debug("clientinstanceid {}",clientInstanceId);
Request request = new Request.Builder()
.url("https://" + relayHost + "/")
.header(ORIGIN, LiveJasmin.baseUrl)
.header(USER_AGENT, config.getSettings().httpUserAgent)
.header(ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.build();
Object monitor = new Object();
relay = client.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
LOG.trace("relay open {}", model.getName());
sendToRelay("{\"event\":\"register\",\"applicationId\":\"" + applicationId
+ "\",\"connectionData\":{\"jasmin2App\":true,\"isMobileClient\":false,\"platform\":\"desktop\",\"chatID\":\"freechat\","
+ "\"sessionID\":\"" + sessionId + "\"," + "\"jsm2SessionId\":\"" + jsm2SessionId + "\",\"userType\":\"user\"," + "\"performerId\":\""
+ model
+ "\",\"clientRevision\":\"\",\"proxyIP\":\"\",\"playerVer\":\"nanoPlayerVersion: 3.10.3 appCodeName: Mozilla appName: Netscape appVersion: 5.0 (X11) platform: Linux x86_64\",\"livejasminTvmember\":false,\"newApplet\":true,\"livefeedtype\":null,\"gravityCookieId\":\"\",\"passparam\":\"\",\"brandID\":\"jasmin\",\"cobrandId\":\"\",\"subbrand\":\"livejasmin\",\"siteName\":\"LiveJasmin\",\"siteUrl\":\""+LiveJasmin.baseUrl+"\","
+ "\"clientInstanceId\":\"" + clientInstanceId + "\",\"armaVersion\":\"34.10.0\",\"isPassive\":false}}");
response.close();
}
@Override
public void onMessage(WebSocket webSocket, String text) {
LOG.trace("relay <-- {} T{}", model.getName(), text);
JSONObject event = new JSONObject(text);
if (event.optString("event").equals("accept")) {
GlobalThreadPool.submit(() -> {
sendToRelay("{\"event\":\"connectSharedObject\",\"name\":\"data/chat_so\"}");
});
} else if(event.optString("event").equals("call")) {
String func = event.optString("funcName");
if (func.equals("setName")) {
LOG.debug("Entered chat -> Sending tip of {}", amount);
sendToRelay("{\"event\":\"call\",\"funcName\":\"sendSurprise\",\"data\":["+amount+",\"SurpriseGirlFlower\"]}");
} else if (func.equals("startSurprise")) {
// {"event":"call","funcName":"startSurprise","userId":"xyz_hash_gibberish","data":[{"memberid":"userxyz","amount":1,"tipName":"SurpriseGirlFlower","err_desc":"OK","err_text":"OK"}]}
JSONArray dataArray = event.getJSONArray("data");
JSONObject data = dataArray.getJSONObject(0);
String errText = data.optString("err_text");
String errDesc = data.optString("err_desc");
LOG.debug("Tip response {} - {}", errText, errDesc);
if(!errText.equalsIgnoreCase("OK")) {
exception = new IOException(errText + " - " + errDesc);
}
synchronized (monitor) {
monitor.notify();
}
}
}
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
LOG.trace("relay <-- {} B{}", model.getName(), bytes.toString());
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
LOG.trace("relay closed {} {} {}", code, reason, model.getName());
exception = new IOException("Socket closed by server - " + code + " " + reason);
synchronized (monitor) {
monitor.notify();
}
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
exception = t;
synchronized (monitor) {
monitor.notify();
}
}
});
synchronized (monitor) {
monitor.wait();
}
if(exception != null) {
throw new IOException(exception);
}
}
private void sendToRelay(String msg) {
LOG.trace("relay --> {} {}", model.getName(), msg);
relay.send(msg);
}
protected void getPerformerDetails(String name) throws IOException {
String url = "https://m." + LiveJasmin.baseDomain + "/en/chat-html5/" + name;
Request req = new Request.Builder()
.url(url)
.header(USER_AGENT, "Mozilla/5.0 (iPhone; CPU OS 10_14 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.1 Mobile/14E304 Safari/605.1.15")
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(REFERER, LiveJasmin.baseUrl)
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response response = client.execute(req)) {
if (response.isSuccessful()) {
String body = response.body().string();
JSONObject json = new JSONObject(body);
if (json.optBoolean("success")) {
JSONObject data = json.getJSONObject("data");
JSONObject config = data.getJSONObject("config");
JSONObject armageddonConfig = config.getJSONObject("armageddonConfig");
JSONObject chatRoom = config.getJSONObject("chatRoom");
sessionId = armageddonConfig.getString("sessionid");
jsm2SessionId = armageddonConfig.getString("jsm2session");
sb_hash = chatRoom.getString("sb_hash");
sb_ip = chatRoom.getString("sb_ip");
applicationId = "memberChat/jasmin" + name + sb_hash;
relayHost = "dss-relay-" + sb_ip.replace('.', '-') + ".dditscdn.com";
streamHost = "dss-live-" + sb_ip.replace('.', '-') + ".dditscdn.com";
} else {
throw new IOException("Response was not successful: " + body);
}
} else {
throw new IOException(response.code() + " - " + response.message());
}
}
}
}

View File

@ -1,247 +0,0 @@
package ctbrec.sites.jasmin;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.HttpClient;
import ctbrec.recorder.download.AbstractDownload;
import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource;
import ctbrec.sites.showup.Showup;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern;
import static ctbrec.io.HttpConstants.*;
public class LiveJasminWebrtcDownload extends AbstractDownload {
private static final Logger LOG = LoggerFactory.getLogger(LiveJasminWebrtcDownload.class);
private static final int MAX_SECONDS_WITHOUT_TRANSFER = 20;
private final HttpClient httpClient;
private WebSocket ws;
private FileOutputStream fout;
private Instant timeOfLastTransfer = Instant.MAX;
private volatile boolean running;
private volatile boolean started;
private File targetFile;
public LiveJasminWebrtcDownload(HttpClient httpClient) {
this.httpClient = httpClient;
}
@Override
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
this.config = config;
this.model = model;
this.startTime = startTime;
this.downloadExecutor = executorService;
splittingStrategy = initSplittingStrategy(config.getSettings());
targetFile = config.getFileForRecording(model, "mp4", startTime);
timeOfLastTransfer = Instant.now();
}
@Override
public void stop() {
running = false;
if (ws != null) {
ws.close(1000, "");
ws = null;
}
}
@Override
public void finalizeDownload() {
if (fout != null) {
try {
LOG.debug("Closing recording file {}", targetFile);
fout.close();
} catch (IOException e) {
LOG.error("Error while closing recording file {}", targetFile, e);
}
}
}
@Override
public boolean isRunning() {
return running;
}
@Override
public void postProcess(Recording recording) {
// nothing to do
}
@Override
public File getTarget() {
return targetFile;
}
@Override
public String getPath(Model model) {
String absolutePath = targetFile.getAbsolutePath();
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
String relativePath = absolutePath.replaceFirst(Pattern.quote(recordingsDir), "");
return relativePath;
}
@Override
public boolean isSingleFile() {
return true;
}
@Override
public long getSizeInByte() {
return getTarget().length();
}
@Override
public RecordingProcess call() throws Exception {
if (!started) {
started = true;
startDownload();
}
if (splittingStrategy.splitNecessary(this)) {
stop();
rescheduleTime = Instant.now();
} else {
rescheduleTime = Instant.now().plusSeconds(5);
}
if (!model.isOnline(true)) {
stop();
}
if (Duration.between(timeOfLastTransfer, Instant.now()).getSeconds() > MAX_SECONDS_WITHOUT_TRANSFER) {
LOG.info("No video data received for {} seconds. Stopping recording for model {}", MAX_SECONDS_WITHOUT_TRANSFER, model);
stop();
}
return this;
}
private void startDownload() throws IOException, PlaylistException, ParseException, ExecutionException {
LiveJasminModel liveJasminModel = (LiveJasminModel) model;
List<StreamSource> streamSources = liveJasminModel.getStreamSources();
LiveJasminStreamSource streamSource = (LiveJasminStreamSource) selectStreamSource(streamSources);
LiveJasminStreamRegistration streamRegistration = streamSource.getStreamRegistration();
Request request = new Request.Builder()
.url(streamSource.getMediaPlaylistUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "pl")
.header(REFERER, model.getSite().getBaseUrl() + "/")
.header(ORIGIN, Showup.BASE_URL)
.build();
running = true;
LOG.debug("Opening webrtc connection {}", request.url());
ws = httpClient.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
LOG.trace("onOpen {} {}", webSocket, response);
response.close();
try {
LOG.trace("Recording video stream to {}", targetFile);
Files.createDirectories(targetFile.getParentFile().toPath());
fout = new FileOutputStream(targetFile);
} catch (Exception e) {
LOG.error("Couldn't open file {} to save the video stream", targetFile, e);
stop();
}
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
super.onMessage(webSocket, bytes);
LOG.trace("received video data with length {}", bytes.size());
timeOfLastTransfer = Instant.now();
try {
byte[] videoData = bytes.toByteArray();
fout.write(videoData);
BandwidthMeter.add(videoData.length);
} catch (IOException e) {
if (running) {
LOG.error("Couldn't write video stream to file", e);
stop();
}
}
}
@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
LOG.trace("onMessageT {} {}", webSocket, text);
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
stop();
if (t instanceof EOFException) {
LOG.info("End of stream detected for model {}", model);
} else {
LOG.error("Websocket failure for model {} {}", model, response, t);
}
if (response != null) {
response.close();
}
streamRegistration.close();
}
@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
super.onClosing(webSocket, code, reason);
LOG.trace("Websocket closing for model {} {} {}", model, code, reason);
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
super.onClosed(webSocket, code, reason);
LOG.debug("Websocket closed for model {} {} {}", model, code, reason);
stop();
streamRegistration.close();
}
});
}
@Override
public void awaitEnd() {
long secondsToWait = 30;
for (int i = 0; i < secondsToWait; i++) {
if (ws == null) {
return;
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.warn("Interrupted while waiting for the download to terminate");
}
}
}
LOG.warn("Download didn't finish in {} seconds", secondsToWait);
}
}

View File

@ -1,212 +0,0 @@
package ctbrec.sites.manyvids;
import ctbrec.Model;
import ctbrec.Model.State;
import ctbrec.io.HtmlParser;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.sites.AbstractSite;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject;
import org.jsoup.nodes.Element;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8;
public class MVLive extends AbstractSite {
private static final String STARS = "stars";
public static final String WS_ORIGIN = "https://live.manyvids.com";
public static final String BASE_URL = "https://www.manyvids.com/";
private MVLiveHttpClient httpClient;
private String mvtoken;
@Override
public String getName() {
return "MV Live";
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
public String getAffiliateLink() {
return getBaseUrl() + "/Join-MV/1002294529";
}
@Override
public MVLiveModel createModel(String name) {
MVLiveModel model = new MVLiveModel();
model.setName(name);
model.setDescription("");
model.setSite(this);
model.setUrl(WS_ORIGIN + '/' + name);
return model;
}
@Override
public Double getTokenBalance() throws IOException {
return 0d;
}
@Override
public String getBuyTokensLink() {
return getAffiliateLink();
}
@Override
public boolean login() throws IOException {
return false;
}
@Override
public HttpClient getHttpClient() {
if (httpClient == null) {
httpClient = new MVLiveHttpClient(getConfig());
}
return httpClient;
}
@Override
public void shutdown() {
if (httpClient != null) {
httpClient.shutdown();
}
}
@Override
public boolean supportsTips() {
return false;
}
@Override
public boolean supportsFollow() {
return false;
}
@Override
public boolean isSiteForModel(Model m) {
return m instanceof MVLiveModel;
}
@Override
public boolean credentialsAvailable() {
return false;
}
@Override
public void init() {
// nothing to do
}
@Override
public boolean supportsSearch() {
return true;
}
String getMvToken() throws IOException {
if (mvtoken == null) {
Request request = new Request.Builder()
.url(BASE_URL)
.header(USER_AGENT, getConfig().getSettings().httpUserAgent)
.build();
try (Response response = getHttpClient().execute(request)) {
if (response.isSuccessful()) {
Element tag = HtmlParser.getTag(response.body().string(), "html");
mvtoken = tag.attr("data-mvtoken");
} else {
throw new HttpException(response.code(), response.message());
}
}
}
return mvtoken;
}
@Override
public List<Model> search(String q) throws IOException, InterruptedException {
List<Model> result = new ArrayList<>();
RequestBody body = new FormBody.Builder()
.add("mvtoken", getMvToken())
.add("type", "search")
.add("category", STARS)
.add("search", q)
.build();
Request request = new Request.Builder()
.url("https://www.manyvids.com/includes/filterSearch.php")
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(USER_AGENT, getConfig().getSettings().httpUserAgent)
.header(ORIGIN, MVLive.BASE_URL)
.header(REFERER, MVLive.BASE_URL)
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.post(body)
.build();
try (Response response = getHttpClient().execute(request)) {
if (response.isSuccessful()) {
String responseBody = response.body().string();
parseSearchResult(result, responseBody);
} else {
throw new HttpException(response.code(), response.message());
}
}
return result;
}
private void parseSearchResult(List<Model> result, String responseBody) {
JSONObject json = new JSONObject(responseBody);
if (json.has(STARS)) {
JSONArray stars = json.getJSONArray(STARS);
for (int i = 0; i < stars.length(); i++) {
JSONObject star = stars.getJSONObject(i);
String name = star.getString("label");
MVLiveModel model = createModel(name);
long id = star.getLong("id");
String url = BASE_URL + "/Profile/" + id + '/' + model.getDisplayName().replace(" ", "-") + '/';
model.setUrl(url);
model.setPreview(star.getString("img"));
if (star.optString("is_live").equals("1")) {
if (star.optString("is_private").equals("1")) {
model.setOnlineState(State.PRIVATE);
} else {
model.setOnlineState(State.ONLINE);
}
} else {
model.setOnlineState(State.OFFLINE);
}
result.add(model);
}
}
}
@Override
public Model createModelFromUrl(String url) {
Matcher m = Pattern.compile("https://live.manyvids.com/(?:stream/)?(.*?)(?:/.*?)?").matcher(url.trim());
if (m.matches()) {
String modelName = URLDecoder.decode(m.group(1), UTF_8);
return createModel(modelName);
}
m = Pattern.compile("https://www.manyvids.com/MVLive/(.*?)/\\d+/?").matcher(url.trim());
if (m.matches()) {
String modelName = URLDecoder.decode(m.group(1), UTF_8);
return createModel(modelName);
}
return super.createModelFromUrl(url);
}
}

View File

@ -1,219 +0,0 @@
package ctbrec.sites.manyvids;
import static ctbrec.StringUtil.*;
import static ctbrec.io.HttpConstants.*;
import static ctbrec.sites.manyvids.MVLive.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;
import ctbrec.Config;
import ctbrec.sites.manyvids.wsmsg.GetBroadcastHealth;
import ctbrec.sites.manyvids.wsmsg.Message;
import ctbrec.sites.manyvids.wsmsg.Ping;
import ctbrec.sites.manyvids.wsmsg.RegisterMessage;
import ctbrec.sites.manyvids.wsmsg.SendMessage;
import okhttp3.Cookie;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
public class MVLiveClient {
private static final Logger LOG = LoggerFactory.getLogger(MVLiveClient.class);
private final Map<String, Message> futureResponses = new HashMap<>();
private final MVLiveHttpClient httpClient;
private final Object streamUrlMonitor = new Object();
private final Random rng = new Random();
private WebSocket ws;
private volatile boolean running = false;
private volatile boolean connecting = false;
private String masterPlaylist = null;
private String roomNumber;
private String roomId;
private ScheduledExecutorService scheduler;
public MVLiveClient(MVLiveHttpClient httpClient) {
this.httpClient = httpClient;
}
public void start(MVLiveModel model) throws IOException {
running = true;
if (ws == null && !connecting) {
httpClient.fetchAuthenticationCookies();
JSONObject response = model.getRoomLocation();
roomNumber = response.optString("floorId");
roomId = response.optString("roomId");
int randomNumber = 100 + rng.nextInt(800);
String randomString = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
String wsUrl = model.getApiUrl().replace("https", "wss");
String url = String.format("%s/%s/eventbus/%s/%s/websocket", wsUrl, roomNumber, randomNumber, randomString);
LOG.info("Websocket is null. Starting a new connection to {}", url);
ws = createWebSocket(url, roomId, model.getDisplayName());
}
}
private String getPhpSessionIdCookie() {
List<Cookie> cookies = httpClient.getCookiesByName("PHPSESSID");
return cookies.stream().map(c -> c.name() + "=" + c.value()).findFirst().orElse("");
}
public void stop() {
running = false;
Optional.ofNullable(scheduler).ifPresent(ScheduledExecutorService::shutdown);
ws.close(1000, "Good Bye"); // terminate normally (1000)
ws = null;
}
private WebSocket createWebSocket(String wsUrl, String roomId, String modelName) {
connecting = true;
Request req = new Request.Builder()
.url(wsUrl)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ORIGIN, WS_ORIGIN)
.header(COOKIE, getPhpSessionIdCookie())
.build();
return httpClient.newWebSocket(req, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
try {
connecting = false;
LOG.trace("WS open: [{}]", response.body().string());
scheduler = new ScheduledThreadPoolExecutor(1);
scheduler.scheduleAtFixedRate(() -> sendMessages(new Ping()), 5, 5, TimeUnit.SECONDS);
} catch (IOException e) {
LOG.error("Error while processing onOpen event", e);
}
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
super.onClosed(webSocket, code, reason);
connecting = false;
LOG.trace("MVLive websocket closed: {} {}", code, reason);
MVLiveClient.this.ws = null;
running = false;
synchronized (streamUrlMonitor) {
streamUrlMonitor.notifyAll();
}
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
running = false;
synchronized (streamUrlMonitor) {
streamUrlMonitor.notifyAll();
}
connecting = false;
if (response != null) {
int code = response.code();
String message = response.message();
LOG.error("MVLive websocket failure: {} {}", code, message, t);
response.close();
} else {
LOG.error("MVLive websocket failure", t);
}
MVLiveClient.this.ws = null;
}
@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
LOG.trace("Message: {}", text);
text = Optional.ofNullable(text).orElse("");
if (Objects.equal("o", text)) {
sendMessages(new Ping());
sendMessages(new GetBroadcastHealth(roomId, modelName, (m, r) -> {
LOG.trace("--> {}", m);
LOG.trace("<-- {}", r);
String addr = r.getJSONObject("body").optString("subscribeAddress");
sendMessages(new RegisterMessage(addr, (mr, rr) -> {
LOG.trace("--> {}", mr);
LOG.trace("<-- {}", rr);
masterPlaylist = rr.getJSONObject("body").optString("videoUrl");
LOG.trace("Got the URL: {}", masterPlaylist);
stop();
synchronized (streamUrlMonitor) {
streamUrlMonitor.notifyAll();
}
}));
}));
} else if (text.startsWith("a")) {
JSONArray jsonArray = new JSONArray(text.substring(1));
for (int i = 0; i < jsonArray.length(); i++) {
String respJson = jsonArray.getString(i);
JSONObject response = new JSONObject(respJson);
String address = response.optString("address");
if (isNotBlank(address)) {
Message message = futureResponses.get(address);
if (message != null) {
message.handleResponse(response);
if (!(message instanceof RegisterMessage)) {
futureResponses.remove(address);
}
}
}
}
}
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
super.onMessage(webSocket, bytes);
LOG.trace("Binary Message: {}", bytes.hex());
}
});
}
void sendMessages(Message... messages) {
JSONArray msgs = new JSONArray();
for (Message msg : messages) {
msgs.put(msg.toString());
if (msg instanceof SendMessage) {
SendMessage sendMessage = (SendMessage) msg;
futureResponses.put(sendMessage.getReplyAddress(), sendMessage);
} else if (msg instanceof RegisterMessage) {
RegisterMessage registerMessage = (RegisterMessage) msg;
futureResponses.put(registerMessage.getAddress(), registerMessage);
}
}
ws.send(msgs.toString());
}
public StreamLocation getStreamLocation(MVLiveModel model) throws IOException, InterruptedException {
start(model);
while (running) {
synchronized (streamUrlMonitor) {
streamUrlMonitor.wait(TimeUnit.SECONDS.toMillis(20000));
break;
}
}
if (ws != null) {
stop();
}
return new StreamLocation(roomId, roomNumber, masterPlaylist);
}
}

View File

@ -1,49 +0,0 @@
package ctbrec.sites.manyvids;
import java.io.IOException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.io.HttpClient;
import ctbrec.recorder.download.hls.HlsDownload;
public class MVLiveHlsDownload extends HlsDownload {
private static final Logger LOG = LoggerFactory.getLogger(MVLiveHlsDownload.class);
private transient ScheduledExecutorService scheduler;
public MVLiveHlsDownload(HttpClient client) {
super(client);
}
@Override
public MVLiveHlsDownload call() throws Exception {
try {
scheduler = new ScheduledThreadPoolExecutor(1, r -> {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("MVLive CF cookie updater");
t.setPriority(Thread.MIN_PRIORITY);
return t;
});
scheduler.scheduleAtFixedRate(this::updateCloudFlareCookies, 120, 120, TimeUnit.SECONDS);
updateCloudFlareCookies();
super.call();
} finally {
scheduler.shutdown();
}
return this;
}
private void updateCloudFlareCookies() {
try {
((MVLiveModel)getModel()).updateCloudFlareCookies();
} catch (IOException e) {
LOG.error("Couldn't update cloudflare cookies for model {}", getModel(), e);
}
}
}

View File

@ -1,45 +0,0 @@
package ctbrec.sites.manyvids;
import static ctbrec.io.HttpConstants.*;
import java.io.IOException;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import okhttp3.Request;
import okhttp3.Response;
public class MVLiveHttpClient extends HttpClient {
public MVLiveHttpClient(Config config) {
super("mvlive", config);
}
@Override
public boolean login() throws IOException {
return false;
}
public MVLiveHttpClient newSession() {
MVLiveHttpClient newClient = new MVLiveHttpClient(config);
newClient.client = newClient.client.newBuilder()
.cookieJar(createCookieJar())
.build();
newClient.reconfigure();
newClient.cookieJar.clear();
return newClient;
}
public void fetchAuthenticationCookies() throws IOException {
Request req = new Request.Builder()
.url("https://www.manyvids.com/tak-live-redirect.php")
.header(USER_AGENT, config.getSettings().httpUserAgent)
.build();
try (Response response = execute(req)) {
if (!response.isSuccessful()) {
throw new HttpException(response.code(), response.message());
}
}
}
}

View File

@ -1,50 +0,0 @@
package ctbrec.sites.manyvids;
import java.io.IOException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.io.HttpClient;
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
public class MVLiveMergedHlsDownload extends MergedFfmpegHlsDownload {
private static final Logger LOG = LoggerFactory.getLogger(MVLiveMergedHlsDownload.class);
private transient ScheduledExecutorService scheduler;
public MVLiveMergedHlsDownload(HttpClient client) {
super(client);
}
@Override
public MVLiveMergedHlsDownload call() throws Exception {
try {
scheduler = new ScheduledThreadPoolExecutor(1, r -> {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("MVLive CF cookie updater");
t.setPriority(Thread.MIN_PRIORITY);
return t;
});
scheduler.scheduleAtFixedRate(this::updateCloudFlareCookies, 2, 2, TimeUnit.MINUTES);
updateCloudFlareCookies();
super.call();
} finally {
scheduler.shutdown();
}
return this;
}
private void updateCloudFlareCookies() {
try {
((MVLiveModel)getModel()).updateCloudFlareCookies();
} catch (IOException e) {
LOG.error("Couldn't update cloudflare cookies for model {}", getModel(), e);
}
}
}

View File

@ -1,285 +0,0 @@
package ctbrec.sites.manyvids;
import com.iheartradio.m3u8.*;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.iheartradio.m3u8.data.StreamInfo;
import ctbrec.*;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource;
import ctbrec.sites.ModelOfflineException;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ExecutionException;
import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j
public class MVLiveModel extends AbstractModel {
private transient MVLiveHttpClient httpClient;
private transient MVLiveClient client;
private transient JSONObject roomLocation;
private transient Instant lastRoomLocationUpdate = Instant.EPOCH;
private String roomNumber;
@Getter
@Setter
private String id;
@Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
if (ignoreCache) {
String urlHandle = getDisplayName().toLowerCase().replace(" ", "-");
String url = "https://api.vidchat.manyvids.com/creator?urlHandle=" + URLEncoder.encode(urlHandle, UTF_8);
Request req = new Request.Builder()
.url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
try (Response response = getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = response.body().string();
JSONObject creator = new JSONObject(body);
updateStateFromJson(creator);
} else {
log.debug("{} URL: {}\n\tResponse: {}", response.code(), url, response.body().string());
throw new HttpException(response.code(), response.message());
}
}
}
return this.onlineState == ONLINE;
}
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
log.debug("Loading {}", getUrl());
try {
StreamLocation streamLocation = getClient().getStreamLocation(this);
log.debug("Got the stream location from WS {}", streamLocation.masterPlaylist);
roomNumber = streamLocation.roomNumber;
updateCloudFlareCookies();
MasterPlaylist masterPlaylist = getMasterPlaylist(streamLocation.masterPlaylist);
List<StreamSource> sources = new ArrayList<>();
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource();
src.setBandwidth(Optional.ofNullable(playlist.getStreamInfo()).map(StreamInfo::getBandwidth).orElse(0));
src.setHeight(Optional.ofNullable(playlist.getStreamInfo()).map(StreamInfo::getResolution).map(res -> res.height).orElse(0));
src.setWidth(Optional.ofNullable(playlist.getStreamInfo()).map(StreamInfo::getResolution).map(res -> res.width).orElse(0));
String masterUrl = streamLocation.masterPlaylist;
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
String segmentUri = baseUrl + playlist.getUri();
src.setMediaPlaylistUrl(segmentUri);
if (src.getMediaPlaylistUrl().contains("?")) {
src.setMediaPlaylistUrl((src.getMediaPlaylistUrl().substring(0, src.getMediaPlaylistUrl().lastIndexOf('?'))));
}
log.debug("Media playlist {}", src.getMediaPlaylistUrl());
sources.add(src);
}
}
return sources;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return Collections.emptyList();
}
private MasterPlaylist getMasterPlaylist(String url) throws IOException, ParseException, PlaylistException {
log.trace("Loading master playlist {}", url);
Request req = new Request.Builder()
.url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
try (Response response = getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = response.body().string();
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 {
log.debug("{} URL: {}\n\tResponse: {}", response.code(), url, response.body().string());
throw new HttpException(response.code(), response.message());
}
}
}
public void updateCloudFlareCookies() throws IOException {
String url = getApiUrl() + '/' + getRoomNumber() + "/player-settings/" + getDisplayName();
log.trace("Getting CF cookies: {}", url);
Request req = new Request.Builder()
.url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
try (Response response = getHttpClient().execute(req)) {
if (!response.isSuccessful()) {
log.debug("Loading CF cookies not successful: {}", response.body().string());
throw new HttpException(response.code(), response.message());
}
}
}
String getApiUrl() throws JSONException, IOException {
return getRoomLocation().getString("publicAPIURL");
}
public String getRoomNumber() throws IOException {
if (StringUtil.isBlank(roomNumber)) {
JSONObject json = getRoomLocation();
if (json.optBoolean("success")) {
roomNumber = json.getString("floorId");
} else {
log.debug("Room number response: {}", json.toString(2));
throw new ModelOfflineException(this);
}
}
return roomNumber;
}
private void fetchGeneralCookies() throws IOException {
Request req = new Request.Builder()
.url(getSite().getBaseUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
try (Response response = getHttpClient().execute(req)) {
if (!response.isSuccessful()) {
throw new HttpException(response.code(), response.message());
}
}
}
public JSONObject getRoomLocation() throws IOException {
if (Duration.between(lastRoomLocationUpdate, Instant.now()).getSeconds() > 60) {
fetchGeneralCookies();
httpClient.fetchAuthenticationCookies();
String url = "https://roompool.live.manyvids.com/roompool/" + getDisplayName() + "?private=false";
log.debug("Fetching room location from {}", url);
Request req = new Request.Builder()
.url(url)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(REFERER, MVLive.WS_ORIGIN + "/stream/" + getName())
.build();
try (Response response = getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = response.body().string();
roomLocation = new JSONObject(body);
log.debug("Room location response: {}", roomLocation);
lastRoomLocationUpdate = Instant.now();
return roomLocation;
} else {
throw new HttpException(response.code(), response.message());
}
}
} else {
return roomLocation;
}
}
private synchronized MVLiveClient getClient() {
if (client == null) {
client = new MVLiveClient(getHttpClient());
}
return client;
}
@Override
public void invalidateCacheEntries() {
roomNumber = null;
}
@Override
public void receiveTip(Double tokens) throws IOException {
throw new NotImplementedExcetion("Sending tips is not implemeted for MVLive");
}
@Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
return new int[]{1280, 720};
}
@Override
public boolean follow() throws IOException {
return false;
}
@Override
public boolean unfollow() throws IOException {
return false;
}
@Override
public RecordingProcess createDownload() {
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
return new MVLiveHlsDownload(getHttpClient());
} else {
return new MVLiveMergedHlsDownload(getHttpClient());
}
}
private synchronized MVLiveHttpClient getHttpClient() {
if (httpClient == null) {
MVLiveHttpClient siteHttpClient = (MVLiveHttpClient) getSite().getHttpClient();
httpClient = siteHttpClient.newSession();
}
return httpClient;
}
@Override
public void writeSiteSpecificData(Map<String, String> data) {
data.put("id", id);
}
@Override
public void readSiteSpecificData(Map<String, String> data) {
id = data.get("id");
}
public void updateStateFromJson(JSONObject creator) {
setId(creator.getString("id"));
setDisplayName(creator.optString("display_name", null));
setUrl(creator.getString("session_url"));
setOnlineState(mapState(creator.optString("live_status"), creator.optString("session_type")));
setPreview(creator.optString("avatar", null));
}
protected Model.State mapState(String liveStatus, String sessionType) {
if (Objects.equals("ONLINE", liveStatus)) {
switch (sessionType) {
case "PUBLIC" -> {
return ONLINE;
}
case "PRIVATE" -> {
return PRIVATE;
}
case "OFFLINE" -> {
return OFFLINE;
}
default -> {
log.debug("Unknown state {}", sessionType);
return OFFLINE;
}
}
} else {
return OFFLINE;
}
}
}

View File

@ -1,14 +0,0 @@
package ctbrec.sites.manyvids;
public class StreamLocation {
public String roomId;
public String roomNumber;
public String masterPlaylist;
public StreamLocation(String roomId, String roomNumber, String masterPlaylist) {
this.roomId = roomId;
this.roomNumber = roomNumber;
this.masterPlaylist = masterPlaylist;
}
}

View File

@ -1,32 +0,0 @@
package ctbrec.sites.manyvids.wsmsg;
import java.util.UUID;
import java.util.function.BiConsumer;
import org.json.JSONObject;
public class GetBroadcastHealth extends SendMessage {
private String roomId;
private String showId;
public GetBroadcastHealth(String roomId, String showId, BiConsumer<Message, JSONObject> responseConsumer) {
super(responseConsumer);
this.roomId = roomId;
this.showId = showId;
address = "api/StreamService";
action = "getBroadcastHealth";
}
@Override
public String toString() {
JSONObject msg = build();
JSONObject body = new JSONObject();
body.put("connectionId", "");
body.put("telemetryId", UUID.randomUUID().toString());
body.put("roomId", roomId);
body.put("showId", showId);
msg.put("body", body);
return msg.toString();
}
}

View File

@ -1,34 +0,0 @@
package ctbrec.sites.manyvids.wsmsg;
import java.util.UUID;
import java.util.function.BiConsumer;
import org.json.JSONObject;
public class JoinChat extends SendMessage {
private String roomId;
private String showId;
public JoinChat(String roomId, String showId, BiConsumer<Message, JSONObject> responseConsumer) {
super(responseConsumer);
this.roomId = roomId;
this.showId = showId;
address = "api/ChatService";
action = "join";
}
@Override
public String toString() {
JSONObject msg = build();
JSONObject body = new JSONObject();
body.put("connectionId", "");
body.put("telemetryId", UUID.randomUUID().toString());
body.put("roomId", roomId);
body.put("showId", showId);
body.put("joinPrivateShow", false);
body.put("deviceSourceType", "DESKTOP");
msg.put("body", body);
return msg.toString();
}
}

View File

@ -1,19 +0,0 @@
package ctbrec.sites.manyvids.wsmsg;
import java.util.Optional;
import java.util.function.BiConsumer;
import org.json.JSONObject;
public abstract class Message extends JSONObject {
private BiConsumer<Message, JSONObject> responseConsumer;
public Message(BiConsumer<Message, JSONObject> responseConsumer) {
this.responseConsumer = responseConsumer;
}
public void handleResponse(JSONObject response) {
Optional.ofNullable(responseConsumer).ifPresent(consumer -> consumer.accept(this, response));
}
}

View File

@ -1,9 +0,0 @@
package ctbrec.sites.manyvids.wsmsg;
public class Ping extends Message {
public Ping() {
super(null);
put("type", "ping");
}
}

View File

@ -1,28 +0,0 @@
package ctbrec.sites.manyvids.wsmsg;
import java.util.function.BiConsumer;
import org.json.JSONObject;
public class RegisterMessage extends Message {
protected String address;
public RegisterMessage(String address, BiConsumer<Message, JSONObject> responseConsumer) {
super(responseConsumer);
this.address = address;
}
public String getAddress() {
return address;
}
@Override
public String toString() {
JSONObject json = new JSONObject();
json.put("type", "register");
json.put("address", address);
json.put("headers", new JSONObject());
return json.toString();
}
}

View File

@ -1,7 +0,0 @@
package ctbrec.sites.manyvids.wsmsg;
import org.json.JSONObject;
public class Response extends JSONObject {
}

View File

@ -1,33 +0,0 @@
package ctbrec.sites.manyvids.wsmsg;
import java.util.UUID;
import java.util.function.BiConsumer;
import org.json.JSONObject;
public class SendMessage extends Message {
protected String address;
protected String action;
protected String replyAddress;
public SendMessage(BiConsumer<Message, JSONObject> responseConsumer) {
super(responseConsumer);
replyAddress = UUID.randomUUID().toString();
}
public String getReplyAddress() {
return replyAddress;
}
protected JSONObject build() {
JSONObject msg = new JSONObject();
msg.put("type", "send");
msg.put("address", address);
msg.put("replyAddress", replyAddress);
JSONObject headers = new JSONObject();
headers.put("action", action);
msg.put("headers", headers);
return msg;
}
}

View File

@ -1,153 +0,0 @@
package ctbrec.sites.secretfriends;
import ctbrec.Model;
import ctbrec.io.HtmlParser;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.sites.AbstractSite;
import okhttp3.Request;
import okhttp3.Response;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static ctbrec.ErrorMessages.HTTP_RESPONSE_BODY_IS_NULL;
import static ctbrec.io.HttpConstants.USER_AGENT;
public class SecretFriends extends AbstractSite {
private static final Logger LOG = LoggerFactory.getLogger(SecretFriends.class);
public static final String BASE_URI = "https://www.secretfriends.com";
private HttpClient httpClient;
@Override
public void init() throws IOException {
// nothing to do
}
@Override
public String getName() {
return "SecretFriends";
}
@Override
public String getBaseUrl() {
return BASE_URI;
}
@Override
public String getAffiliateLink() {
return BASE_URI;
}
@Override
public String getBuyTokensLink() {
return getAffiliateLink();
}
@Override
public SecretFriendsModel createModel(String name) {
SecretFriendsModel model = new SecretFriendsModel();
model.setName(name);
model.setUrl(getBaseUrl() + "/friends/" + name);
model.setSite(this);
return model;
}
@Override
public Double getTokenBalance() throws IOException {
return 0d;
}
@Override
public synchronized boolean login() throws IOException {
return false;
}
@Override
public HttpClient getHttpClient() {
if (httpClient == null) {
httpClient = new SecretFriendsHttpClient(getConfig());
}
return httpClient;
}
@Override
public void shutdown() {
if (httpClient != null) {
httpClient.shutdown();
}
}
@Override
public boolean supportsTips() {
return false;
}
@Override
public boolean supportsFollow() {
return false;
}
@Override
public boolean supportsSearch() {
return true;
}
@Override
public List<Model> search(String q) throws IOException, InterruptedException {
String url = BASE_URI + "/user?SearchForm[keyword]=" + URLEncoder.encode(q, "utf-8");
Request req = new Request.Builder()
.url(url)
.header(USER_AGENT, getConfig().getSettings().httpUserAgent)
.build();
try (Response response = getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
List<Model> models = new ArrayList<>();
Elements modelDivs = HtmlParser.getTags(body, "div[class~=model-wrapper]");
LOG.debug("Found {} models", modelDivs.size());
for (Element div : modelDivs) {
try {
models.add(SecretFriendsModelParser.parse(this, div));
} catch (Exception e) {
LOG.warn("Couldn't parse one of the models: {}", div.html(), e);
}
}
return models;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
@Override
public boolean isSiteForModel(Model m) {
return m instanceof SecretFriendsModel;
}
@Override
public boolean credentialsAvailable() {
return false;
}
@Override
public Model createModelFromUrl(String url) {
Matcher m = Pattern.compile("https?://(?:www\\.)?secretfriends.com/friends/([^/]*?)/?").matcher(url);
if (m.matches()) {
String modelName = m.group(1);
return createModel(modelName);
} else {
return super.createModelFromUrl(url);
}
}
}

View File

@ -1,174 +0,0 @@
package ctbrec.sites.secretfriends;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
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.IOException;
import static ctbrec.io.HttpConstants.*;
public class SecretFriendsHttpClient extends HttpClient {
private static final Logger LOG = LoggerFactory.getLogger(SecretFriendsHttpClient.class);
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private long userId;
private String csrfToken;
private String csrfTimestamp;
private String csrfNotifyTimestamp;
public SecretFriendsHttpClient(Config config) {
super("secretfirends", config);
}
@Override
public boolean login() throws IOException {
if (loggedIn) {
if (csrfToken == null) {
loadCsrfToken();
}
return true;
}
// persisted cookies might let us log in
if (checkLoginSuccess()) {
loggedIn = true;
LOG.debug("Logged in with cookies");
if (csrfToken == null) {
loadCsrfToken();
}
return true;
}
if (csrfToken == null) {
loadCsrfToken();
}
String url = SecretFriends.BASE_URI + "/api/front/auth/login";
JSONObject requestParams = new JSONObject();
requestParams.put("loginOrEmail", config.getSettings().stripchatUsername);
requestParams.put("password", config.getSettings().stripchatPassword);
requestParams.put("remember", true);
requestParams.put("csrfToken", csrfToken);
requestParams.put("csrfTimestamp", csrfTimestamp);
requestParams.put("csrfNotifyTimestamp", csrfNotifyTimestamp);
RequestBody body = RequestBody.Companion.create(requestParams.toString(), JSON);
Request request = new Request.Builder()
.url(url)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(USER_AGENT, config.getSettings().httpUserAgent)
.header(ORIGIN, SecretFriends.BASE_URI)
.header(REFERER, SecretFriends.BASE_URI)
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
.post(body)
.build();
try (Response response = execute(request)) {
if (response.isSuccessful()) {
JSONObject resp = new JSONObject(response.body().string());
if(resp.has("user")) {
JSONObject user = resp.getJSONObject("user");
userId = user.optLong("id");
return true;
} else {
return false;
}
} else {
LOG.info("Auto-Login failed: {} {} {}", response.code(), response.message(), url);
return false;
}
}
}
private void loadCsrfToken() throws IOException {
String url = SecretFriends.BASE_URI + "/api/front/v2/config/data?requestPath=%2F&timezoneOffset=0";
Request request = new Request.Builder()
.url(url)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(USER_AGENT, config.getSettings().httpUserAgent)
.header(ORIGIN, SecretFriends.BASE_URI)
.header(REFERER, SecretFriends.BASE_URI)
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
.build();
try (Response response = execute(request)) {
if (response.isSuccessful()) {
JSONObject resp = new JSONObject(response.body().string());
JSONObject data = resp.getJSONObject("data");
csrfToken = data.optString("csrfToken");
csrfTimestamp = data.optString("csrfTimestamp");
csrfNotifyTimestamp = data.optString("csrfNotifyTimestamp");
} else {
throw new HttpException(response.code(), response.message());
}
}
}
/**
* check, if the login worked
* @throws IOException
*/
public boolean checkLoginSuccess() throws IOException {
userId = getUserId();
String url = SecretFriends.BASE_URI + "/api/front/users/" + userId + "/favorites";
Request request = new Request.Builder()
.url(url)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(USER_AGENT, config.getSettings().httpUserAgent)
.header(ORIGIN, SecretFriends.BASE_URI)
.header(REFERER, SecretFriends.BASE_URI + "/favorites")
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
.build();
try (Response response = execute(request)) {
if (response.isSuccessful()) {
return true;
}
} catch (Exception e) {
LOG.info("Login check returned unsuccessful: {}", e.getLocalizedMessage());
}
return false;
}
public long getUserId() throws JSONException, IOException {
if (userId == 0) {
String url = SecretFriends.BASE_URI + "/api/front/users/username/" + config.getSettings().stripchatUsername;
Request request = new Request.Builder()
.url(url)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(USER_AGENT, config.getSettings().httpUserAgent)
.header(ORIGIN, SecretFriends.BASE_URI)
.header(REFERER, SecretFriends.BASE_URI)
.header(CONTENT_TYPE, MIMETYPE_APPLICATION_JSON)
.build();
try (Response response = execute(request)) {
if (response.isSuccessful()) {
JSONObject resp = new JSONObject(response.body().string());
JSONObject user = resp.getJSONObject("user");
userId = user.optLong("id");
} else {
throw new HttpException(url, response.code(), response.message());
}
}
}
return userId;
}
public String getCsrfNotifyTimestamp() {
return csrfNotifyTimestamp;
}
public String getCsrfTimestamp() {
return csrfTimestamp;
}
public String getCsrfToken() {
return csrfToken;
}
}

View File

@ -1,198 +0,0 @@
package ctbrec.sites.secretfriends;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.io.HtmlParser;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONObject;
import org.jsoup.nodes.Element;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static ctbrec.ErrorMessages.HTTP_RESPONSE_BODY_IS_NULL;
import static ctbrec.Model.State.ONLINE;
import static ctbrec.io.HttpConstants.*;
public class SecretFriendsModel extends AbstractModel {
private int[] resolution = new int[]{0, 0};
private static final Random RNG = new Random();
private static final String H5LIVE = "h5live";
private static final String SECURITY = "security";
@Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
if (ignoreCache) {
String url = SecretFriends.BASE_URI + "/friend/bio/" + getName();
Request req = new Request.Builder()
.url(url)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(REFERER, getUrl())
.build();
try (Response response = site.getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
Element wrapper = HtmlParser.getTag(body, "div[class~=model-wrapper]");
SecretFriendsModel parsedModel = SecretFriendsModelParser.parse((SecretFriends) getSite(), wrapper);
setName(parsedModel.getName());
setUrl(parsedModel.getUrl());
setPreview(parsedModel.getPreview());
setOnlineState(parsedModel.getOnlineState(true));
} else {
throw new HttpException(response.code(), response.message());
}
}
}
return onlineState == ONLINE;
}
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
String bioPage = loadBioPage();
String streamName = getStreamName(bioPage);
String streamId = getStreamId(bioPage);
JSONObject token = getToken(streamName);
String stream = streamName + "?host=www.secretfriends.com"
+ "&startAt=" + Instant.now().getEpochSecond()
+ "&userId=null&ip=0.0.0.0&cSessionId=guestKey"
+ "&streamId=" + streamId
+ "&groupId=null"
+ "&userAgent=" + Config.getInstance().getSettings().httpUserAgent;
HttpUrl wsUrl = new HttpUrl.Builder()
.scheme("https")
.host("bintu-splay.nanocosmos.de")
.addPathSegments("h5live/authstream")
.addQueryParameter("url", "rtmp://bintu-splay.nanocosmos.de/splay")
.addQueryParameter("stream", stream)
.addQueryParameter("cid", String.valueOf(RNG.nextInt(899000) + 100000))
.addQueryParameter("pid", String.valueOf(RNG.nextLong() + 10_000_000_000L))
.addQueryParameter("token", token.getJSONObject(H5LIVE).getJSONObject(SECURITY).getString("token"))
.addQueryParameter("expires", token.getJSONObject(H5LIVE).getJSONObject(SECURITY).getString("expires"))
.addQueryParameter("options", token.getJSONObject(H5LIVE).getJSONObject(SECURITY).getString("options"))
.build();
StreamSource src = new StreamSource();
src.setWidth(1280);
src.setHeight(720);
src.setMediaPlaylistUrl(wsUrl.toString());
return Collections.singletonList(src);
}
private String getStreamId(String bioPage) throws IOException {
Pattern p = Pattern.compile("app.configure\\((.*?)\\);");
Matcher m = p.matcher(bioPage);
if (m.find()) {
JSONObject appConfig = new JSONObject(m.group(1));
return appConfig.getJSONObject("page").getJSONObject("user").getString("id");
} else {
throw new IOException("app configuration not found in HTML");
}
}
private JSONObject getToken(String streamName) throws IOException {
String url = SecretFriends.BASE_URI + "/nano/generateToken?streamName=" + streamName;
Request req = new Request.Builder()
.url(url)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(REFERER, getUrl())
.build();
try (Response response = site.getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
return new JSONObject(body);
} else {
throw new HttpException(response.code(), response.message());
}
}
}
private String loadBioPage() throws IOException {
String url = SecretFriends.BASE_URI + "/friends/" + getName();
Request req = new Request.Builder()
.url(url)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(REFERER, getUrl())
.build();
try (Response response = site.getHttpClient().execute(req)) {
if (response.isSuccessful()) {
return Objects.requireNonNull(response.body(), HTTP_RESPONSE_BODY_IS_NULL).string();
} else {
throw new HttpException(response.code(), response.message());
}
}
}
private String getStreamName(String bioPage) throws IOException {
Pattern p = Pattern.compile("'streamName'\\s*:\\s*\"(.*?)\",");
Matcher m = p.matcher(bioPage);
if (m.find()) {
return m.group(1);
} else {
throw new IOException("Stream name not found in HTML");
}
}
@Override
public void invalidateCacheEntries() {
resolution = new int[]{0, 0};
}
@Override
public void receiveTip(Double tokens) throws IOException {
// not implemented
}
@Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
if (!failFast) {
try {
List<StreamSource> sources = getStreamSources();
if (!sources.isEmpty()) {
StreamSource best = sources.get(sources.size() - 1);
resolution = new int[]{best.getWidth(), best.getHeight()};
}
} catch (IOException | ParseException | PlaylistException | JAXBException e) {
throw new ExecutionException(e);
}
}
return resolution;
}
@Override
public boolean follow() throws IOException {
return false;
}
@Override
public boolean unfollow() throws IOException {
return false;
}
@Override
public RecordingProcess createDownload() {
return new SecretFriendsWebrtcDownload(getSite().getHttpClient());
}
}

View File

@ -1,75 +0,0 @@
package ctbrec.sites.secretfriends;
import ctbrec.Model;
import org.jsoup.nodes.Element;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SecretFriendsModelParser {
private SecretFriendsModelParser() {
}
public static SecretFriendsModel parse(SecretFriends site, Element modelWrapper) {
String name = parseName(modelWrapper);
SecretFriendsModel model = site.createModel(name);
model.setPreview(parsePreview(modelWrapper));
model.setOnlineState(extractOnlineState(modelWrapper));
return model;
}
private static String parsePreview(Element div) {
Element wrapper = div.selectFirst("div.placeholder-wrapper");
if (wrapper == null) {
return null;
}
String style = wrapper.attr("style");
Pattern p = Pattern.compile("background-image: url\\('(.*?)'\\)");
Matcher m = p.matcher(style);
if (m.find()) {
return m.group(1);
} else {
return null;
}
}
private static String parseName(Element div) {
Element bioLink = Objects.requireNonNull(div.selectFirst("a[href*=/friend]"), "a[href*=/friend] not found");
bioLink.setBaseUri(SecretFriends.BASE_URI);
String href = bioLink.attr("href");
if (href.contains("signup")) {
return href.substring(href.indexOf('=') + 1);
} else {
String name = href.substring(href.lastIndexOf('/') + 1);
if (name.indexOf('?') >= 0) {
name = name.substring(0, name.indexOf('?'));
}
if (name.indexOf('#') >= 0) {
name = name.substring(0, name.indexOf('#'));
}
return name;
}
}
private static Model.State extractOnlineState(Element div) {
Element modelTag = Objects.requireNonNull(div.selectFirst("div[class~=model-tag]"), "div.model-tag not found");
Set<String> cssClasses = modelTag.classNames();
for (String cssClass : cssClasses) {
switch (cssClass) {
case "model-online":
return Model.State.ONLINE;
case "model-private":
case "model-show":
case "model-vip":
return Model.State.PRIVATE;
case "model-offline":
return Model.State.OFFLINE;
default:
// keep going
}
}
return Model.State.OFFLINE;
}
}

View File

@ -1,227 +0,0 @@
package ctbrec.sites.secretfriends;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.HttpClient;
import ctbrec.recorder.download.AbstractDownload;
import ctbrec.recorder.download.RecordingProcess;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern;
import static ctbrec.io.HttpConstants.*;
public class SecretFriendsWebrtcDownload extends AbstractDownload {
private static final Logger LOG = LoggerFactory.getLogger(SecretFriendsWebrtcDownload.class);
private static final int MAX_SECONDS_WITHOUT_TRANSFER = 20;
private final HttpClient httpClient;
private WebSocket ws;
private FileOutputStream fout;
private Instant timeOfLastTransfer = Instant.MAX;
private volatile boolean running;
private volatile boolean started;
private File targetFile;
public SecretFriendsWebrtcDownload(HttpClient httpClient) {
this.httpClient = httpClient;
}
@Override
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
this.config = config;
this.model = model;
this.startTime = startTime;
this.downloadExecutor = executorService;
splittingStrategy = initSplittingStrategy(config.getSettings());
targetFile = config.getFileForRecording(model, "mp4", startTime);
timeOfLastTransfer = Instant.now();
}
@Override
public void stop() {
running = false;
if (ws != null) {
ws.close(1000, "");
ws = null;
}
}
@Override
public void finalizeDownload() {
if (fout != null) {
try {
LOG.debug("Closing recording file {}", targetFile);
fout.close();
} catch (IOException e) {
LOG.error("Error while closing recording file {}", targetFile, e);
}
}
}
@Override
public boolean isRunning() {
return running;
}
@Override
public void postProcess(Recording recording) {
// nothing to do
}
@Override
public File getTarget() {
return targetFile;
}
@Override
public String getPath(Model model) {
String absolutePath = targetFile.getAbsolutePath();
String recordingsDir = Config.getInstance().getSettings().recordingsDir;
String relativePath = absolutePath.replaceFirst(Pattern.quote(recordingsDir), "");
return relativePath;
}
@Override
public boolean isSingleFile() {
return true;
}
@Override
public long getSizeInByte() {
return getTarget().length();
}
@Override
public RecordingProcess call() throws Exception {
if (!started) {
started = true;
startDownload();
}
if (splittingStrategy.splitNecessary(this)) {
stop();
rescheduleTime = Instant.now();
} else {
rescheduleTime = Instant.now().plusSeconds(5);
}
if (!model.isOnline(true)) {
stop();
}
if (Duration.between(timeOfLastTransfer, Instant.now()).getSeconds() > MAX_SECONDS_WITHOUT_TRANSFER) {
LOG.info("No video data received for {} seconds. Stopping recording for model {}", MAX_SECONDS_WITHOUT_TRANSFER, model);
stop();
}
return this;
}
private void startDownload() throws IOException {
Request request;
try {
request = new Request.Builder()
.url(model.getStreamSources().get(0).getMediaPlaylistUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "pl")
.header(ORIGIN, model.getSite().getBaseUrl())
.build();
} catch (Exception e) {
throw new IOException(e);
}
running = true;
LOG.debug("Opening webrtc connection {}", request.url());
ws = httpClient.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
LOG.trace("onOpen {} {}", webSocket, response);
response.close();
try {
LOG.debug("Recording video stream to {}", targetFile);
Files.createDirectories(targetFile.getParentFile().toPath());
fout = new FileOutputStream(targetFile);
} catch (Exception e) {
LOG.error("Couldn't open file {} to save the video stream", targetFile, e);
stop();
}
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
super.onMessage(webSocket, bytes);
timeOfLastTransfer = Instant.now();
try {
byte[] videoData = bytes.toByteArray();
fout.write(videoData);
BandwidthMeter.add(videoData.length);
} catch (IOException e) {
if (running) {
LOG.error("Couldn't write video stream to file", e);
stop();
}
}
}
@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
LOG.trace("onMessageT {} {}", webSocket, text);
JSONObject msg = new JSONObject(text);
if (msg.optString("eventType").equals("onStreamInfo")) {
JSONObject streamInfo = msg.getJSONObject("onStreamInfo");
JSONObject videoInfo = streamInfo.getJSONObject("videoInfo");
LOG.info("Stream resolution for {} is {}", model, videoInfo.getInt("height"));
}
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
stop();
if (t instanceof EOFException) {
LOG.info("End of stream detected for model {}", model);
} else {
LOG.error("Websocket failure for model {} {} {}", model, response, t);
}
if (response != null) {
response.close();
}
}
@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
super.onClosing(webSocket, code, reason);
LOG.trace("Websocket closing for model {} {} {}", model, code, reason);
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
super.onClosed(webSocket, code, reason);
LOG.debug("Websocket closed for model {} {} {}", model, code, reason);
stop();
}
});
}
}

View File

@ -6,15 +6,15 @@ The configuration file stores all your settings and recorded models.
ctbrec application:
- Windows: `C:\Users\{your user name}\AppData\Roaming\ctbrec\settings.json`
- Linux: `~/.config/ctbrec/settings.json`
- macOS: `/Users/{your user name}/Library/Preferences/ctbrec/settings.json`
- Windows: `C:\Users\{your user name}\AppData\Roaming\ctbrec\<version>\settings.json`
- Linux: `~/.config/ctbrec/<version>/settings.json`
- macOS: `/Users/{your user name}/Library/Preferences/ctbrec/<version>/settings.json`
server:
- Windows: `C:\Users\{your user name}\AppData\Roaming\ctbrec\server.json`
- Linux: `~/.config/ctbrec/server.json`
- macOS: `/Users/{your user name}/Library/Preferences/ctbrec/server.json`
- Windows: `C:\Users\{your user name}\AppData\Roaming\ctbrec\<version>\server.json`
- Linux: `~/.config/ctbrec/<version>/server.json`
- macOS: `/Users/{your user name}/Library/Preferences/ctbrec/<version>/server.json`
##### Values
@ -100,7 +100,7 @@ ignored. This is a collection of the most interesting values:
- **timeoutRecordingEndingAt** - [00:00 - 23:59] - End of the recording timeout timeframe - No new recordings will be started in this period
- **useHlsdl** - [`true`,`false`] Use hlsdl to record the live streams. You also have to set `hlsdlExecutable`, if hlsdl is not globally available on your
system. hlsdl won't be used for MV Live, LiveJasmin and Showup.
system. hlsdl won't be used for Showup.
- **webinterface** (server only) - [`true`,`false`] Enables the webinterface for the server. You can access it with http://host:port/static/index.html Don't
activate this on

View File

@ -16,19 +16,14 @@ import ctbrec.servlet.MarkdownServlet;
import ctbrec.servlet.SearchServlet;
import ctbrec.servlet.StaticFileServlet;
import ctbrec.sites.Site;
import ctbrec.sites.amateurtv.AmateurTv;
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.dreamcam.Dreamcam;
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.streamray.Streamray;
@ -137,19 +132,14 @@ public class HttpServer {
}
private void createSites() {
sites.add(new AmateurTv());
sites.add(new BongaCams());
sites.add(new Cam4());
sites.add(new Camsoda());
sites.add(new Chaturbate());
sites.add(new CherryTv());
sites.add(new Dreamcam());
sites.add(new Fc2Live());
sites.add(new Flirt4Free());
sites.add(new LiveJasmin());
sites.add(new MVLive());
sites.add(new MyFreeCams());
sites.add(new SecretFriends());
sites.add(new Showup());
sites.add(new Streamate());
sites.add(new Stripchat());

View File

@ -306,7 +306,7 @@
});
} else {
$('#addModelByUrl').autocomplete({
source: ["AmateurTv:", "BongaCams:", "Cam4:", "Camsoda:", "Chaturbate:", "CherryTv:", "Dreamcam:", "Fc2Live:", "Flirt4Free:", "LiveJasmin:", "MVLive:", "MyFreeCams:", "SecretFriends:", "Showup:", "Streamate:", "Streamray:", "Stripchat:", "XloveCam:"]
source: ["BongaCams:", "Cam4:", "Camsoda:", "Chaturbate:", "Dreamcam:", "Fc2Live:", "Flirt4Free:", "MyFreeCams:", "Showup:", "Streamate:", "Streamray:", "Stripchat:", "XloveCam:"]
});
}
}