Integrate DramCam, Streamray and WinkTv by @winkru
This commit is contained in:
parent
1c8d9bf678
commit
addbeab76e
|
@ -27,6 +27,7 @@ import ctbrec.sites.cam4.Cam4;
|
||||||
import ctbrec.sites.camsoda.Camsoda;
|
import ctbrec.sites.camsoda.Camsoda;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
import ctbrec.sites.chaturbate.Chaturbate;
|
||||||
import ctbrec.sites.cherrytv.CherryTv;
|
import ctbrec.sites.cherrytv.CherryTv;
|
||||||
|
import ctbrec.sites.dreamcam.Dreamcam;
|
||||||
import ctbrec.sites.fc2live.Fc2Live;
|
import ctbrec.sites.fc2live.Fc2Live;
|
||||||
import ctbrec.sites.flirt4free.Flirt4Free;
|
import ctbrec.sites.flirt4free.Flirt4Free;
|
||||||
import ctbrec.sites.jasmin.LiveJasmin;
|
import ctbrec.sites.jasmin.LiveJasmin;
|
||||||
|
@ -35,7 +36,9 @@ import ctbrec.sites.mfc.MyFreeCams;
|
||||||
import ctbrec.sites.secretfriends.SecretFriends;
|
import ctbrec.sites.secretfriends.SecretFriends;
|
||||||
import ctbrec.sites.showup.Showup;
|
import ctbrec.sites.showup.Showup;
|
||||||
import ctbrec.sites.streamate.Streamate;
|
import ctbrec.sites.streamate.Streamate;
|
||||||
|
import ctbrec.sites.streamray.Streamray;
|
||||||
import ctbrec.sites.stripchat.Stripchat;
|
import ctbrec.sites.stripchat.Stripchat;
|
||||||
|
import ctbrec.sites.winktv.WinkTv;
|
||||||
import ctbrec.sites.xlovecam.XloveCam;
|
import ctbrec.sites.xlovecam.XloveCam;
|
||||||
import ctbrec.ui.controls.Dialogs;
|
import ctbrec.ui.controls.Dialogs;
|
||||||
import ctbrec.ui.news.NewsTab;
|
import ctbrec.ui.news.NewsTab;
|
||||||
|
@ -123,7 +126,9 @@ public class CamrecApplication extends Application {
|
||||||
initSites();
|
initSites();
|
||||||
startOnlineMonitor();
|
startOnlineMonitor();
|
||||||
createGui(primaryStage);
|
createGui(primaryStage);
|
||||||
|
if (config.getSettings().checkForUpdates) {
|
||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
|
}
|
||||||
registerClipboardListener();
|
registerClipboardListener();
|
||||||
registerTrayIconListener();
|
registerTrayIconListener();
|
||||||
}
|
}
|
||||||
|
@ -162,6 +167,7 @@ public class CamrecApplication extends Application {
|
||||||
sites.add(new Camsoda());
|
sites.add(new Camsoda());
|
||||||
sites.add(new Chaturbate());
|
sites.add(new Chaturbate());
|
||||||
sites.add(new CherryTv());
|
sites.add(new CherryTv());
|
||||||
|
sites.add(new Dreamcam());
|
||||||
sites.add(new Fc2Live());
|
sites.add(new Fc2Live());
|
||||||
sites.add(new Flirt4Free());
|
sites.add(new Flirt4Free());
|
||||||
sites.add(new LiveJasmin());
|
sites.add(new LiveJasmin());
|
||||||
|
@ -171,6 +177,8 @@ public class CamrecApplication extends Application {
|
||||||
sites.add(new Showup());
|
sites.add(new Showup());
|
||||||
sites.add(new Streamate());
|
sites.add(new Streamate());
|
||||||
sites.add(new Stripchat());
|
sites.add(new Stripchat());
|
||||||
|
sites.add(new Streamray());
|
||||||
|
sites.add(new WinkTv());
|
||||||
sites.add(new XloveCam());
|
sites.add(new XloveCam());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import ctbrec.sites.cam4.Cam4;
|
||||||
import ctbrec.sites.camsoda.Camsoda;
|
import ctbrec.sites.camsoda.Camsoda;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
import ctbrec.sites.chaturbate.Chaturbate;
|
||||||
import ctbrec.sites.cherrytv.CherryTv;
|
import ctbrec.sites.cherrytv.CherryTv;
|
||||||
|
import ctbrec.sites.dreamcam.Dreamcam;
|
||||||
import ctbrec.sites.fc2live.Fc2Live;
|
import ctbrec.sites.fc2live.Fc2Live;
|
||||||
import ctbrec.sites.flirt4free.Flirt4Free;
|
import ctbrec.sites.flirt4free.Flirt4Free;
|
||||||
import ctbrec.sites.jasmin.LiveJasmin;
|
import ctbrec.sites.jasmin.LiveJasmin;
|
||||||
|
@ -15,7 +16,9 @@ import ctbrec.sites.mfc.MyFreeCams;
|
||||||
import ctbrec.sites.secretfriends.SecretFriends;
|
import ctbrec.sites.secretfriends.SecretFriends;
|
||||||
import ctbrec.sites.showup.Showup;
|
import ctbrec.sites.showup.Showup;
|
||||||
import ctbrec.sites.streamate.Streamate;
|
import ctbrec.sites.streamate.Streamate;
|
||||||
|
import ctbrec.sites.streamray.Streamray;
|
||||||
import ctbrec.sites.stripchat.Stripchat;
|
import ctbrec.sites.stripchat.Stripchat;
|
||||||
|
import ctbrec.sites.winktv.WinkTv;
|
||||||
import ctbrec.sites.xlovecam.XloveCam;
|
import ctbrec.sites.xlovecam.XloveCam;
|
||||||
import ctbrec.ui.sites.amateurtv.AmateurTvSiteUi;
|
import ctbrec.ui.sites.amateurtv.AmateurTvSiteUi;
|
||||||
import ctbrec.ui.sites.bonga.BongaCamsSiteUi;
|
import ctbrec.ui.sites.bonga.BongaCamsSiteUi;
|
||||||
|
@ -23,6 +26,7 @@ import ctbrec.ui.sites.cam4.Cam4SiteUi;
|
||||||
import ctbrec.ui.sites.camsoda.CamsodaSiteUi;
|
import ctbrec.ui.sites.camsoda.CamsodaSiteUi;
|
||||||
import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi;
|
import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi;
|
||||||
import ctbrec.ui.sites.cherrytv.CherryTvSiteUi;
|
import ctbrec.ui.sites.cherrytv.CherryTvSiteUi;
|
||||||
|
import ctbrec.ui.sites.dreamcam.DreamcamSiteUi;
|
||||||
import ctbrec.ui.sites.fc2live.Fc2LiveSiteUi;
|
import ctbrec.ui.sites.fc2live.Fc2LiveSiteUi;
|
||||||
import ctbrec.ui.sites.flirt4free.Flirt4FreeSiteUi;
|
import ctbrec.ui.sites.flirt4free.Flirt4FreeSiteUi;
|
||||||
import ctbrec.ui.sites.jasmin.LiveJasminSiteUi;
|
import ctbrec.ui.sites.jasmin.LiveJasminSiteUi;
|
||||||
|
@ -31,7 +35,9 @@ import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi;
|
||||||
import ctbrec.ui.sites.secretfriends.SecretFriendsSiteUi;
|
import ctbrec.ui.sites.secretfriends.SecretFriendsSiteUi;
|
||||||
import ctbrec.ui.sites.showup.ShowupSiteUi;
|
import ctbrec.ui.sites.showup.ShowupSiteUi;
|
||||||
import ctbrec.ui.sites.streamate.StreamateSiteUi;
|
import ctbrec.ui.sites.streamate.StreamateSiteUi;
|
||||||
|
import ctbrec.ui.sites.streamray.StreamraySiteUi;
|
||||||
import ctbrec.ui.sites.stripchat.StripchatSiteUi;
|
import ctbrec.ui.sites.stripchat.StripchatSiteUi;
|
||||||
|
import ctbrec.ui.sites.winktv.WinkTvSiteUi;
|
||||||
import ctbrec.ui.sites.xlovecam.XloveCamSiteUi;
|
import ctbrec.ui.sites.xlovecam.XloveCamSiteUi;
|
||||||
|
|
||||||
public class SiteUiFactory {
|
public class SiteUiFactory {
|
||||||
|
@ -52,8 +58,12 @@ public class SiteUiFactory {
|
||||||
private static StreamateSiteUi streamateSiteUi;
|
private static StreamateSiteUi streamateSiteUi;
|
||||||
private static StripchatSiteUi stripchatSiteUi;
|
private static StripchatSiteUi stripchatSiteUi;
|
||||||
private static XloveCamSiteUi xloveCamSiteUi;
|
private static XloveCamSiteUi xloveCamSiteUi;
|
||||||
|
private static StreamraySiteUi streamraySiteUi;
|
||||||
|
private static WinkTvSiteUi winkTvSiteUi;
|
||||||
|
private static DreamcamSiteUi dreamcamSiteUi;
|
||||||
|
|
||||||
private SiteUiFactory () {}
|
private SiteUiFactory() {
|
||||||
|
}
|
||||||
|
|
||||||
public static synchronized SiteUI getUi(Site site) { // NOSONAR
|
public static synchronized SiteUI getUi(Site site) { // NOSONAR
|
||||||
if (site instanceof AmateurTv) {
|
if (site instanceof AmateurTv) {
|
||||||
|
@ -86,6 +96,11 @@ public class SiteUiFactory {
|
||||||
cherryTvSiteUi = new CherryTvSiteUi((CherryTv) site);
|
cherryTvSiteUi = new CherryTvSiteUi((CherryTv) site);
|
||||||
}
|
}
|
||||||
return cherryTvSiteUi;
|
return cherryTvSiteUi;
|
||||||
|
} else if (site instanceof Dreamcam) {
|
||||||
|
if (dreamcamSiteUi == null) {
|
||||||
|
dreamcamSiteUi = new DreamcamSiteUi((Dreamcam) site);
|
||||||
|
}
|
||||||
|
return dreamcamSiteUi;
|
||||||
} else if (site instanceof Fc2Live) {
|
} else if (site instanceof Fc2Live) {
|
||||||
if (fc2SiteUi == null) {
|
if (fc2SiteUi == null) {
|
||||||
fc2SiteUi = new Fc2LiveSiteUi((Fc2Live) site);
|
fc2SiteUi = new Fc2LiveSiteUi((Fc2Live) site);
|
||||||
|
@ -131,6 +146,16 @@ public class SiteUiFactory {
|
||||||
stripchatSiteUi = new StripchatSiteUi((Stripchat) site);
|
stripchatSiteUi = new StripchatSiteUi((Stripchat) site);
|
||||||
}
|
}
|
||||||
return stripchatSiteUi;
|
return stripchatSiteUi;
|
||||||
|
} else if (site instanceof Streamray) {
|
||||||
|
if (streamraySiteUi == null) {
|
||||||
|
streamraySiteUi = new StreamraySiteUi((Streamray) site);
|
||||||
|
}
|
||||||
|
return streamraySiteUi;
|
||||||
|
} else if (site instanceof WinkTv) {
|
||||||
|
if (winkTvSiteUi == null) {
|
||||||
|
winkTvSiteUi = new WinkTvSiteUi((WinkTv) site);
|
||||||
|
}
|
||||||
|
return winkTvSiteUi;
|
||||||
} else if (site instanceof XloveCam) {
|
} else if (site instanceof XloveCam) {
|
||||||
if (xloveCamSiteUi == null) {
|
if (xloveCamSiteUi == null) {
|
||||||
xloveCamSiteUi = new XloveCamSiteUi((XloveCam) site);
|
xloveCamSiteUi = new XloveCamSiteUi((XloveCam) site);
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package ctbrec.ui.sites.dreamcam;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.sites.dreamcam.Dreamcam;
|
||||||
|
import ctbrec.ui.settings.SettingsTab;
|
||||||
|
import ctbrec.ui.sites.AbstractConfigUI;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.RadioButton;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
public class DreamcamConfigUI extends AbstractConfigUI {
|
||||||
|
private final Dreamcam site;
|
||||||
|
|
||||||
|
public DreamcamConfigUI(Dreamcam 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++);
|
||||||
|
|
||||||
|
row++;
|
||||||
|
l = new Label("VR Mode");
|
||||||
|
layout.add(l, 0, row);
|
||||||
|
var vr = new CheckBox();
|
||||||
|
vr.setSelected(settings.dreamcamVR);
|
||||||
|
vr.setOnAction(e -> {
|
||||||
|
settings.dreamcamVR = vr.isSelected();
|
||||||
|
save();
|
||||||
|
});
|
||||||
|
GridPane.setMargin(vr, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
layout.add(vr, 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(deleteCookies, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package ctbrec.ui.sites.dreamcam;
|
||||||
|
|
||||||
|
import ctbrec.sites.dreamcam.Dreamcam;
|
||||||
|
import ctbrec.ui.sites.AbstractSiteUi;
|
||||||
|
import ctbrec.ui.sites.ConfigUI;
|
||||||
|
import ctbrec.ui.tabs.TabProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class DreamcamSiteUi extends AbstractSiteUi {
|
||||||
|
|
||||||
|
private DreamcamTabProvider tabProvider;
|
||||||
|
private DreamcamConfigUI configUi;
|
||||||
|
private final Dreamcam site;
|
||||||
|
|
||||||
|
public DreamcamSiteUi(Dreamcam site) {
|
||||||
|
this.site = site;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TabProvider getTabProvider() {
|
||||||
|
if (tabProvider == null) {
|
||||||
|
tabProvider = new DreamcamTabProvider(site);
|
||||||
|
}
|
||||||
|
return tabProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConfigUI getConfigUI() {
|
||||||
|
if (configUi == null) {
|
||||||
|
configUi = new DreamcamConfigUI(site);
|
||||||
|
}
|
||||||
|
return configUi;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean login() throws IOException {
|
||||||
|
return site.login();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package ctbrec.ui.sites.dreamcam;
|
||||||
|
|
||||||
|
import ctbrec.sites.dreamcam.Dreamcam;
|
||||||
|
import ctbrec.sites.dreamcam.DreamcamModel;
|
||||||
|
import ctbrec.ui.sites.AbstractTabProvider;
|
||||||
|
import ctbrec.ui.tabs.ThumbOverviewTab;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Tab;
|
||||||
|
|
||||||
|
public class DreamcamTabProvider extends AbstractTabProvider {
|
||||||
|
private final static String API_URL = "https://bss.dreamcamtrue.com/api/clients/v1/broadcasts?partnerId=dreamcam_oauth2&show-offline=false&stream-types=video2D&include-tags=false&include-tip-menu=false";
|
||||||
|
|
||||||
|
public DreamcamTabProvider(Dreamcam site) {
|
||||||
|
super(site);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Tab> getSiteTabs(Scene scene) {
|
||||||
|
List<Tab> tabs = new ArrayList<>();
|
||||||
|
tabs.add(createTab("Girls", API_URL + "&tag-categories=girls"));
|
||||||
|
tabs.add(createTab("Boys", API_URL + "&tag-categories=men"));
|
||||||
|
tabs.add(createTab("Couples", API_URL + "&tag-categories=couples"));
|
||||||
|
tabs.add(createTab("Trans", API_URL + "&tag-categories=trans"));
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tab createTab(String title, String url) {
|
||||||
|
var updateService = new DreamcamUpdateService((Dreamcam) site, url);
|
||||||
|
var tab = new ThumbOverviewTab(title, updateService, site);
|
||||||
|
tab.setImageAspectRatio(10.0 / 16.0);
|
||||||
|
tab.setRecorder(recorder);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package ctbrec.ui.sites.dreamcam;
|
||||||
|
|
||||||
|
import static ctbrec.io.HttpConstants.*;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.sites.dreamcam.Dreamcam;
|
||||||
|
import ctbrec.sites.dreamcam.DreamcamModel;
|
||||||
|
import ctbrec.ui.tabs.PaginatedScheduledService;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class DreamcamUpdateService extends PaginatedScheduledService {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(DreamcamUpdateService.class);
|
||||||
|
private static final String API_URL = "https://api.dreamcam.co.kr/v1/live";
|
||||||
|
private static final int modelsPerPage = 64;
|
||||||
|
private Dreamcam site;
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
public DreamcamUpdateService(Dreamcam 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 {
|
||||||
|
return loadModelList();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Model> loadModelList() throws IOException {
|
||||||
|
int offset = (getPage() - 1) * modelsPerPage;
|
||||||
|
int limit = modelsPerPage;
|
||||||
|
String paginatedUrl = url + "&offset=" + offset + "&limit=" + limit;
|
||||||
|
LOG.debug("Fetching page {}", paginatedUrl);
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(paginatedUrl)
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
|
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
||||||
|
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
|
||||||
|
.header(REFERER, site.getBaseUrl() + "/")
|
||||||
|
.header(ORIGIN, site.getBaseUrl())
|
||||||
|
.build();
|
||||||
|
try (Response response = site.getHttpClient().execute(req)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
List<Model> models = new ArrayList<>();
|
||||||
|
String content = response.body().string();
|
||||||
|
JSONObject json = new JSONObject(content);
|
||||||
|
if (json.has("pageItems")) {
|
||||||
|
JSONArray modelNodes = json.getJSONArray("pageItems");
|
||||||
|
parseModels(modelNodes, models);
|
||||||
|
}
|
||||||
|
return models;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseModels(JSONArray jsonModels, List<Model> models) {
|
||||||
|
for (int i = 0; i < jsonModels.length(); i++) {
|
||||||
|
JSONObject m = jsonModels.getJSONObject(i);
|
||||||
|
String name = m.optString("modelNickname");
|
||||||
|
DreamcamModel model = (DreamcamModel) site.createModel(name);
|
||||||
|
model.setDisplayName(name);
|
||||||
|
model.setPreview(m.optString("modelProfilePhotoUrl"));
|
||||||
|
model.setDescription(m.optString("broadcastTextStatus"));
|
||||||
|
models.add(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package ctbrec.ui.sites.streamray;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.sites.streamray.Streamray;
|
||||||
|
import ctbrec.ui.settings.SettingsTab;
|
||||||
|
import ctbrec.ui.sites.AbstractConfigUI;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.RadioButton;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
public class StreamrayConfigUI extends AbstractConfigUI {
|
||||||
|
private final Streamray site;
|
||||||
|
|
||||||
|
public StreamrayConfigUI(Streamray 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++);
|
||||||
|
row++;
|
||||||
|
|
||||||
|
l = new Label("Record Goal/Tipping shows");
|
||||||
|
layout.add(l, 0, row);
|
||||||
|
var cb = new CheckBox();
|
||||||
|
cb.setSelected(settings.streamrayRecordGoalShows);
|
||||||
|
cb.setOnAction(e -> {
|
||||||
|
settings.streamrayRecordGoalShows = cb.isSelected();
|
||||||
|
save();
|
||||||
|
});
|
||||||
|
GridPane.setMargin(cb, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
layout.add(cb, 1, row++);
|
||||||
|
row++;
|
||||||
|
|
||||||
|
var deleteCookies = new Button("Delete Cookies");
|
||||||
|
deleteCookies.setOnAction(e -> site.getHttpClient().clearCookies());
|
||||||
|
layout.add(deleteCookies, 1, row);
|
||||||
|
GridPane.setColumnSpan(deleteCookies, 2);
|
||||||
|
|
||||||
|
GridPane.setMargin(deleteCookies, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
package ctbrec.ui.sites.streamray;
|
||||||
|
|
||||||
|
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.streamray.Streamray;
|
||||||
|
import ctbrec.ui.ExternalBrowser;
|
||||||
|
import okhttp3.Cookie;
|
||||||
|
import okhttp3.Cookie.Builder;
|
||||||
|
import okhttp3.CookieJar;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
|
||||||
|
public class StreamrayElectronLoginDialog {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(StreamrayElectronLoginDialog.class);
|
||||||
|
public static final String DOMAIN = "streamray.com";
|
||||||
|
public static final String URL = "https://streamray.com/";
|
||||||
|
private CookieJar cookieJar;
|
||||||
|
private ExternalBrowser browser;
|
||||||
|
private boolean firstCall = true;
|
||||||
|
private final static Streamray site = new Streamray();
|
||||||
|
|
||||||
|
public StreamrayElectronLoginDialog(CookieJar cookieJar) throws IOException {
|
||||||
|
this.cookieJar = cookieJar;
|
||||||
|
browser = ExternalBrowser.getInstance();
|
||||||
|
try {
|
||||||
|
var config = new JSONObject();
|
||||||
|
config.put("url", URL);
|
||||||
|
config.put("w", 800);
|
||||||
|
config.put("h", 600);
|
||||||
|
config.put("userAgent", Config.getInstance().getSettings().httpUserAgent);
|
||||||
|
var msg = new JSONObject();
|
||||||
|
msg.put("config", config);
|
||||||
|
browser.run(msg, msgHandler);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IOException("Couldn't wait for login dialog", e);
|
||||||
|
} finally {
|
||||||
|
browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Consumer<String> msgHandler = line -> {
|
||||||
|
if (!line.startsWith("{")) return;
|
||||||
|
JSONObject json = new JSONObject(line);
|
||||||
|
boolean loginCookie = false;
|
||||||
|
if (json.has("cookies")) {
|
||||||
|
var cookies = json.getJSONArray("cookies");
|
||||||
|
for (var i = 0; i < cookies.length(); i++) {
|
||||||
|
var cookie = cookies.getJSONObject(i);
|
||||||
|
if (cookie.getString("domain").contains(DOMAIN)) {
|
||||||
|
if (cookie.optString("name").equals("memberToken")) {
|
||||||
|
loginCookie = true;
|
||||||
|
}
|
||||||
|
Builder b = new Cookie.Builder()
|
||||||
|
.path(cookie.getString("path"))
|
||||||
|
.domain(DOMAIN)
|
||||||
|
.name(cookie.optString("name"))
|
||||||
|
.value(cookie.optString("value"))
|
||||||
|
.expiresAt(Double.valueOf(cookie.optDouble("expirationDate")).longValue()); // NOSONAR
|
||||||
|
if (cookie.optBoolean("hostOnly")) {
|
||||||
|
b.hostOnlyDomain(DOMAIN);
|
||||||
|
}
|
||||||
|
if (cookie.optBoolean("httpOnly")) {
|
||||||
|
b.httpOnly();
|
||||||
|
}
|
||||||
|
if (cookie.optBoolean("secure")) {
|
||||||
|
b.secure();
|
||||||
|
}
|
||||||
|
Cookie c = b.build();
|
||||||
|
LOG.trace("Adding cookie {}={}", c.name(), c.value());
|
||||||
|
cookieJar.saveFromResponse(HttpUrl.parse(URL), Collections.singletonList(c));
|
||||||
|
} // if
|
||||||
|
} // for
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
package ctbrec.ui.sites.streamray;
|
||||||
|
|
||||||
|
import static ctbrec.io.HttpConstants.*;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.StringUtil;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.sites.streamray.*;
|
||||||
|
import ctbrec.ui.SiteUiFactory;
|
||||||
|
import ctbrec.ui.tabs.PaginatedScheduledService;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import okhttp3.*;
|
||||||
|
import org.json.*;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class StreamrayFavoritesService extends PaginatedScheduledService {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(StreamrayFavoritesService.class);
|
||||||
|
private static final String API_URL = "https://beta-api.cams.com/won/compressed/";
|
||||||
|
|
||||||
|
private Streamray site;
|
||||||
|
private static List<StreamrayModel> modelsList;
|
||||||
|
private static JSONArray mapping;
|
||||||
|
protected int modelsPerPage = 48;
|
||||||
|
public boolean loggedIn = false;
|
||||||
|
|
||||||
|
public StreamrayFavoritesService(Streamray site) {
|
||||||
|
this.site = site;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<StreamrayModel> getModelList() throws IOException {
|
||||||
|
modelsList = loadModelList();
|
||||||
|
if (modelsList == null) {
|
||||||
|
modelsList = Collections.emptyList();
|
||||||
|
}
|
||||||
|
return modelsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<StreamrayModel> loadModelList() throws IOException {
|
||||||
|
LOG.debug("Fetching page {}", API_URL);
|
||||||
|
StreamrayHttpClient client = (StreamrayHttpClient) site.getHttpClient();
|
||||||
|
String token = "";
|
||||||
|
if (site.login()) {
|
||||||
|
loggedIn = true;
|
||||||
|
token = client.getUserToken();
|
||||||
|
} else {
|
||||||
|
loggedIn = false;
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(API_URL)
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
|
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
||||||
|
.header(REFERER, site.getBaseUrl() + "/")
|
||||||
|
.header(ORIGIN, site.getBaseUrl())
|
||||||
|
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
|
||||||
|
.header(AUTHORIZATION, "Bearer " + token)
|
||||||
|
.build();
|
||||||
|
try (Response response = client.execute(req)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
List<StreamrayModel> models = new ArrayList<>();
|
||||||
|
String content = response.body().string();
|
||||||
|
JSONObject json = new JSONObject(content);
|
||||||
|
JSONArray modelNodes = json.getJSONArray("models");
|
||||||
|
mapping = json.getJSONArray("mapping");
|
||||||
|
parseModels(modelNodes, models);
|
||||||
|
return models;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseModels(JSONArray jsonModels, List<StreamrayModel> models) {
|
||||||
|
int name_idx = mapping_index("stream_name");
|
||||||
|
int fav_idx = mapping_index("is_favorite");
|
||||||
|
for (int i = 0; i < jsonModels.length(); i++) {
|
||||||
|
JSONArray m = jsonModels.getJSONArray(i);
|
||||||
|
String name = m.optString(name_idx);
|
||||||
|
boolean favorite = m.optBoolean(fav_idx);
|
||||||
|
if (favorite) {
|
||||||
|
StreamrayModel model = (StreamrayModel) site.createModel(name);
|
||||||
|
String preview = getPreviewURL(name);
|
||||||
|
model.setPreview(preview);
|
||||||
|
models.add(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPreviewURL(String name) {
|
||||||
|
String lname = name.toLowerCase();
|
||||||
|
String url = MessageFormat.format("https://images4.streamray.com/images/streamray/won/jpg/{0}/{1}/{2}_640.jpg", lname.substring(0,1), lname.substring(lname.length()-1), lname);
|
||||||
|
try {
|
||||||
|
return MessageFormat.format("https://dynimages.securedataimages.com/unsigned/rs:fill:640::0/g:no/plain/{0}@jpg", URLEncoder.encode(url, "utf-8"));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int mapping_index(String s) {
|
||||||
|
for (var i = 0; i < mapping.length(); i++) {
|
||||||
|
if (Objects.equals(s, mapping.get(i))) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package ctbrec.ui.sites.streamray;
|
||||||
|
|
||||||
|
import ctbrec.sites.streamray.Streamray;
|
||||||
|
import ctbrec.ui.tabs.FollowedTab;
|
||||||
|
import ctbrec.ui.tabs.ThumbOverviewTab;
|
||||||
|
import javafx.concurrent.WorkerStateEvent;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class StreamrayFavoritesTab extends ThumbOverviewTab implements FollowedTab {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(StreamrayFavoritesTab.class);
|
||||||
|
private Label status;
|
||||||
|
private Button loginButton;
|
||||||
|
private Streamray site;
|
||||||
|
private StreamrayFavoritesService updateService;
|
||||||
|
|
||||||
|
public StreamrayFavoritesTab(String title, StreamrayFavoritesService updateService, Streamray site) {
|
||||||
|
super(title, updateService, site);
|
||||||
|
this.site = site;
|
||||||
|
this.updateService = updateService;
|
||||||
|
|
||||||
|
status = new Label("Logging in...");
|
||||||
|
grid.getChildren().addAll(status);
|
||||||
|
|
||||||
|
loginButton = new Button("Login");
|
||||||
|
loginButton.setPadding(new Insets(20));
|
||||||
|
loginButton.setOnAction(e -> {
|
||||||
|
try {
|
||||||
|
new StreamrayElectronLoginDialog(site.getHttpClient().getCookieJar());
|
||||||
|
updateService.restart();
|
||||||
|
} catch (Exception ex) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addLoginButton() {
|
||||||
|
grid.getChildren().clear();
|
||||||
|
grid.setAlignment(Pos.CENTER);
|
||||||
|
grid.getChildren().add(loginButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSuccess() {
|
||||||
|
grid.getChildren().removeAll(status, loginButton);
|
||||||
|
grid.setAlignment(Pos.TOP_LEFT);
|
||||||
|
if (updateService.loggedIn == false) {
|
||||||
|
addLoginButton();
|
||||||
|
} else {
|
||||||
|
super.onSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onFail(WorkerStateEvent event) {
|
||||||
|
status.setText("Login failed");
|
||||||
|
super.onFail(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void selected() {
|
||||||
|
status.setText("Logging in...");
|
||||||
|
super.selected();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScene(Scene scene) {
|
||||||
|
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||||
|
if (this.isSelected() && event.getCode() == KeyCode.DELETE) {
|
||||||
|
follow(selectedThumbCells, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package ctbrec.ui.sites.streamray;
|
||||||
|
|
||||||
|
import ctbrec.sites.streamray.Streamray;
|
||||||
|
import ctbrec.ui.sites.AbstractSiteUi;
|
||||||
|
import ctbrec.ui.sites.ConfigUI;
|
||||||
|
import ctbrec.ui.tabs.TabProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class StreamraySiteUi extends AbstractSiteUi {
|
||||||
|
|
||||||
|
private StreamrayTabProvider tabProvider;
|
||||||
|
private StreamrayConfigUI configUi;
|
||||||
|
private final Streamray site;
|
||||||
|
|
||||||
|
public StreamraySiteUi(Streamray site) {
|
||||||
|
this.site = site;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TabProvider getTabProvider() {
|
||||||
|
if (tabProvider == null) {
|
||||||
|
tabProvider = new StreamrayTabProvider(site);
|
||||||
|
}
|
||||||
|
return tabProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConfigUI getConfigUI() {
|
||||||
|
if (configUi == null) {
|
||||||
|
configUi = new StreamrayConfigUI(site);
|
||||||
|
}
|
||||||
|
return configUi;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean login() throws IOException {
|
||||||
|
return site.login();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean checkLogin() throws IOException {
|
||||||
|
return site.login();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package ctbrec.ui.sites.streamray;
|
||||||
|
|
||||||
|
import ctbrec.sites.streamray.Streamray;
|
||||||
|
import ctbrec.sites.streamray.StreamrayModel;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public class StreamrayTabProvider extends AbstractTabProvider {
|
||||||
|
|
||||||
|
public StreamrayTabProvider(Streamray site) {
|
||||||
|
super(site);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Tab> getSiteTabs(Scene scene) {
|
||||||
|
List<Tab> tabs = new ArrayList<>();
|
||||||
|
tabs.add(createTab("Girls", m -> Objects.equals("F", m.getGender())));
|
||||||
|
tabs.add(createTab("Boys", m -> Objects.equals("M", m.getGender())));
|
||||||
|
tabs.add(createTab("Trans", m -> Objects.equals("TS", m.getGender())));
|
||||||
|
tabs.add(createTab("New", m -> m.isNew()));
|
||||||
|
tabs.add(favoritesTab());
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tab createTab(String title, Predicate<StreamrayModel> filter) {
|
||||||
|
var updateService = new StreamrayUpdateService((Streamray) site, filter);
|
||||||
|
var tab = new ThumbOverviewTab(title, updateService, site);
|
||||||
|
tab.setImageAspectRatio(9.0 / 16.0);
|
||||||
|
tab.setRecorder(recorder);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tab favoritesTab() {
|
||||||
|
var updateService = new StreamrayFavoritesService((Streamray) site);
|
||||||
|
var tab = new StreamrayFavoritesTab("Favorites", updateService, (Streamray) site);
|
||||||
|
tab.setImageAspectRatio(9.0 / 16.0);
|
||||||
|
tab.setRecorder(recorder);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
package ctbrec.ui.sites.streamray;
|
||||||
|
|
||||||
|
import static ctbrec.io.HttpConstants.*;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.StringUtil;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.sites.streamray.*;
|
||||||
|
import ctbrec.ui.SiteUiFactory;
|
||||||
|
import ctbrec.ui.tabs.PaginatedScheduledService;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class StreamrayUpdateService extends PaginatedScheduledService {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(StreamrayUpdateService.class);
|
||||||
|
private static final String API_URL = "https://beta-api.cams.com/won/compressed/";
|
||||||
|
|
||||||
|
private Streamray site;
|
||||||
|
private static List<StreamrayModel> modelsList;
|
||||||
|
private static Instant lastListInfoRequest = Instant.EPOCH;
|
||||||
|
private static JSONArray mapping;
|
||||||
|
protected int modelsPerPage = 48;
|
||||||
|
protected Predicate<StreamrayModel> filter;
|
||||||
|
|
||||||
|
public StreamrayUpdateService(Streamray site, Predicate<StreamrayModel> filter) {
|
||||||
|
this.site = site;
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<List<Model>> createTask() {
|
||||||
|
return new Task<List<Model>>() {
|
||||||
|
@Override
|
||||||
|
public List<Model> call() throws IOException {
|
||||||
|
return getModelList().stream()
|
||||||
|
.filter(filter)
|
||||||
|
.skip((page - 1) * (long) modelsPerPage)
|
||||||
|
.limit(modelsPerPage)
|
||||||
|
.collect(Collectors.toList()); // NOSONAR
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<StreamrayModel> getModelList() throws IOException {
|
||||||
|
if (Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 30) {
|
||||||
|
return Optional.ofNullable(modelsList).orElse(loadModelList());
|
||||||
|
}
|
||||||
|
modelsList = loadModelList();
|
||||||
|
return Optional.ofNullable(modelsList).orElse(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<StreamrayModel> loadModelList() throws IOException {
|
||||||
|
LOG.debug("Fetching page {}", API_URL);
|
||||||
|
lastListInfoRequest = Instant.now();
|
||||||
|
StreamrayHttpClient client = (StreamrayHttpClient) site.getHttpClient();
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(API_URL)
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
|
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
||||||
|
.header(REFERER, site.getBaseUrl() + "/")
|
||||||
|
.header(ORIGIN, site.getBaseUrl())
|
||||||
|
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
|
||||||
|
.build();
|
||||||
|
try (Response response = client.execute(req)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
List<StreamrayModel> models = new ArrayList<>();
|
||||||
|
String content = response.body().string();
|
||||||
|
JSONObject json = new JSONObject(content);
|
||||||
|
JSONArray modelNodes = json.getJSONArray("models");
|
||||||
|
mapping = json.getJSONArray("mapping");
|
||||||
|
parseModels(modelNodes, models);
|
||||||
|
return models;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseModels(JSONArray jsonModels, List<StreamrayModel> models) {
|
||||||
|
int name_idx = mapping_index("stream_name");
|
||||||
|
int date_idx = mapping_index("create_date");
|
||||||
|
int gen_idx = mapping_index("gender");
|
||||||
|
for (var i = 0; i < jsonModels.length(); i++) {
|
||||||
|
var m = jsonModels.getJSONArray(i);
|
||||||
|
String name = m.optString(name_idx);
|
||||||
|
String gender = m.optString(gen_idx);
|
||||||
|
String reg = m.optString(date_idx);
|
||||||
|
StreamrayModel model = (StreamrayModel) site.createModel(name);
|
||||||
|
try {
|
||||||
|
LocalDate regDate = LocalDate.parse(reg, DateTimeFormatter.BASIC_ISO_DATE);
|
||||||
|
model.setRegDate(regDate);
|
||||||
|
} catch (DateTimeParseException e) {
|
||||||
|
model.setRegDate(LocalDate.EPOCH);
|
||||||
|
}
|
||||||
|
String preview = getPreviewURL(name);
|
||||||
|
model.setPreview(preview);
|
||||||
|
model.setGender(gender);
|
||||||
|
models.add(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPreviewURL(String name) {
|
||||||
|
String lname = name.toLowerCase();
|
||||||
|
String url = MessageFormat.format("https://images4.streamray.com/images/streamray/won/jpg/{0}/{1}/{2}_640.jpg", lname.substring(0,1), lname.substring(lname.length()-1), lname);
|
||||||
|
try {
|
||||||
|
return MessageFormat.format("https://dynimages.securedataimages.com/unsigned/rs:fill:640::0/g:no/plain/{0}@jpg", URLEncoder.encode(url, "utf-8"));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFilter(Predicate<StreamrayModel> filter) {
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int mapping_index(String s) {
|
||||||
|
for (var i = 0; i < mapping.length(); i++) {
|
||||||
|
if (Objects.equals(s, mapping.get(i))) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package ctbrec.ui.sites.winktv;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.sites.winktv.WinkTv;
|
||||||
|
import ctbrec.ui.settings.SettingsTab;
|
||||||
|
import ctbrec.ui.sites.AbstractConfigUI;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.RadioButton;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
public class WinkTvConfigUI extends AbstractConfigUI {
|
||||||
|
private final WinkTv site;
|
||||||
|
|
||||||
|
public WinkTvConfigUI(WinkTv 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++);
|
||||||
|
row++;
|
||||||
|
|
||||||
|
var deleteCookies = new Button("Delete Cookies");
|
||||||
|
deleteCookies.setOnAction(e -> site.getHttpClient().clearCookies());
|
||||||
|
layout.add(deleteCookies, 1, row);
|
||||||
|
GridPane.setColumnSpan(deleteCookies, 2);
|
||||||
|
|
||||||
|
GridPane.setMargin(deleteCookies, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package ctbrec.ui.sites.winktv;
|
||||||
|
|
||||||
|
import ctbrec.sites.winktv.WinkTv;
|
||||||
|
import ctbrec.ui.sites.AbstractSiteUi;
|
||||||
|
import ctbrec.ui.sites.ConfigUI;
|
||||||
|
import ctbrec.ui.tabs.TabProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class WinkTvSiteUi extends AbstractSiteUi {
|
||||||
|
|
||||||
|
private WinkTvTabProvider tabProvider;
|
||||||
|
private WinkTvConfigUI configUi;
|
||||||
|
private final WinkTv site;
|
||||||
|
|
||||||
|
public WinkTvSiteUi(WinkTv site) {
|
||||||
|
this.site = site;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TabProvider getTabProvider() {
|
||||||
|
if (tabProvider == null) {
|
||||||
|
tabProvider = new WinkTvTabProvider(site);
|
||||||
|
}
|
||||||
|
return tabProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConfigUI getConfigUI() {
|
||||||
|
if (configUi == null) {
|
||||||
|
configUi = new WinkTvConfigUI(site);
|
||||||
|
}
|
||||||
|
return configUi;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean login() throws IOException {
|
||||||
|
return site.login();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package ctbrec.ui.sites.winktv;
|
||||||
|
|
||||||
|
import ctbrec.sites.winktv.WinkTv;
|
||||||
|
import ctbrec.sites.winktv.WinkTvModel;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
public class WinkTvTabProvider extends AbstractTabProvider {
|
||||||
|
|
||||||
|
public WinkTvTabProvider(WinkTv site) {
|
||||||
|
super(site);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<Tab> getSiteTabs(Scene scene) {
|
||||||
|
List<Tab> tabs = new ArrayList<>();
|
||||||
|
tabs.add(createTab("Live", m -> !m.isAdult()));
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tab createTab(String title, Predicate<WinkTvModel> filter) {
|
||||||
|
var updateService = new WinkTvUpdateService((WinkTv) site, filter);
|
||||||
|
var tab = new ThumbOverviewTab(title, updateService, site);
|
||||||
|
tab.setImageAspectRatio(9.0 / 16.0);
|
||||||
|
tab.setRecorder(recorder);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
package ctbrec.ui.sites.winktv;
|
||||||
|
|
||||||
|
import static ctbrec.io.HttpConstants.*;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.sites.winktv.WinkTv;
|
||||||
|
import ctbrec.sites.winktv.WinkTvModel;
|
||||||
|
import ctbrec.ui.SiteUiFactory;
|
||||||
|
import ctbrec.ui.tabs.PaginatedScheduledService;
|
||||||
|
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.Optional;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class WinkTvUpdateService extends PaginatedScheduledService {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(WinkTvUpdateService.class);
|
||||||
|
private static final String API_URL = "https://api.winktv.co.kr/v1/live";
|
||||||
|
|
||||||
|
private WinkTv site;
|
||||||
|
private String url;
|
||||||
|
private static List<WinkTvModel> modelsList;
|
||||||
|
private static Instant lastListInfoRequest = Instant.EPOCH;
|
||||||
|
protected int modelsPerPage = 48;
|
||||||
|
protected Predicate<WinkTvModel> filter;
|
||||||
|
|
||||||
|
public WinkTvUpdateService(WinkTv site, Predicate<WinkTvModel> filter) {
|
||||||
|
this.site = site;
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<List<Model>> createTask() {
|
||||||
|
return new Task<List<Model>>() {
|
||||||
|
@Override
|
||||||
|
public List<Model> call() throws IOException {
|
||||||
|
return getModelList().stream()
|
||||||
|
.filter(filter)
|
||||||
|
.skip((page - 1) * (long) modelsPerPage)
|
||||||
|
.limit(modelsPerPage)
|
||||||
|
.collect(Collectors.toList()); // NOSONAR
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<WinkTvModel> getModelList() throws IOException {
|
||||||
|
if (Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 30) {
|
||||||
|
return Optional.ofNullable(modelsList).orElse(loadModelList());
|
||||||
|
}
|
||||||
|
modelsList = loadModelList();
|
||||||
|
return Optional.ofNullable(modelsList).orElse(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<WinkTvModel> loadModelList() throws IOException {
|
||||||
|
LOG.debug("Fetching page {}", API_URL);
|
||||||
|
lastListInfoRequest = Instant.now();
|
||||||
|
FormBody body = new FormBody.Builder()
|
||||||
|
.add("offset", "0")
|
||||||
|
.add("limit", "500")
|
||||||
|
.add("orderBy", "hot")
|
||||||
|
.build();
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(API_URL)
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
|
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
||||||
|
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
|
||||||
|
.header(REFERER, site.getBaseUrl() + "/")
|
||||||
|
.header(ORIGIN, site.getBaseUrl())
|
||||||
|
.post(body)
|
||||||
|
.build();
|
||||||
|
try (var response = site.getHttpClient().execute(req)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
List<WinkTvModel> models = new ArrayList<>();
|
||||||
|
var content = response.body().string();
|
||||||
|
var json = new JSONObject(content);
|
||||||
|
if (json.optBoolean("result")) {
|
||||||
|
var modelNodes = json.getJSONArray("list");
|
||||||
|
parseModels(modelNodes, models);
|
||||||
|
}
|
||||||
|
return models;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseModels(JSONArray jsonModels, List<WinkTvModel> models) {
|
||||||
|
for (var i = 0; i < jsonModels.length(); i++) {
|
||||||
|
var m = jsonModels.getJSONObject(i);
|
||||||
|
String name = m.optString("userId");
|
||||||
|
WinkTvModel model = (WinkTvModel) site.createModel(name);
|
||||||
|
model.setDisplayName(m.getString("userNick"));
|
||||||
|
boolean isAdult = m.optBoolean("isAdult");
|
||||||
|
model.setAdult(isAdult);
|
||||||
|
if (isAdult && m.has("ivsThumbnail")) {
|
||||||
|
model.setPreview(m.optString("ivsThumbnail"));
|
||||||
|
} else {
|
||||||
|
model.setPreview(m.optString("thumbUrl"));
|
||||||
|
}
|
||||||
|
boolean isLive = m.optBoolean("isLive");
|
||||||
|
if (isLive) models.add(model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFilter(Predicate<WinkTvModel> filter) {
|
||||||
|
this.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,18 +1,7 @@
|
||||||
package ctbrec;
|
package ctbrec;
|
||||||
|
|
||||||
import static ctbrec.io.HttpConstants.*;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
|
|
||||||
import com.squareup.moshi.JsonReader;
|
import com.squareup.moshi.JsonReader;
|
||||||
import com.squareup.moshi.JsonWriter;
|
import com.squareup.moshi.JsonWriter;
|
||||||
|
|
||||||
import ctbrec.recorder.download.Download;
|
import ctbrec.recorder.download.Download;
|
||||||
import ctbrec.recorder.download.HttpHeaderFactory;
|
import ctbrec.recorder.download.HttpHeaderFactory;
|
||||||
import ctbrec.recorder.download.HttpHeaderFactoryImpl;
|
import ctbrec.recorder.download.HttpHeaderFactoryImpl;
|
||||||
|
@ -23,6 +12,16 @@ import ctbrec.sites.Site;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
import static ctbrec.io.HttpConstants.USER_AGENT;
|
||||||
|
|
||||||
public abstract class AbstractModel implements Model {
|
public abstract class AbstractModel implements Model {
|
||||||
|
|
||||||
private String url;
|
private String url;
|
||||||
|
@ -42,6 +41,7 @@ public abstract class AbstractModel implements Model {
|
||||||
private Instant lastRecorded;
|
private Instant lastRecorded;
|
||||||
private Instant recordUntil;
|
private Instant recordUntil;
|
||||||
private Instant addedTimestamp = Instant.EPOCH;
|
private Instant addedTimestamp = Instant.EPOCH;
|
||||||
|
private transient Instant delayUntil = Instant.EPOCH;
|
||||||
private SubsequentAction recordUntilSubsequentAction;
|
private SubsequentAction recordUntilSubsequentAction;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -149,6 +149,16 @@ public abstract class AbstractModel implements Model {
|
||||||
this.suspended = suspended;
|
this.suspended = suspended;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delay() {
|
||||||
|
this.delayUntil = Instant.now().plusSeconds(120);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDelayed() {
|
||||||
|
return this.delayUntil.isAfter(Instant.now());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isMarkedForLaterRecording() {
|
public boolean isMarkedForLaterRecording() {
|
||||||
return markedForLaterRecording;
|
return markedForLaterRecording;
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
package ctbrec;
|
package ctbrec;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
|
|
||||||
import javax.xml.bind.JAXBException;
|
|
||||||
|
|
||||||
import com.iheartradio.m3u8.ParseException;
|
import com.iheartradio.m3u8.ParseException;
|
||||||
import com.iheartradio.m3u8.PlaylistException;
|
import com.iheartradio.m3u8.PlaylistException;
|
||||||
import com.squareup.moshi.JsonReader;
|
import com.squareup.moshi.JsonReader;
|
||||||
import com.squareup.moshi.JsonWriter;
|
import com.squareup.moshi.JsonWriter;
|
||||||
|
|
||||||
import ctbrec.recorder.download.Download;
|
import ctbrec.recorder.download.Download;
|
||||||
import ctbrec.recorder.download.HttpHeaderFactory;
|
import ctbrec.recorder.download.HttpHeaderFactory;
|
||||||
import ctbrec.recorder.download.StreamSource;
|
import ctbrec.recorder.download.StreamSource;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
|
|
||||||
|
import javax.xml.bind.JAXBException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
public interface Model extends Comparable<Model>, Serializable {
|
public interface Model extends Comparable<Model>, Serializable {
|
||||||
|
|
||||||
long RECORD_INDEFINITELY = 9000000000000000000L;
|
long RECORD_INDEFINITELY = 9000000000000000000L;
|
||||||
|
@ -32,6 +30,7 @@ public interface Model extends Comparable<Model>, Serializable {
|
||||||
UNKNOWN("unknown");
|
UNKNOWN("unknown");
|
||||||
|
|
||||||
final String display;
|
final String display;
|
||||||
|
|
||||||
State(String display) {
|
State(String display) {
|
||||||
this.display = display;
|
this.display = display;
|
||||||
}
|
}
|
||||||
|
@ -102,10 +101,8 @@ public interface Model extends Comparable<Model>, Serializable {
|
||||||
/**
|
/**
|
||||||
* Determines the stream resolution for this model
|
* Determines the stream resolution for this model
|
||||||
*
|
*
|
||||||
* @param failFast
|
* @param failFast If set to true, the method returns immediately, even if the resolution is unknown. If
|
||||||
* If set to true, the method returns immediately, even if the resolution is unknown. If
|
|
||||||
* the resolution is unknown, the array contains 0,0
|
* the resolution is unknown, the array contains 0,0
|
||||||
*
|
|
||||||
* @return a tupel of width and height represented by an int[2]
|
* @return a tupel of width and height represented by an int[2]
|
||||||
* @throws ExecutionException
|
* @throws ExecutionException
|
||||||
*/
|
*/
|
||||||
|
@ -127,6 +124,10 @@ public interface Model extends Comparable<Model>, Serializable {
|
||||||
|
|
||||||
void setSuspended(boolean suspended);
|
void setSuspended(boolean suspended);
|
||||||
|
|
||||||
|
void delay();
|
||||||
|
|
||||||
|
boolean isDelayed();
|
||||||
|
|
||||||
boolean isMarkedForLaterRecording();
|
boolean isMarkedForLaterRecording();
|
||||||
|
|
||||||
void setMarkedForLaterRecording(boolean marked);
|
void setMarkedForLaterRecording(boolean marked);
|
||||||
|
@ -140,14 +141,18 @@ public interface Model extends Comparable<Model>, Serializable {
|
||||||
HttpHeaderFactory getHttpHeaderFactory();
|
HttpHeaderFactory getHttpHeaderFactory();
|
||||||
|
|
||||||
boolean isRecordingTimeLimited();
|
boolean isRecordingTimeLimited();
|
||||||
|
|
||||||
Instant getRecordUntil();
|
Instant getRecordUntil();
|
||||||
|
|
||||||
void setRecordUntil(Instant instant);
|
void setRecordUntil(Instant instant);
|
||||||
|
|
||||||
SubsequentAction getRecordUntilSubsequentAction();
|
SubsequentAction getRecordUntilSubsequentAction();
|
||||||
|
|
||||||
void setRecordUntilSubsequentAction(SubsequentAction action);
|
void setRecordUntilSubsequentAction(SubsequentAction action);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check, if this model account exists
|
* Check, if this model account exists
|
||||||
|
*
|
||||||
* @return true, if it exists, false otherwise
|
* @return true, if it exists, false otherwise
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -206,4 +206,9 @@ public class Settings {
|
||||||
public String webinterfacePassword = "sucks";
|
public String webinterfacePassword = "sucks";
|
||||||
public String xlovecamUsername = "";
|
public String xlovecamUsername = "";
|
||||||
public String xlovecamPassword = "";
|
public String xlovecamPassword = "";
|
||||||
|
public boolean stripchatVR = false;
|
||||||
|
public boolean streamrayRecordGoalShows = false;
|
||||||
|
public boolean checkForUpdates = true;
|
||||||
|
public int thumbCacheSize = 16;
|
||||||
|
public boolean dreamcamVR = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
package ctbrec.recorder.download;
|
package ctbrec.recorder.download;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
import ctbrec.Settings;
|
import ctbrec.Settings;
|
||||||
import ctbrec.UnknownModel;
|
import ctbrec.UnknownModel;
|
||||||
import ctbrec.recorder.download.hls.CombinedSplittingStrategy;
|
import ctbrec.recorder.download.hls.*;
|
||||||
import ctbrec.recorder.download.hls.NoopSplittingStrategy;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import ctbrec.recorder.download.hls.SizeSplittingStrategy;
|
|
||||||
import ctbrec.recorder.download.hls.TimeSplittingStrategy;
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
|
import static ctbrec.recorder.download.StreamSource.UNKNOWN;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public abstract class AbstractDownload implements Download {
|
public abstract class AbstractDownload implements Download {
|
||||||
|
|
||||||
protected Instant startTime;
|
protected Instant startTime;
|
||||||
|
@ -22,6 +25,7 @@ public abstract class AbstractDownload implements Download {
|
||||||
protected Config config;
|
protected Config config;
|
||||||
protected SplittingStrategy splittingStrategy;
|
protected SplittingStrategy splittingStrategy;
|
||||||
protected ExecutorService downloadExecutor;
|
protected ExecutorService downloadExecutor;
|
||||||
|
protected int selectedResolution = UNKNOWN;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
|
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
|
||||||
|
@ -72,6 +76,39 @@ public abstract class AbstractDownload implements Download {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getSelectedResolution() {
|
public int getSelectedResolution() {
|
||||||
return StreamSource.UNKNOWN;
|
return selectedResolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void awaitEnd() {
|
||||||
|
// do nothing per default
|
||||||
|
}
|
||||||
|
|
||||||
|
protected StreamSource selectStreamSource(List<StreamSource> streamSources) throws ExecutionException {
|
||||||
|
if (model.getStreamUrlIndex() >= 0 && model.getStreamUrlIndex() < streamSources.size()) {
|
||||||
|
// TODO don't use the index, but the bandwidth. if the bandwidth does not match, take the closest one
|
||||||
|
log.debug("Model stream index: {}", model.getStreamUrlIndex());
|
||||||
|
streamSources.forEach(ss -> log.debug(ss.toString()));
|
||||||
|
StreamSource source = streamSources.get(model.getStreamUrlIndex());
|
||||||
|
log.debug("{} selected {}", model.getName(), source);
|
||||||
|
selectedResolution = source.height;
|
||||||
|
return source;
|
||||||
|
} else {
|
||||||
|
// filter out stream resolutions, which are out of range of the configured min and max
|
||||||
|
int minRes = Config.getInstance().getSettings().minimumResolution;
|
||||||
|
int maxRes = Config.getInstance().getSettings().maximumResolution;
|
||||||
|
List<StreamSource> filteredStreamSources = streamSources.stream()
|
||||||
|
.filter(src -> src.height == 0 || src.height == UNKNOWN || minRes <= src.height)
|
||||||
|
.filter(src -> src.height == 0 || src.height == UNKNOWN || maxRes >= src.height)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (filteredStreamSources.isEmpty()) {
|
||||||
|
throw new ExecutionException(new NoStreamFoundException("No stream left in playlist"));
|
||||||
|
} else {
|
||||||
|
StreamSource source = filteredStreamSources.get(filteredStreamSources.size() - 1);
|
||||||
|
log.debug("{} selected {}", model.getName(), source);
|
||||||
|
selectedResolution = source.height;
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,325 @@
|
||||||
|
package ctbrec.recorder.download.hls;
|
||||||
|
|
||||||
|
import com.iheartradio.m3u8.*;
|
||||||
|
import com.iheartradio.m3u8.data.MediaPlaylist;
|
||||||
|
import com.iheartradio.m3u8.data.Playlist;
|
||||||
|
import com.iheartradio.m3u8.data.TrackData;
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.OS;
|
||||||
|
import ctbrec.Recording;
|
||||||
|
import ctbrec.io.BandwidthMeter;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.recorder.FFmpeg;
|
||||||
|
import ctbrec.recorder.InvalidPlaylistException;
|
||||||
|
import ctbrec.recorder.download.AbstractDownload;
|
||||||
|
import ctbrec.recorder.download.Download;
|
||||||
|
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||||
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.xml.bind.JAXBException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static ctbrec.io.HttpConstants.*;
|
||||||
|
|
||||||
|
public class FfmpegHlsDownload extends AbstractDownload {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(FfmpegHlsDownload.class);
|
||||||
|
private static final int MAX_SECONDS_WITHOUT_TRANSFER = 20;
|
||||||
|
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
private Instant timeOfLastTransfer = Instant.MAX;
|
||||||
|
|
||||||
|
protected File targetFile;
|
||||||
|
protected FFmpeg ffmpeg;
|
||||||
|
protected Process ffmpegProcess;
|
||||||
|
protected OutputStream ffmpegStdIn;
|
||||||
|
protected Lock ffmpegStreamLock = new ReentrantLock();
|
||||||
|
protected String mediaUrl = null;
|
||||||
|
|
||||||
|
private volatile boolean running;
|
||||||
|
private volatile boolean started;
|
||||||
|
private int selectedResolution = 0;
|
||||||
|
|
||||||
|
public FfmpegHlsDownload(HttpClient httpClient) {
|
||||||
|
this.httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
|
||||||
|
super.init(config, model, startTime, executorService);
|
||||||
|
|
||||||
|
timeOfLastTransfer = Instant.now();
|
||||||
|
String fileSuffix = config.getSettings().ffmpegFileSuffix;
|
||||||
|
|
||||||
|
targetFile = config.getFileForRecording(model, fileSuffix, startTime);
|
||||||
|
createTargetDirectory();
|
||||||
|
startFfmpegProcess(targetFile);
|
||||||
|
if (ffmpegProcess == null) {
|
||||||
|
throw new ProcessExitedUncleanException("Couldn't spawn FFmpeg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSelectedResolution() {
|
||||||
|
return selectedResolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
if (running) {
|
||||||
|
internalStop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void internalStop() {
|
||||||
|
running = false;
|
||||||
|
if (ffmpegStdIn != null) {
|
||||||
|
try {
|
||||||
|
ffmpegStdIn.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Couldn't terminate FFmpeg by closing stdin", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ffmpegProcess != null) {
|
||||||
|
try {
|
||||||
|
boolean waitFor = ffmpegProcess.waitFor(45, TimeUnit.SECONDS);
|
||||||
|
if (!waitFor && ffmpegProcess.isAlive()) {
|
||||||
|
ffmpegProcess.destroy();
|
||||||
|
if (ffmpegProcess.isAlive()) {
|
||||||
|
LOG.info("FFmpeg didn't terminate. Destroying the process with force!");
|
||||||
|
ffmpegProcess.destroyForcibly();
|
||||||
|
ffmpegProcess = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOG.error("Interrupted while waiting for FFmpeg to terminate");
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startFfmpegProcess(File target) {
|
||||||
|
try {
|
||||||
|
String[] cmdline = prepareCommandLine(target);
|
||||||
|
ffmpeg = new FFmpeg.Builder()
|
||||||
|
.logOutput(config.getSettings().logFFmpegOutput)
|
||||||
|
.onStarted(p -> {
|
||||||
|
ffmpegProcess = p;
|
||||||
|
ffmpegStdIn = ffmpegProcess.getOutputStream();
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
ffmpeg.exec(cmdline, new String[0], target.getParentFile());
|
||||||
|
} catch (IOException | ProcessExitedUncleanException e) {
|
||||||
|
LOG.error("Error in FFmpeg thread", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] prepareCommandLine(File target) {
|
||||||
|
String[] args = config.getSettings().ffmpegMergedDownloadArgs.split(" ");
|
||||||
|
String[] argsPlusFile = new String[args.length + 3];
|
||||||
|
int i = 0;
|
||||||
|
argsPlusFile[i++] = "-i";
|
||||||
|
argsPlusFile[i++] = "-";
|
||||||
|
System.arraycopy(args, 0, argsPlusFile, i, args.length);
|
||||||
|
argsPlusFile[argsPlusFile.length - 1] = target.getAbsolutePath();
|
||||||
|
return OS.getFFmpegCommand(argsPlusFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finalizeDownload() {
|
||||||
|
internalStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 Download call() throws Exception {
|
||||||
|
try {
|
||||||
|
if (!ffmpegProcess.isAlive()) {
|
||||||
|
running = false;
|
||||||
|
int exitValue = ffmpegProcess.exitValue();
|
||||||
|
ffmpeg.shutdown(exitValue);
|
||||||
|
}
|
||||||
|
} catch (ProcessExitedUncleanException e) {
|
||||||
|
LOG.error("FFmpeg exited unclean", e);
|
||||||
|
internalStop();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!started) {
|
||||||
|
started = true;
|
||||||
|
startDownload();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Error while downloading MP4", e);
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
if (!model.isOnline()) {
|
||||||
|
LOG.debug("Model {} not online. Stop recording.", model);
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
if (splittingStrategy.splitNecessary(this)) {
|
||||||
|
LOG.debug("Split necessary for model {}. Stop recording.", model);
|
||||||
|
internalStop();
|
||||||
|
} else {
|
||||||
|
rescheduleTime = Instant.now().plusSeconds(5);
|
||||||
|
}
|
||||||
|
if (Duration.between(timeOfLastTransfer, Instant.now()).getSeconds() > MAX_SECONDS_WITHOUT_TRANSFER) {
|
||||||
|
LOG.debug("No video data received for {} seconds. Stopping recording for model {}", MAX_SECONDS_WITHOUT_TRANSFER, model);
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMediaUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException, InvalidPlaylistException, JAXBException {
|
||||||
|
List<StreamSource> streamSources = model.getStreamSources();
|
||||||
|
Collections.sort(streamSources);
|
||||||
|
for (StreamSource streamSource : streamSources) {
|
||||||
|
LOG.debug("{}:{} src {}", model.getSite().getName(), model.getName(), streamSource);
|
||||||
|
}
|
||||||
|
StreamSource selectedStreamSource = selectStreamSource(streamSources);
|
||||||
|
String playlistUrl = selectedStreamSource.getMediaPlaylistUrl();
|
||||||
|
selectedResolution = selectedStreamSource.height;
|
||||||
|
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(playlistUrl)
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ACCEPT, "*/*")
|
||||||
|
.header(ACCEPT_LANGUAGE, "en")
|
||||||
|
.header(ORIGIN, model.getSite().getBaseUrl())
|
||||||
|
.header(REFERER, model.getSite().getBaseUrl())
|
||||||
|
.build();
|
||||||
|
try (Response response = model.getSite().getHttpClient().execute(req)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
InputStream inputStream = Objects.requireNonNull(response.body()).byteStream();
|
||||||
|
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||||
|
Playlist playlist = parser.parse();
|
||||||
|
MediaPlaylist media = playlist.getMediaPlaylist();
|
||||||
|
if (media.hasTracks()) {
|
||||||
|
TrackData firstTrack = media.getTracks().get(0);
|
||||||
|
if (firstTrack.isEncrypted()) {
|
||||||
|
LOG.warn("Video track is encrypted. Playlist URL: {}", playlistUrl);
|
||||||
|
}
|
||||||
|
String uri = firstTrack.getUri();
|
||||||
|
if (!uri.startsWith("http")) {
|
||||||
|
URL context = new URL(playlistUrl);
|
||||||
|
uri = new URL(context, uri).toExternalForm();
|
||||||
|
}
|
||||||
|
LOG.debug("Media url {}", uri);
|
||||||
|
return uri;
|
||||||
|
} else {
|
||||||
|
throw new InvalidPlaylistException("Playlist has no media");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startDownload() {
|
||||||
|
downloadExecutor.submit(() -> {
|
||||||
|
running = true;
|
||||||
|
ffmpegStreamLock.lock();
|
||||||
|
try {
|
||||||
|
if (mediaUrl == null) {
|
||||||
|
mediaUrl = getMediaUrl(model);
|
||||||
|
}
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(mediaUrl)
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ACCEPT, "*/*")
|
||||||
|
.header(ACCEPT_LANGUAGE, "en")
|
||||||
|
.header(ORIGIN, model.getSite().getBaseUrl())
|
||||||
|
.header(REFERER, model.getSite().getBaseUrl())
|
||||||
|
.build();
|
||||||
|
try (Response resp = httpClient.execute(request)) {
|
||||||
|
if (resp.isSuccessful()) {
|
||||||
|
LOG.debug("Recording video stream to {}", targetFile);
|
||||||
|
InputStream in = Objects.requireNonNull(resp.body()).byteStream();
|
||||||
|
byte[] b = new byte[1024 * 4];
|
||||||
|
int len;
|
||||||
|
while (running && !Thread.currentThread().isInterrupted() && (len = in.read(b)) >= 0) {
|
||||||
|
ffmpegStdIn.write(b, 0, len);
|
||||||
|
timeOfLastTransfer = Instant.now();
|
||||||
|
BandwidthMeter.add(len);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new HttpException(resp.code(), resp.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SocketTimeoutException e) {
|
||||||
|
LOG.debug("Socket timeout while downloading MP4 for {}. Stop recording.", model.getName());
|
||||||
|
model.delay();
|
||||||
|
stop();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.debug("IO error while downloading MP4 for {}. Stop recording.", model.getName());
|
||||||
|
model.delay();
|
||||||
|
stop();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Error while downloading MP4", e);
|
||||||
|
stop();
|
||||||
|
} finally {
|
||||||
|
ffmpegStreamLock.unlock();
|
||||||
|
}
|
||||||
|
LOG.debug("Record finished for model {}", model);
|
||||||
|
running = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void createTargetDirectory() throws IOException {
|
||||||
|
Files.createDirectories(targetFile.getParentFile().toPath());
|
||||||
|
}
|
||||||
|
}
|
|
@ -115,7 +115,7 @@ public class Chaturbate extends AbstractSite {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsTips() {
|
public boolean supportsTips() {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -4,14 +4,12 @@ import ctbrec.Config;
|
||||||
import ctbrec.io.HtmlParser;
|
import ctbrec.io.HtmlParser;
|
||||||
import ctbrec.io.HttpClient;
|
import ctbrec.io.HttpClient;
|
||||||
import okhttp3.*;
|
import okhttp3.*;
|
||||||
import org.jsoup.nodes.Element;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.Semaphore;
|
import java.util.concurrent.Semaphore;
|
||||||
|
|
||||||
import static ctbrec.io.HttpConstants.REFERER;
|
import static ctbrec.io.HttpConstants.REFERER;
|
||||||
|
@ -98,6 +96,8 @@ public class ChaturbateHttpClient extends HttpClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response.close();
|
response.close();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LOG.debug("Login failed: {}", ex.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
loginTries = 0;
|
loginTries = 0;
|
||||||
}
|
}
|
||||||
|
@ -105,25 +105,16 @@ public class ChaturbateHttpClient extends HttpClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkLogin() throws IOException {
|
public boolean checkLogin() throws IOException {
|
||||||
String url = "https://chaturbate.com/p/" + Config.getInstance().getSettings().chaturbateUsername + "/";
|
String url = "https://chaturbate.com/api/ts/chatmessages/pm_users/?offset=0";
|
||||||
Request req = new Request.Builder()
|
Request req = new Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
.build();
|
.build();
|
||||||
try (Response resp = execute(req)) {
|
try (Response resp = execute(req)) {
|
||||||
if (resp.isSuccessful()) {
|
return (resp.isSuccessful() && !resp.isRedirect());
|
||||||
String profilePage = resp.body().string();
|
} catch (Exception ex) {
|
||||||
try {
|
|
||||||
Element userIcon = HtmlParser.getTag(profilePage, "img.user_information_header_icon");
|
|
||||||
return !Objects.equals("Anonymous Icon", userIcon.attr("alt"));
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.debug("Token tag not found. Login failed");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw new IOException("HTTP response: " + resp.code() + " - " + resp.message());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
package ctbrec.sites.dreamcam;
|
||||||
|
|
||||||
|
import static ctbrec.io.HttpConstants.*;
|
||||||
|
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import ctbrec.sites.AbstractSite;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class Dreamcam extends AbstractSite {
|
||||||
|
|
||||||
|
public static String domain = "dreamcam.com";
|
||||||
|
public static String baseUri = "https://dreamcam.com";
|
||||||
|
private HttpClient httpClient;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() throws IOException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "DreamCam";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return baseUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffiliateLink() {
|
||||||
|
return baseUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBuyTokensLink() {
|
||||||
|
return getAffiliateLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DreamcamModel createModel(String name) {
|
||||||
|
DreamcamModel model = new DreamcamModel();
|
||||||
|
model.setName(name);
|
||||||
|
model.setUrl(getBaseUrl() + "/models/" + name);
|
||||||
|
model.setSite(this);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Double getTokenBalance() throws IOException {
|
||||||
|
return 0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean login() throws IOException {
|
||||||
|
return credentialsAvailable() && getHttpClient().login();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient getHttpClient() {
|
||||||
|
if (httpClient == null) {
|
||||||
|
httpClient = new DreamcamHttpClient(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 false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean searchRequiresLogin() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Model> search(String q) throws IOException, InterruptedException {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSiteForModel(Model m) {
|
||||||
|
return m instanceof DreamcamModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean credentialsAvailable() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Model createModelFromUrl(String url) {
|
||||||
|
String[] patterns = {
|
||||||
|
"https://.*?dreamcam.com/models/([_a-zA-Z0-9]+)",
|
||||||
|
};
|
||||||
|
for (String p : patterns) {
|
||||||
|
Matcher m = Pattern.compile(p).matcher(url);
|
||||||
|
if (m.matches()) {
|
||||||
|
String modelName = m.group(1);
|
||||||
|
return createModel(modelName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.createModelFromUrl(url);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,341 @@
|
||||||
|
package ctbrec.sites.dreamcam;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.StringUtil;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.OS;
|
||||||
|
import ctbrec.Recording;
|
||||||
|
import ctbrec.io.BandwidthMeter;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.recorder.FFmpeg;
|
||||||
|
import ctbrec.recorder.InvalidPlaylistException;
|
||||||
|
import ctbrec.recorder.download.AbstractDownload;
|
||||||
|
import ctbrec.recorder.download.Download;
|
||||||
|
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
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 static ctbrec.io.HttpConstants.*;
|
||||||
|
|
||||||
|
public class DreamcamDownload extends AbstractDownload {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(DreamcamDownload.class);
|
||||||
|
private static final int MAX_SECONDS_WITHOUT_TRANSFER = 30;
|
||||||
|
|
||||||
|
private final HttpClient httpClient;
|
||||||
|
private Instant timeOfLastTransfer = Instant.MAX;
|
||||||
|
|
||||||
|
protected File targetFile;
|
||||||
|
protected FFmpeg ffmpeg;
|
||||||
|
protected Process ffmpegProcess;
|
||||||
|
protected OutputStream ffmpegStdIn;
|
||||||
|
protected Lock ffmpegStreamLock = new ReentrantLock();
|
||||||
|
protected String wsUrl;
|
||||||
|
|
||||||
|
private volatile boolean running;
|
||||||
|
private volatile boolean started;
|
||||||
|
private final transient Object monitor = new Object();
|
||||||
|
private WebSocket ws;
|
||||||
|
private DreamcamModel model;
|
||||||
|
|
||||||
|
public DreamcamDownload(HttpClient httpClient) {
|
||||||
|
this.httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config config, Model model, Instant startTime, ExecutorService executorService) throws IOException {
|
||||||
|
super.init(config, model, startTime, executorService);
|
||||||
|
this.model = (DreamcamModel) model;
|
||||||
|
timeOfLastTransfer = startTime;
|
||||||
|
String fileSuffix = config.getSettings().ffmpegFileSuffix;
|
||||||
|
targetFile = config.getFileForRecording(model, fileSuffix, startTime);
|
||||||
|
createTargetDirectory();
|
||||||
|
startFfmpegProcess(targetFile);
|
||||||
|
if (ffmpegProcess == null) {
|
||||||
|
throw new ProcessExitedUncleanException("Couldn't spawn FFmpeg");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSelectedResolution() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
if (running) {
|
||||||
|
internalStop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void internalStop() {
|
||||||
|
running = false;
|
||||||
|
if (ws != null) {
|
||||||
|
ws.close(1000, null);
|
||||||
|
ws = null;
|
||||||
|
}
|
||||||
|
if (ffmpegStdIn != null) {
|
||||||
|
try {
|
||||||
|
ffmpegStdIn.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Couldn't terminate FFmpeg by closing stdin", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ffmpegProcess != null) {
|
||||||
|
try {
|
||||||
|
boolean waitFor = ffmpegProcess.waitFor(45, TimeUnit.SECONDS);
|
||||||
|
if (!waitFor && ffmpegProcess.isAlive()) {
|
||||||
|
ffmpegProcess.destroy();
|
||||||
|
if (ffmpegProcess.isAlive()) {
|
||||||
|
LOG.info("FFmpeg didn't terminate. Destroying the process with force!");
|
||||||
|
ffmpegProcess.destroyForcibly();
|
||||||
|
ffmpegProcess = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOG.error("Interrupted while waiting for FFmpeg to terminate");
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startFfmpegProcess(File target) {
|
||||||
|
try {
|
||||||
|
String[] cmdline = prepareCommandLine(target);
|
||||||
|
ffmpeg = new FFmpeg.Builder()
|
||||||
|
.logOutput(config.getSettings().logFFmpegOutput)
|
||||||
|
.onStarted(p -> {
|
||||||
|
ffmpegProcess = p;
|
||||||
|
ffmpegStdIn = ffmpegProcess.getOutputStream();
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
ffmpeg.exec(cmdline, new String[0], target.getParentFile());
|
||||||
|
} catch (IOException | ProcessExitedUncleanException e) {
|
||||||
|
LOG.error("Error in FFmpeg thread", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] prepareCommandLine(File target) {
|
||||||
|
String[] args = config.getSettings().ffmpegMergedDownloadArgs.split(" ");
|
||||||
|
String[] argsPlusFile = new String[args.length + 3];
|
||||||
|
int i = 0;
|
||||||
|
argsPlusFile[i++] = "-i";
|
||||||
|
argsPlusFile[i++] = "-";
|
||||||
|
System.arraycopy(args, 0, argsPlusFile, i, args.length);
|
||||||
|
argsPlusFile[argsPlusFile.length - 1] = target.getAbsolutePath();
|
||||||
|
return OS.getFFmpegCommand(argsPlusFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finalizeDownload() {
|
||||||
|
internalStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 Download call() throws Exception {
|
||||||
|
try {
|
||||||
|
if (!ffmpegProcess.isAlive()) {
|
||||||
|
running = false;
|
||||||
|
int exitValue = ffmpegProcess.exitValue();
|
||||||
|
ffmpeg.shutdown(exitValue);
|
||||||
|
}
|
||||||
|
} catch (ProcessExitedUncleanException e) {
|
||||||
|
LOG.error("FFmpeg exited unclean", e);
|
||||||
|
internalStop();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!started) {
|
||||||
|
started = true;
|
||||||
|
startDownload();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Error while downloading", e);
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
if (!model.isOnline()) {
|
||||||
|
LOG.debug("Model {} not online. Stop recording.", model);
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
if (splittingStrategy.splitNecessary(this)) {
|
||||||
|
LOG.debug("Split necessary for model {}. Stop recording.", model);
|
||||||
|
internalStop();
|
||||||
|
rescheduleTime = Instant.now();
|
||||||
|
} else {
|
||||||
|
rescheduleTime = Instant.now().plusSeconds(5);
|
||||||
|
}
|
||||||
|
if (Duration.between(timeOfLastTransfer, Instant.now()).getSeconds() > MAX_SECONDS_WITHOUT_TRANSFER) {
|
||||||
|
LOG.debug("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;
|
||||||
|
ffmpegStreamLock.lock();
|
||||||
|
try {
|
||||||
|
wsUrl = model.getWsUrl();
|
||||||
|
LOG.debug("{} ws url: {}", model.getName(), wsUrl);
|
||||||
|
if (StringUtil.isBlank(wsUrl)) {
|
||||||
|
LOG.error("{}: Stream URL not found", model);
|
||||||
|
stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(wsUrl)
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ACCEPT, "*/*")
|
||||||
|
.header(ACCEPT_LANGUAGE, "en")
|
||||||
|
.header(ORIGIN, model.getSite().getBaseUrl())
|
||||||
|
.header(REFERER, model.getSite().getBaseUrl() + "/")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ws = httpClient.newWebSocket(request, new WebSocketListener() {
|
||||||
|
@Override
|
||||||
|
public void onOpen(WebSocket webSocket, Response response) {
|
||||||
|
super.onOpen(webSocket, response);
|
||||||
|
LOG.debug("{}: Websocket open", model);
|
||||||
|
if (response != null) {
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
JSONObject msg = new JSONObject();
|
||||||
|
msg.put("url", "stream/hello");
|
||||||
|
msg.put("version", "0.0.1");
|
||||||
|
webSocket.send(msg.toString());
|
||||||
|
} // onOpen
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClosed(WebSocket webSocket, int code, String reason) {
|
||||||
|
super.onClosed(webSocket, code, reason);
|
||||||
|
LOG.trace("{}: Websocket closed", model);
|
||||||
|
stop();
|
||||||
|
synchronized (monitor) {
|
||||||
|
monitor.notifyAll();
|
||||||
|
}
|
||||||
|
} // onClosed
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
||||||
|
super.onFailure(webSocket, t, response);
|
||||||
|
LOG.debug("{}: Websocket failed: {}", model, t.getMessage());
|
||||||
|
if (response != null) {
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
stop();
|
||||||
|
synchronized (monitor) {
|
||||||
|
monitor.notifyAll();
|
||||||
|
}
|
||||||
|
} // onFailure
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocket webSocket, String text) {
|
||||||
|
super.onMessage(webSocket, text);
|
||||||
|
LOG.trace("{} ws message: {}", model, text);
|
||||||
|
JSONObject message = new JSONObject(text);
|
||||||
|
if (message.optString("url").equals("stream/qual")) {
|
||||||
|
JSONObject msg = new JSONObject();
|
||||||
|
msg.put("quality", "test");
|
||||||
|
msg.put("url", "stream/play");
|
||||||
|
msg.put("version", "0.0.1");
|
||||||
|
webSocket.send(msg.toString());
|
||||||
|
}
|
||||||
|
} // onMessage
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocket webSocket, ByteString bytes) {
|
||||||
|
super.onMessage(webSocket, bytes);
|
||||||
|
timeOfLastTransfer = Instant.now();
|
||||||
|
try {
|
||||||
|
if (running) {
|
||||||
|
byte[] videoData = bytes.toByteArray();
|
||||||
|
ffmpegStdIn.write(videoData);
|
||||||
|
BandwidthMeter.add(videoData.length);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (running) {
|
||||||
|
LOG.error("Couldn't write video stream to file", e);
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // onMessage
|
||||||
|
|
||||||
|
}); // websocket
|
||||||
|
|
||||||
|
synchronized (monitor) {
|
||||||
|
try {
|
||||||
|
monitor.wait();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
LOG.warn("Interrupted while waiting for the download to terminate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
if (running) {
|
||||||
|
LOG.error("Error while downloading: {}", ex.getMessage());
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
ffmpegStreamLock.unlock();
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
}); // submit
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void createTargetDirectory() throws IOException {
|
||||||
|
Files.createDirectories(targetFile.getParentFile().toPath());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package ctbrec.sites.dreamcam;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class DreamcamHttpClient extends HttpClient {
|
||||||
|
|
||||||
|
public DreamcamHttpClient(Config config) {
|
||||||
|
super("dreamcam", config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean login() throws IOException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
package ctbrec.sites.dreamcam;
|
||||||
|
|
||||||
|
import com.iheartradio.m3u8.*;
|
||||||
|
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||||
|
import com.iheartradio.m3u8.data.Playlist;
|
||||||
|
import com.iheartradio.m3u8.data.PlaylistData;
|
||||||
|
import ctbrec.AbstractModel;
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.StringUtil;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.recorder.InvalidPlaylistException;
|
||||||
|
import ctbrec.recorder.download.Download;
|
||||||
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import javax.xml.bind.JAXBException;
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import static ctbrec.Model.State.*;
|
||||||
|
import static ctbrec.io.HttpConstants.*;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
public class DreamcamModel extends AbstractModel {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(DreamcamModel.class);
|
||||||
|
private static final String API_URL = "https://bss.dreamcamtrue.com";
|
||||||
|
private int[] resolution = new int[2];
|
||||||
|
private JSONObject modelInfo;
|
||||||
|
private boolean VRMode = false;
|
||||||
|
|
||||||
|
private transient Instant lastInfoRequest = Instant.EPOCH;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||||
|
if (ignoreCache) {
|
||||||
|
try {
|
||||||
|
JSONObject json = getModelInfo();
|
||||||
|
mapOnlineState(json.optString("broadcastStatus"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
setOnlineState(OFFLINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return onlineState == ONLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mapOnlineState(String status) {
|
||||||
|
switch (status) {
|
||||||
|
case "public" -> setOnlineState(ONLINE);
|
||||||
|
case "private" -> setOnlineState(PRIVATE);
|
||||||
|
case "offline" -> setOnlineState(OFFLINE);
|
||||||
|
default -> setOnlineState(OFFLINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||||
|
if (failFast && onlineState != UNKNOWN) {
|
||||||
|
return onlineState;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
JSONObject json = getModelInfo();
|
||||||
|
mapOnlineState(json.optString("broadcastStatus"));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
setOnlineState(OFFLINE);
|
||||||
|
}
|
||||||
|
return onlineState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<StreamSource> getStreamSources() throws InvalidPlaylistException {
|
||||||
|
List<StreamSource> sources = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
StreamSource src = new StreamSource();
|
||||||
|
src.mediaPlaylistUrl = getPlaylistUrl();
|
||||||
|
sources.add(src);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Can not get stream sources for {}: {}", getName(), e.getMessage());
|
||||||
|
throw new InvalidPlaylistException(e.getMessage());
|
||||||
|
}
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPlaylistUrl() throws IOException, InvalidPlaylistException {
|
||||||
|
JSONObject json = getModelInfo();
|
||||||
|
String mediaUrl = "";
|
||||||
|
if (json.has("streams")) {
|
||||||
|
JSONArray streams = json.getJSONArray("streams");
|
||||||
|
for (int i=0; i < streams.length(); i++) {
|
||||||
|
JSONObject s = streams.getJSONObject(i);
|
||||||
|
if (s.has("streamType") && s.has("url")) {
|
||||||
|
String streamType = s.getString("streamType");
|
||||||
|
if (streamType.equals("video2D")) {
|
||||||
|
mediaUrl = s.optString("url");
|
||||||
|
LOG.trace("PlaylistUrl for {}: {}", getName(), mediaUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (StringUtil.isBlank(mediaUrl)) {
|
||||||
|
throw new InvalidPlaylistException("Playlist has no media");
|
||||||
|
}
|
||||||
|
return mediaUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWsUrl() throws IOException {
|
||||||
|
JSONObject json = getModelInfo();
|
||||||
|
return json.optString("streamUrl").replace("fmp4s://", "wss://");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getChatId() throws IOException {
|
||||||
|
JSONObject json = getModelInfo();
|
||||||
|
return json.optString("roomChatId");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
|
||||||
|
return new int[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject getModelInfo() throws IOException {
|
||||||
|
if (Duration.between(lastInfoRequest, Instant.now()).getSeconds() < 5) {
|
||||||
|
modelInfo = Optional.ofNullable(modelInfo).orElse(loadModelInfo());
|
||||||
|
} else {
|
||||||
|
modelInfo = loadModelInfo();
|
||||||
|
}
|
||||||
|
return modelInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject loadModelInfo() throws IOException {
|
||||||
|
lastInfoRequest = Instant.now();
|
||||||
|
String url = MessageFormat.format(API_URL + "/api/clients/v1/broadcasts/models/{0}?partnerId=dreamcam_oauth2&show-hidden=true&stream-types=video2D,video3D", getName());
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
|
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
||||||
|
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
|
||||||
|
.header(REFERER, getUrl())
|
||||||
|
.header(ORIGIN, getSite().getBaseUrl())
|
||||||
|
.build();
|
||||||
|
try (Response response = getSite().getHttpClient().execute(req)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
|
return json;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPreviewURL() throws IOException {
|
||||||
|
JSONObject json = getModelInfo();
|
||||||
|
return json.optString("modelProfilePhotoUrl");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean follow() throws IOException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean unfollow() throws IOException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receiveTip(Double tokens) throws IOException {
|
||||||
|
// not implemented
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateCacheEntries() {
|
||||||
|
resolution = new int[]{0, 0};
|
||||||
|
lastInfoRequest = Instant.EPOCH;
|
||||||
|
modelInfo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Download createDownload() {
|
||||||
|
if (Config.getInstance().getSettings().dreamcamVR) {
|
||||||
|
return new DreamcamDownload(getSite().getHttpClient());
|
||||||
|
} else {
|
||||||
|
return new MergedFfmpegHlsDownload(getSite().getHttpClient());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,178 @@
|
||||||
|
package ctbrec.sites.streamray;
|
||||||
|
|
||||||
|
import static ctbrec.io.HttpConstants.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.StringUtil;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.sites.AbstractSite;
|
||||||
|
|
||||||
|
|
||||||
|
public class Streamray extends AbstractSite {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(Streamray.class);
|
||||||
|
|
||||||
|
private StreamrayHttpClient httpClient;
|
||||||
|
public static String domain = "streamray.com";
|
||||||
|
public static String baseUri = "https://streamray.com";
|
||||||
|
public static String apiURL = "https://beta-api.cams.com";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() throws IOException {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Streamray";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return baseUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getApiUrl() {
|
||||||
|
return apiURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public StreamrayModel createModel(String name) {
|
||||||
|
StreamrayModel model = new StreamrayModel();
|
||||||
|
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 getBaseUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean login() throws IOException {
|
||||||
|
boolean result = getHttpClient().login();
|
||||||
|
LOG.debug("Streamray site login call result: {}", result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient getHttpClient() {
|
||||||
|
if (httpClient == null) {
|
||||||
|
httpClient = new StreamrayHttpClient(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 boolean searchRequiresLogin() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Model> search(String q) throws IOException, InterruptedException {
|
||||||
|
if (StringUtil.isBlank(q)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
String url = getApiUrl() + "/models/new/?limit=30&search=" + URLEncoder.encode(q, "utf-8") + "&order=is_online";
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header(USER_AGENT, getConfig().getSettings().httpUserAgent)
|
||||||
|
.build();
|
||||||
|
try (Response response = getHttpClient().execute(req)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
|
if (json.has("results")) {
|
||||||
|
List<Model> models = new ArrayList<>();
|
||||||
|
JSONArray results = json.getJSONArray("results");
|
||||||
|
if (results.length() == 0) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
for (int i = 0; i < results.length(); i++) {
|
||||||
|
JSONObject result = results.getJSONObject(i);
|
||||||
|
StreamrayModel model = createModel(result.getString("stream_name"));
|
||||||
|
String image = result.optString("profile_image");
|
||||||
|
if (StringUtil.isBlank(image)) {
|
||||||
|
image = model.getPreviewURL();
|
||||||
|
}
|
||||||
|
model.setPreview(image);
|
||||||
|
models.add(model);
|
||||||
|
}
|
||||||
|
return models;
|
||||||
|
} else {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSiteForModel(Model m) {
|
||||||
|
return m instanceof StreamrayModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean credentialsAvailable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Model createModelFromUrl(String url) {
|
||||||
|
Matcher m = Pattern.compile("https://(streamray|cams).com/([_a-zA-Z0-9]+)").matcher(url);
|
||||||
|
if (m.matches()) {
|
||||||
|
String modelName = m.group(2);
|
||||||
|
return createModel(modelName);
|
||||||
|
} else {
|
||||||
|
return super.createModelFromUrl(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffiliateLink() {
|
||||||
|
return getBaseUrl();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package ctbrec.sites.streamray;
|
||||||
|
|
||||||
|
import static ctbrec.io.HttpConstants.*;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import ctbrec.StringUtil;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Locale;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import okhttp3.*;
|
||||||
|
|
||||||
|
|
||||||
|
public class StreamrayHttpClient extends HttpClient {
|
||||||
|
|
||||||
|
public StreamrayHttpClient(Config config) {
|
||||||
|
super("streamray", config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean login() throws IOException {
|
||||||
|
String token = getUserToken();
|
||||||
|
if (StringUtil.isBlank(token)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
boolean isSuccess = checkLoginSuccess();
|
||||||
|
if (isSuccess) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
updateToken();
|
||||||
|
return checkLoginSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateToken() {
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(Streamray.baseUri)
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ACCEPT, "*/*")
|
||||||
|
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
||||||
|
.build();
|
||||||
|
try (Response response = execute(req)) {} catch (Exception ex) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean checkLoginSuccess() {
|
||||||
|
String token = getUserToken();
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(Streamray.apiURL + "/members/me/balance/")
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
|
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
||||||
|
.header(AUTHORIZATION, "Bearer " + token)
|
||||||
|
.header(REFERER, Streamray.baseUri + "/")
|
||||||
|
.header(ORIGIN, Streamray.baseUri)
|
||||||
|
.build();
|
||||||
|
try (Response response = execute(req)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
String content = response.body().string();
|
||||||
|
JSONObject json = new JSONObject(content);
|
||||||
|
return json.has("balance");
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserToken() {
|
||||||
|
try {
|
||||||
|
Cookie cookie = getCookieJar().getCookie(HttpUrl.parse(Streamray.baseUri), "memberToken");
|
||||||
|
String token = cookie.value();
|
||||||
|
return token;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
package ctbrec.sites.streamray;
|
||||||
|
|
||||||
|
import com.iheartradio.m3u8.ParseException;
|
||||||
|
import com.iheartradio.m3u8.PlaylistException;
|
||||||
|
import ctbrec.AbstractModel;
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.recorder.download.Download;
|
||||||
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
import ctbrec.recorder.download.hls.FfmpegHlsDownload;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.xml.bind.JAXBException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.text.MessageFormat;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
import static ctbrec.Model.State.*;
|
||||||
|
import static ctbrec.io.HttpConstants.*;
|
||||||
|
|
||||||
|
public class StreamrayModel extends AbstractModel {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(StreamrayModel.class);
|
||||||
|
private String status = null;
|
||||||
|
private String gender = null;
|
||||||
|
private LocalDate regDate = LocalDate.EPOCH;
|
||||||
|
private JSONObject modelInfo;
|
||||||
|
|
||||||
|
private transient Instant lastInfoRequest = Instant.EPOCH;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||||
|
if (ignoreCache) {
|
||||||
|
try {
|
||||||
|
JSONObject json = getModelInfo();
|
||||||
|
if (json.has("online")) {
|
||||||
|
status = json.optString("online");
|
||||||
|
mapOnlineState(status);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
setOnlineState(UNKNOWN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return onlineState == ONLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void mapOnlineState(String status) {
|
||||||
|
boolean goalShows = Config.getInstance().getSettings().streamrayRecordGoalShows;
|
||||||
|
switch (status) {
|
||||||
|
case "0" -> setOnlineState(OFFLINE);
|
||||||
|
case "1" -> setOnlineState(ONLINE);
|
||||||
|
case "6" -> setOnlineState(goalShows ? ONLINE : PRIVATE);
|
||||||
|
case "2", "3", "4", "7", "10", "11", "12", "13", "14" -> setOnlineState(PRIVATE);
|
||||||
|
default -> setOnlineState(OFFLINE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||||
|
if (failFast && onlineState != UNKNOWN) {
|
||||||
|
return onlineState;
|
||||||
|
} else {
|
||||||
|
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> sources = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
String url = getMasterPlaylistUrl();
|
||||||
|
StreamSource src = new StreamSource();
|
||||||
|
src.mediaPlaylistUrl = url;
|
||||||
|
src.height = 0;
|
||||||
|
src.width = 0;
|
||||||
|
src.bandwidth = 0;
|
||||||
|
sources.add(src);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Can not get stream sources for {}", getName());
|
||||||
|
}
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMasterPlaylistUrl() throws IOException {
|
||||||
|
JSONObject json = getModelInfo();
|
||||||
|
String mpp = json.getString("mpp");
|
||||||
|
String lname = getName().toLowerCase();
|
||||||
|
return MessageFormat.format("https://stream14.cams.com/h5live/http/playlist.m3u8?url=rtmp%3A%2F%2F{0}%3A1935%2Fcams%2F{1}%3Fcams%2F{1}_720p&stream={2}", mpp, lname, getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
|
||||||
|
return new int[]{0, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject getModelInfo() throws IOException {
|
||||||
|
if (Duration.between(lastInfoRequest, Instant.now()).getSeconds() < 5) {
|
||||||
|
modelInfo = Optional.ofNullable(modelInfo).orElse(loadModelInfo());
|
||||||
|
} else {
|
||||||
|
modelInfo = loadModelInfo();
|
||||||
|
}
|
||||||
|
return modelInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject loadModelInfo() throws IOException {
|
||||||
|
lastInfoRequest = Instant.now();
|
||||||
|
String url = "https://beta-api.cams.com/models/stream/" + getName() + "/";
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPreviewURL() {
|
||||||
|
String lname = getName().toLowerCase();
|
||||||
|
String url = MessageFormat.format("https://images4.streamray.com/images/streamray/won/jpg/{0}/{1}/{2}_640.jpg", lname.substring(0, 1), lname.substring(lname.length() - 1), lname);
|
||||||
|
try {
|
||||||
|
return MessageFormat.format("https://dynimages.securedataimages.com/unsigned/rs:fill:320::0/g:no/plain/{0}@jpg", URLEncoder.encode(url, "utf-8"));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Download createDownload() {
|
||||||
|
return new FfmpegHlsDownload(getSite().getHttpClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean follow() throws IOException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean unfollow() throws IOException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receiveTip(Double tokens) throws IOException {
|
||||||
|
// not implemented
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateCacheEntries() {
|
||||||
|
status = null;
|
||||||
|
lastInfoRequest = Instant.EPOCH;
|
||||||
|
modelInfo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getGender() {
|
||||||
|
return gender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGender(String gender) {
|
||||||
|
this.gender = gender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegDate(LocalDate reg) {
|
||||||
|
this.regDate = reg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isNew() {
|
||||||
|
return ChronoUnit.DAYS.between(this.regDate, LocalDate.now()) < 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
package ctbrec.sites.winktv;
|
||||||
|
|
||||||
|
import static ctbrec.io.HttpConstants.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.Locale;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.StringUtil;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.sites.AbstractSite;
|
||||||
|
|
||||||
|
public class WinkTv extends AbstractSite {
|
||||||
|
|
||||||
|
public static String domain = "www.winktv.co.kr";
|
||||||
|
public static String baseUri = "https://www.winktv.co.kr";
|
||||||
|
private HttpClient httpClient;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() throws IOException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "WinkTv";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return baseUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffiliateLink() {
|
||||||
|
return baseUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBuyTokensLink() {
|
||||||
|
return getAffiliateLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WinkTvModel createModel(String name) {
|
||||||
|
WinkTvModel model = new WinkTvModel();
|
||||||
|
model.setName(name);
|
||||||
|
model.setUrl(getBaseUrl() + "/live/play/" + name);
|
||||||
|
model.setSite(this);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Double getTokenBalance() throws IOException {
|
||||||
|
return 0d;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean login() throws IOException {
|
||||||
|
return credentialsAvailable() && getHttpClient().login();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient getHttpClient() {
|
||||||
|
if (httpClient == null) {
|
||||||
|
httpClient = new WinkTvHttpClient(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 boolean searchRequiresLogin() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Model> search(String q) throws IOException, InterruptedException {
|
||||||
|
if (StringUtil.isBlank(q)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
String url = "https://api.winktv.co.kr/v1/live";
|
||||||
|
FormBody body = new FormBody.Builder()
|
||||||
|
.add("offset", "0")
|
||||||
|
.add("limit", "30")
|
||||||
|
.add("orderBy", "user")
|
||||||
|
.add("searchVal", URLEncoder.encode(q, "utf-8"))
|
||||||
|
.build();
|
||||||
|
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(X_REQUESTED_WITH, XML_HTTP_REQUEST)
|
||||||
|
.header(REFERER, getBaseUrl() + "/")
|
||||||
|
.header(ORIGIN, getBaseUrl())
|
||||||
|
.post(body)
|
||||||
|
.build();
|
||||||
|
try (Response response = getHttpClient().execute(req)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
|
if (json.optBoolean("result") && json.has("list")) {
|
||||||
|
List<Model> models = new ArrayList<>();
|
||||||
|
JSONArray results = json.getJSONArray("list");
|
||||||
|
if (results.length() == 0) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
for (int i = 0; i < results.length(); i++) {
|
||||||
|
JSONObject result = results.getJSONObject(i);
|
||||||
|
WinkTvModel model = createModel(result.optString("userId"));
|
||||||
|
model.setPreview(result.optString("thumbUrl"));
|
||||||
|
models.add(model);
|
||||||
|
}
|
||||||
|
return models;
|
||||||
|
} else {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSiteForModel(Model m) {
|
||||||
|
return m instanceof WinkTvModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean credentialsAvailable() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Model createModelFromUrl(String url) {
|
||||||
|
String[] patterns = {
|
||||||
|
"https://.*?winktv.co.kr/live/play/([_a-zA-Z0-9]+)",
|
||||||
|
"https://.*?winktv.co.kr/channel/([_a-zA-Z0-9]+)",
|
||||||
|
"https://.*?pandalive.co.kr/live/play/([_a-zA-Z0-9]+)",
|
||||||
|
"https://.*?pandalive.co.kr/channel/([_a-zA-Z0-9]+)"
|
||||||
|
};
|
||||||
|
for (String p : patterns) {
|
||||||
|
Matcher m = Pattern.compile(p).matcher(url);
|
||||||
|
if (m.matches()) {
|
||||||
|
String modelName = m.group(1);
|
||||||
|
return createModel(modelName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.createModelFromUrl(url);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package ctbrec.sites.winktv;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class WinkTvHttpClient extends HttpClient {
|
||||||
|
|
||||||
|
public WinkTvHttpClient(Config config) {
|
||||||
|
super("winktv", config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean login() throws IOException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,270 @@
|
||||||
|
package ctbrec.sites.winktv;
|
||||||
|
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import javax.xml.bind.JAXBException;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import com.iheartradio.m3u8.*;
|
||||||
|
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||||
|
import com.iheartradio.m3u8.data.Playlist;
|
||||||
|
import com.iheartradio.m3u8.data.PlaylistData;
|
||||||
|
|
||||||
|
import ctbrec.AbstractModel;
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import ctbrec.io.HttpException;
|
||||||
|
import ctbrec.recorder.download.Download;
|
||||||
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
import ctbrec.recorder.download.hls.HlsdlDownload;
|
||||||
|
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
|
||||||
|
|
||||||
|
import static ctbrec.Model.State.*;
|
||||||
|
import static ctbrec.io.HttpConstants.*;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
|
public class WinkTvModel extends AbstractModel {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(WinkTvModel.class);
|
||||||
|
private int[] resolution = new int[]{0, 0};
|
||||||
|
private boolean adult = false;
|
||||||
|
private JSONObject modelInfo;
|
||||||
|
|
||||||
|
private transient Instant lastInfoRequest = Instant.EPOCH;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||||
|
if (ignoreCache) {
|
||||||
|
try {
|
||||||
|
JSONObject json = getModelInfo();
|
||||||
|
if (json.has("media")) {
|
||||||
|
JSONObject media = json.getJSONObject("media");
|
||||||
|
boolean isLive = media.optBoolean("isLive");
|
||||||
|
String meType = media.optString("type");
|
||||||
|
if (isLive && meType.equals("free")) {
|
||||||
|
setOnlineState(ONLINE);
|
||||||
|
} else {
|
||||||
|
setOnlineState(PRIVATE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setOnlineState(OFFLINE);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
setOnlineState(UNKNOWN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return onlineState == ONLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||||
|
if (failFast && onlineState != UNKNOWN) {
|
||||||
|
return onlineState;
|
||||||
|
} else {
|
||||||
|
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 {
|
||||||
|
String url = getMasterPlaylistUrl();
|
||||||
|
MasterPlaylist masterPlaylist = getMasterPlaylist(url);
|
||||||
|
List<StreamSource> streamSources = extractStreamSources(masterPlaylist);
|
||||||
|
return streamSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<StreamSource> extractStreamSources(MasterPlaylist masterPlaylist) {
|
||||||
|
List<StreamSource> sources = new ArrayList<>();
|
||||||
|
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||||
|
if (playlist.hasStreamInfo()) {
|
||||||
|
StreamSource src = new StreamSource();
|
||||||
|
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||||
|
src.height = playlist.getStreamInfo().getResolution().height;
|
||||||
|
src.mediaPlaylistUrl = playlist.getUri();
|
||||||
|
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||||
|
sources.add(src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = getSite().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 {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMasterPlaylistUrl() throws IOException {
|
||||||
|
JSONObject json = getModelInfo();
|
||||||
|
JSONObject info = json.getJSONObject("bjInfo");
|
||||||
|
long userIdx = info.optLong("idx");
|
||||||
|
String url = "https://api.winktv.co.kr/v1/live/play";
|
||||||
|
FormBody body = new FormBody.Builder()
|
||||||
|
.add("action", "watch")
|
||||||
|
.add("userIdx", String.valueOf(userIdx))
|
||||||
|
.add("password", "")
|
||||||
|
.build();
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
|
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
||||||
|
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
|
||||||
|
.header(REFERER, getUrl())
|
||||||
|
.header(ORIGIN, getSite().getBaseUrl())
|
||||||
|
.post(body)
|
||||||
|
.build();
|
||||||
|
try (Response response = getSite().getHttpClient().execute(req)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
JSONObject jsonResponse = new JSONObject(response.body().string());
|
||||||
|
JSONObject playlist = jsonResponse.getJSONObject("PlayList");
|
||||||
|
JSONObject hls = playlist.getJSONArray("hls").getJSONObject(0);
|
||||||
|
String hlsUrl = hls.optString("url");
|
||||||
|
return hlsUrl;
|
||||||
|
} else {
|
||||||
|
LOG.debug("Error while get master playlist url for {}: {}", getName(), response.body().string());
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 e) {
|
||||||
|
throw new ExecutionException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject getModelInfo() throws IOException {
|
||||||
|
if (Duration.between(lastInfoRequest, Instant.now()).getSeconds() < 5) {
|
||||||
|
return Optional.ofNullable(modelInfo).orElse(new JSONObject()) ;
|
||||||
|
}
|
||||||
|
lastInfoRequest = Instant.now();
|
||||||
|
modelInfo = loadModelInfo();
|
||||||
|
return modelInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject loadModelInfo() throws IOException {
|
||||||
|
String url = "https://api.winktv.co.kr/v1/member/bj";
|
||||||
|
FormBody body = new FormBody.Builder()
|
||||||
|
.add("userId", getName())
|
||||||
|
.add("info", "media")
|
||||||
|
.build();
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
||||||
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
||||||
|
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
||||||
|
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
|
||||||
|
.header(REFERER, getUrl())
|
||||||
|
.header(ORIGIN, getSite().getBaseUrl())
|
||||||
|
.post(body)
|
||||||
|
.build();
|
||||||
|
try (Response response = getSite().getHttpClient().execute(req)) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
JSONObject jsonResponse = new JSONObject(response.body().string());
|
||||||
|
return jsonResponse;
|
||||||
|
} else {
|
||||||
|
throw new HttpException(response.code(), response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPreviewURL() throws IOException {
|
||||||
|
JSONObject json = getModelInfo();
|
||||||
|
if (json.has("media")) {
|
||||||
|
JSONObject media = json.getJSONObject("media");
|
||||||
|
return media.optString("ivsThumbnail");
|
||||||
|
}
|
||||||
|
if (json.has("bjInfo")) {
|
||||||
|
JSONObject info = json.getJSONObject("bjInfo");
|
||||||
|
return info.optString("thumbUrl");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean follow() throws IOException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean unfollow() throws IOException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAdult() {
|
||||||
|
return adult;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdult(boolean a) {
|
||||||
|
this.adult = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receiveTip(Double tokens) throws IOException {
|
||||||
|
// not implemented
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateCacheEntries() {
|
||||||
|
resolution = new int[]{0, 0};
|
||||||
|
lastInfoRequest = Instant.EPOCH;
|
||||||
|
modelInfo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Download createDownload() {
|
||||||
|
if (Config.getInstance().getSettings().useHlsdl) {
|
||||||
|
return new HlsdlDownload();
|
||||||
|
} else {
|
||||||
|
return new MergedFfmpegHlsDownload(getSite().getHttpClient());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ import ctbrec.sites.cam4.Cam4;
|
||||||
import ctbrec.sites.camsoda.Camsoda;
|
import ctbrec.sites.camsoda.Camsoda;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
import ctbrec.sites.chaturbate.Chaturbate;
|
||||||
import ctbrec.sites.cherrytv.CherryTv;
|
import ctbrec.sites.cherrytv.CherryTv;
|
||||||
|
import ctbrec.sites.dreamcam.Dreamcam;
|
||||||
import ctbrec.sites.fc2live.Fc2Live;
|
import ctbrec.sites.fc2live.Fc2Live;
|
||||||
import ctbrec.sites.flirt4free.Flirt4Free;
|
import ctbrec.sites.flirt4free.Flirt4Free;
|
||||||
import ctbrec.sites.jasmin.LiveJasmin;
|
import ctbrec.sites.jasmin.LiveJasmin;
|
||||||
|
@ -26,7 +27,9 @@ import ctbrec.sites.mfc.MyFreeCams;
|
||||||
import ctbrec.sites.secretfriends.SecretFriends;
|
import ctbrec.sites.secretfriends.SecretFriends;
|
||||||
import ctbrec.sites.showup.Showup;
|
import ctbrec.sites.showup.Showup;
|
||||||
import ctbrec.sites.streamate.Streamate;
|
import ctbrec.sites.streamate.Streamate;
|
||||||
|
import ctbrec.sites.streamray.Streamray;
|
||||||
import ctbrec.sites.stripchat.Stripchat;
|
import ctbrec.sites.stripchat.Stripchat;
|
||||||
|
import ctbrec.sites.winktv.WinkTv;
|
||||||
import ctbrec.sites.xlovecam.XloveCam;
|
import ctbrec.sites.xlovecam.XloveCam;
|
||||||
import org.eclipse.jetty.security.*;
|
import org.eclipse.jetty.security.*;
|
||||||
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
|
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
|
||||||
|
@ -134,6 +137,7 @@ public class HttpServer {
|
||||||
sites.add(new Camsoda());
|
sites.add(new Camsoda());
|
||||||
sites.add(new Chaturbate());
|
sites.add(new Chaturbate());
|
||||||
sites.add(new CherryTv());
|
sites.add(new CherryTv());
|
||||||
|
sites.add(new Dreamcam());
|
||||||
sites.add(new Fc2Live());
|
sites.add(new Fc2Live());
|
||||||
sites.add(new Flirt4Free());
|
sites.add(new Flirt4Free());
|
||||||
sites.add(new LiveJasmin());
|
sites.add(new LiveJasmin());
|
||||||
|
@ -143,7 +147,9 @@ public class HttpServer {
|
||||||
sites.add(new Showup());
|
sites.add(new Showup());
|
||||||
sites.add(new Streamate());
|
sites.add(new Streamate());
|
||||||
sites.add(new Stripchat());
|
sites.add(new Stripchat());
|
||||||
|
sites.add(new Streamray());
|
||||||
sites.add(new XloveCam());
|
sites.add(new XloveCam());
|
||||||
|
sites.add(new WinkTv());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addShutdownHook() {
|
private void addShutdownHook() {
|
||||||
|
|
Loading…
Reference in New Issue