Remove non-working sites
This commit is contained in:
parent
c91b410307
commit
7bee404ec1
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
""";
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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, "");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package ctbrec.sites.manyvids.wsmsg;
|
||||
|
||||
public class Ping extends Message {
|
||||
|
||||
public Ping() {
|
||||
super(null);
|
||||
put("type", "ping");
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package ctbrec.sites.manyvids.wsmsg;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class Response extends JSONObject {
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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:"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue