Code cleanup

This commit is contained in:
0xb00bface 2023-11-04 21:58:32 +01:00
parent 541fcf5bc7
commit 03dd723fb6
14 changed files with 247 additions and 374 deletions

View File

@ -0,0 +1,18 @@
package ctbrec.ui.sites.streamray;
import ctbrec.sites.streamray.Streamray;
import ctbrec.ui.tabs.PaginatedScheduledService;
abstract class AbstractStreamrayUpdateService extends PaginatedScheduledService {
protected int modelsPerPage = 48;
protected final Streamray site;
AbstractStreamrayUpdateService(Streamray site) {
this.site = site;
}
boolean isLoggedIn() {
return site.isLoggedIn();
}
}

View File

@ -1,37 +1,31 @@
package ctbrec.ui.sites.streamray; package ctbrec.ui.sites.streamray;
import ctbrec.Config;
import ctbrec.sites.streamray.Streamray;
import ctbrec.ui.ExternalBrowser;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Cookie;
import okhttp3.Cookie.Builder;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.json.JSONObject; @Slf4j
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 { public class StreamrayElectronLoginDialog {
private static final Logger LOG = LoggerFactory.getLogger(StreamrayElectronLoginDialog.class);
public static final String DOMAIN = "streamray.com"; public static final String DOMAIN = "streamray.com";
public static final String URL = "https://streamray.com/";
private CookieJar cookieJar; private CookieJar cookieJar;
private ExternalBrowser browser;
private boolean firstCall = true;
private final static Streamray site = new Streamray();
public StreamrayElectronLoginDialog(CookieJar cookieJar) throws IOException { public StreamrayElectronLoginDialog(CookieJar cookieJar) throws IOException {
this.cookieJar = cookieJar; this.cookieJar = cookieJar;
browser = ExternalBrowser.getInstance(); try (ExternalBrowser browser = ExternalBrowser.getInstance()) {
try {
var config = new JSONObject(); var config = new JSONObject();
config.put("url", URL); config.put("url", Streamray.BASE_URI);
config.put("w", 800); config.put("w", 800);
config.put("h", 600); config.put("h", 600);
config.put("userAgent", Config.getInstance().getSettings().httpUserAgent); config.put("userAgent", Config.getInstance().getSettings().httpUserAgent);
@ -41,23 +35,17 @@ public class StreamrayElectronLoginDialog {
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
throw new IOException("Couldn't wait for login dialog", e); throw new IOException("Couldn't wait for login dialog", e);
} finally {
browser.close();
} }
} }
private Consumer<String> msgHandler = line -> { private final Consumer<String> msgHandler = line -> {
if (!line.startsWith("{")) return; if (!line.startsWith("{")) return;
JSONObject json = new JSONObject(line); JSONObject json = new JSONObject(line);
boolean loginCookie = false;
if (json.has("cookies")) { if (json.has("cookies")) {
var cookies = json.getJSONArray("cookies"); var cookies = json.getJSONArray("cookies");
for (var i = 0; i < cookies.length(); i++) { for (var i = 0; i < cookies.length(); i++) {
var cookie = cookies.getJSONObject(i); var cookie = cookies.getJSONObject(i);
if (cookie.getString("domain").contains(DOMAIN)) { if (cookie.getString("domain").contains(DOMAIN)) {
if (cookie.optString("name").equals("memberToken")) {
loginCookie = true;
}
Builder b = new Cookie.Builder() Builder b = new Cookie.Builder()
.path(cookie.getString("path")) .path(cookie.getString("path"))
.domain(DOMAIN) .domain(DOMAIN)
@ -74,8 +62,8 @@ public class StreamrayElectronLoginDialog {
b.secure(); b.secure();
} }
Cookie c = b.build(); Cookie c = b.build();
LOG.trace("Adding cookie {}={}", c.name(), c.value()); log.trace("Adding cookie {}={}", c.name(), c.value());
cookieJar.saveFromResponse(HttpUrl.parse(URL), Collections.singletonList(c)); cookieJar.saveFromResponse(HttpUrl.parse(Streamray.BASE_URI), Collections.singletonList(c));
} // if } // if
} // for } // for
} }

View File

@ -1,132 +1,41 @@
package ctbrec.ui.sites.streamray; package ctbrec.ui.sites.streamray;
import static ctbrec.io.HttpConstants.*;
import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.StringUtil; import ctbrec.sites.streamray.Streamray;
import ctbrec.io.HttpException; import ctbrec.sites.streamray.StreamrayModel;
import ctbrec.sites.streamray.*; import javafx.concurrent.Task;
import ctbrec.ui.SiteUiFactory; import lombok.extern.slf4j.Slf4j;
import ctbrec.ui.tabs.PaginatedScheduledService;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; 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 { @Slf4j
public class StreamrayFavoritesService extends AbstractStreamrayUpdateService {
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) { public StreamrayFavoritesService(Streamray site) {
this.site = site; super(site);
} }
@Override @Override
protected Task<List<Model>> createTask() { protected Task<List<Model>> createTask() {
return new Task<List<Model>>() { return new Task<>() {
@Override @Override
public List<Model> call() throws IOException { public List<Model> call() throws IOException {
return getModelList().stream() return getModelList().stream()
.skip((page - 1) * (long) modelsPerPage) .skip((page - 1) * (long) modelsPerPage)
.limit(modelsPerPage) .limit(modelsPerPage)
.collect(Collectors.toList()); // NOSONAR .map(Model.class::cast)
.toList();
} }
}; };
} }
private List<StreamrayModel> getModelList() throws IOException { private List<StreamrayModel> getModelList() throws IOException {
modelsList = loadModelList(); List<StreamrayModel> models = site.loadModelList(true).stream().filter(StreamrayModel::isFavorite).toList();
if (modelsList == null) { if (models == null) {
modelsList = Collections.emptyList(); models = Collections.emptyList();
} }
return modelsList; return models;
}
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;
} }
} }

View File

@ -4,26 +4,22 @@ import ctbrec.sites.streamray.Streamray;
import ctbrec.ui.tabs.FollowedTab; import ctbrec.ui.tabs.FollowedTab;
import ctbrec.ui.tabs.ThumbOverviewTab; import ctbrec.ui.tabs.ThumbOverviewTab;
import javafx.concurrent.WorkerStateEvent; 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.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import org.slf4j.Logger; import javafx.scene.Scene;
import org.slf4j.LoggerFactory; import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class StreamrayFavoritesTab extends ThumbOverviewTab implements FollowedTab { public class StreamrayFavoritesTab extends ThumbOverviewTab implements FollowedTab {
private static final Logger LOG = LoggerFactory.getLogger(StreamrayFavoritesTab.class); private final Label status;
private Label status; private final Button loginButton;
private Button loginButton; private final StreamrayFavoritesService streamrayFavoritesService;
private Streamray site;
private StreamrayFavoritesService updateService;
public StreamrayFavoritesTab(String title, StreamrayFavoritesService updateService, Streamray site) { public StreamrayFavoritesTab(String title, StreamrayFavoritesService updateService, Streamray site) {
super(title, updateService, site); super(title, updateService, site);
this.site = site; this.streamrayFavoritesService = updateService;
this.updateService = updateService;
status = new Label("Logging in..."); status = new Label("Logging in...");
grid.getChildren().addAll(status); grid.getChildren().addAll(status);
@ -34,7 +30,8 @@ public class StreamrayFavoritesTab extends ThumbOverviewTab implements FollowedT
try { try {
new StreamrayElectronLoginDialog(site.getHttpClient().getCookieJar()); new StreamrayElectronLoginDialog(site.getHttpClient().getCookieJar());
updateService.restart(); updateService.restart();
} catch (Exception ex) {} } catch (Exception ex) {
}
}); });
} }
@ -48,7 +45,7 @@ public class StreamrayFavoritesTab extends ThumbOverviewTab implements FollowedT
protected void onSuccess() { protected void onSuccess() {
grid.getChildren().removeAll(status, loginButton); grid.getChildren().removeAll(status, loginButton);
grid.setAlignment(Pos.TOP_LEFT); grid.setAlignment(Pos.TOP_LEFT);
if (updateService.loggedIn == false) { if (!((AbstractStreamrayUpdateService) updateService).isLoggedIn()) {
addLoginButton(); addLoginButton();
} else { } else {
super.onSuccess(); super.onSuccess();

View File

@ -37,8 +37,4 @@ public class StreamraySiteUi extends AbstractSiteUi {
public boolean login() throws IOException { public boolean login() throws IOException {
return site.login(); return site.login();
} }
public synchronized boolean checkLogin() throws IOException {
return site.login();
}
} }

View File

@ -2,7 +2,6 @@ package ctbrec.ui.sites.streamray;
import ctbrec.sites.streamray.Streamray; import ctbrec.sites.streamray.Streamray;
import ctbrec.sites.streamray.StreamrayModel; import ctbrec.sites.streamray.StreamrayModel;
import ctbrec.ui.sites.AbstractTabProvider; import ctbrec.ui.sites.AbstractTabProvider;
import ctbrec.ui.tabs.ThumbOverviewTab; import ctbrec.ui.tabs.ThumbOverviewTab;
import javafx.scene.Scene; import javafx.scene.Scene;
@ -25,7 +24,7 @@ public class StreamrayTabProvider extends AbstractTabProvider {
tabs.add(createTab("Girls", m -> Objects.equals("F", m.getGender()))); tabs.add(createTab("Girls", m -> Objects.equals("F", m.getGender())));
tabs.add(createTab("Boys", m -> Objects.equals("M", 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("Trans", m -> Objects.equals("TS", m.getGender())));
tabs.add(createTab("New", m -> m.isNew())); tabs.add(createTab("New", StreamrayModel::isNew));
tabs.add(favoritesTab()); tabs.add(favoritesTab());
return tabs; return tabs;
} }

View File

@ -1,146 +1,52 @@
package ctbrec.ui.sites.streamray; package ctbrec.ui.sites.streamray;
import static ctbrec.io.HttpConstants.*;
import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.StringUtil; import ctbrec.sites.streamray.Streamray;
import ctbrec.io.HttpException; import ctbrec.sites.streamray.StreamrayModel;
import ctbrec.sites.streamray.*; import javafx.concurrent.Task;
import ctbrec.ui.SiteUiFactory; import lombok.Setter;
import ctbrec.ui.tabs.PaginatedScheduledService; import lombok.extern.slf4j.Slf4j;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; 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.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate; 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 { @Slf4j
public class StreamrayUpdateService extends AbstractStreamrayUpdateService {
private static final Logger LOG = LoggerFactory.getLogger(StreamrayUpdateService.class); private List<StreamrayModel> models;
private static final String API_URL = "https://beta-api.cams.com/won/compressed/"; private Instant lastListInfoRequest = Instant.EPOCH;
private Streamray site; @Setter
private static List<StreamrayModel> modelsList;
private static Instant lastListInfoRequest = Instant.EPOCH;
private static JSONArray mapping;
protected int modelsPerPage = 48;
protected Predicate<StreamrayModel> filter; protected Predicate<StreamrayModel> filter;
public StreamrayUpdateService(Streamray site, Predicate<StreamrayModel> filter) { public StreamrayUpdateService(Streamray site, Predicate<StreamrayModel> filter) {
this.site = site; super(site);
this.filter = filter; this.filter = filter;
} }
@Override @Override
protected Task<List<Model>> createTask() { protected Task<List<Model>> createTask() {
return new Task<List<Model>>() { return new Task<>() {
@Override @Override
public List<Model> call() throws IOException { public List<Model> call() throws IOException {
return getModelList().stream() return getModels().stream()
.filter(filter) .filter(filter)
.skip((page - 1) * (long) modelsPerPage) .skip((page - 1) * (long) modelsPerPage)
.limit(modelsPerPage) .limit(modelsPerPage)
.collect(Collectors.toList()); // NOSONAR .map(Model.class::cast)
.toList();
} }
}; };
} }
private List<StreamrayModel> getModelList() throws IOException { private List<StreamrayModel> getModels() throws IOException {
if (Duration.between(lastListInfoRequest, Instant.now()).getSeconds() < 30) { if (models == null || Duration.between(lastListInfoRequest, Instant.now()).getSeconds() >= 10) {
return Optional.ofNullable(modelsList).orElse(loadModelList()); models = site.loadModelList();
lastListInfoRequest = Instant.now();
} }
modelsList = loadModelList(); return models;
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;
} }
} }

View File

@ -32,7 +32,9 @@ public interface Site {
HttpClient getHttpClient(); HttpClient getHttpClient();
void init() throws IOException; default void init() throws IOException {
// do nothing per default
}
void shutdown(); void shutdown();

View File

@ -32,7 +32,7 @@ import static ctbrec.io.HttpConstants.*;
public class AmateurTvModel extends AbstractModel { public class AmateurTvModel extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTvModel.class); private static final Logger LOG = LoggerFactory.getLogger(AmateurTvModel.class);
private JSONArray qualities = new JSONArray(); private transient JSONArray qualities = new JSONArray();
private int[] resolution = new int[2]; private int[] resolution = new int[2];
@Override @Override
@ -55,9 +55,7 @@ public class AmateurTvModel extends AbstractModel {
@Override @Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException { public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if (failFast && onlineState != UNKNOWN) { if (!failFast || onlineState == UNKNOWN) {
return onlineState;
} else {
try { try {
onlineState = isOnline(true) ? ONLINE : OFFLINE; onlineState = isOnline(true) ? ONLINE : OFFLINE;
} catch (InterruptedException e) { } catch (InterruptedException e) {
@ -66,8 +64,8 @@ public class AmateurTvModel extends AbstractModel {
} catch (IOException | ExecutionException e) { } catch (IOException | ExecutionException e) {
onlineState = OFFLINE; onlineState = OFFLINE;
} }
return onlineState;
} }
return onlineState;
} }
@Override @Override

View File

@ -31,7 +31,7 @@ import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
public class ChaturbateModel extends AbstractModel { // NOSONAR public class ChaturbateModel extends AbstractModel {
private static final String PUBLIC = "public"; private static final String PUBLIC = "public";
private static final Logger LOG = LoggerFactory.getLogger(ChaturbateModel.class); private static final Logger LOG = LoggerFactory.getLogger(ChaturbateModel.class);

View File

@ -108,11 +108,6 @@ public class CherryTvModel extends AbstractModel {
return onlineState; return onlineState;
} }
@Override
public void setOnlineState(State onlineState) {
this.onlineState = onlineState;
}
@Override @Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
try { try {

View File

@ -1,40 +1,43 @@
package ctbrec.sites.streamray; package ctbrec.sites.streamray;
import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Model.State;
import ctbrec.StringUtil; import ctbrec.StringUtil;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.sites.AbstractSite; import ctbrec.sites.AbstractSite;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.ArrayList; import java.text.MessageFormat;
import java.util.Collections; import java.time.LocalDate;
import java.util.List; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static ctbrec.io.HttpConstants.USER_AGENT; import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j
public class Streamray extends AbstractSite { public class Streamray extends AbstractSite {
private static final Logger LOG = LoggerFactory.getLogger(Streamray.class); public static final String BASE_URI = "https://streamray.com";
public static final String API_URL = "https://beta-api.cams.com";
@Getter
private boolean loggedIn = false;
private StreamrayHttpClient httpClient; 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 @Override
public String getName() { public String getName() {
@ -43,11 +46,11 @@ public class Streamray extends AbstractSite {
@Override @Override
public String getBaseUrl() { public String getBaseUrl() {
return baseUri; return BASE_URI;
} }
public String getApiUrl() { public String getApiUrl() {
return apiURL; return API_URL;
} }
@Override @Override
@ -72,9 +75,14 @@ public class Streamray extends AbstractSite {
@Override @Override
public synchronized boolean login() throws IOException { public synchronized boolean login() throws IOException {
boolean result = getHttpClient().login(); if (!loggedIn) {
LOG.debug("Streamray site login call result: {}", result); boolean result = getHttpClient().login();
return result; log.debug("Streamray site login call result: {}", result);
loggedIn = result;
return result;
} else {
return loggedIn;
}
} }
@Override @Override
@ -107,17 +115,12 @@ public class Streamray extends AbstractSite {
return true; return true;
} }
@Override
public boolean searchRequiresLogin() {
return false;
}
@Override @Override
public List<Model> search(String q) throws IOException, InterruptedException { public List<Model> search(String q) throws IOException, InterruptedException {
if (StringUtil.isBlank(q)) { if (StringUtil.isBlank(q)) {
return Collections.emptyList(); return Collections.emptyList();
} }
String url = getApiUrl() + "/models/new/?limit=30&search=" + URLEncoder.encode(q, "utf-8") + "&order=is_online"; String url = getApiUrl() + "/models/new/?limit=30&search=" + URLEncoder.encode(q, UTF_8) + "&order=is_online";
Request req = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)
.header(USER_AGENT, getConfig().getSettings().httpUserAgent) .header(USER_AGENT, getConfig().getSettings().httpUserAgent)
@ -128,15 +131,12 @@ public class Streamray extends AbstractSite {
if (json.has("results")) { if (json.has("results")) {
List<Model> models = new ArrayList<>(); List<Model> models = new ArrayList<>();
JSONArray results = json.getJSONArray("results"); JSONArray results = json.getJSONArray("results");
if (results.length() == 0) {
return Collections.emptyList();
}
for (int i = 0; i < results.length(); i++) { for (int i = 0; i < results.length(); i++) {
JSONObject result = results.getJSONObject(i); JSONObject result = results.getJSONObject(i);
StreamrayModel model = createModel(result.getString("stream_name")); StreamrayModel model = createModel(result.getString("stream_name"));
String image = result.optString("profile_image"); String image = result.optString("profile_image");
if (StringUtil.isBlank(image)) { if (StringUtil.isBlank(image)) {
image = model.getPreviewURL(); image = getPreviewURL(model.getName());
} }
model.setPreview(image); model.setPreview(image);
models.add(model); models.add(model);
@ -163,7 +163,7 @@ public class Streamray extends AbstractSite {
@Override @Override
public Model createModelFromUrl(String url) { public Model createModelFromUrl(String url) {
Matcher m = Pattern.compile("https://(streamray|cams).com/([_a-zA-Z0-9]+)").matcher(url); Matcher m = Pattern.compile("https://(streamray|cams).com/(\\w+)").matcher(url);
if (m.matches()) { if (m.matches()) {
String modelName = m.group(2); String modelName = m.group(2);
return createModel(modelName); return createModel(modelName);
@ -176,4 +176,98 @@ public class Streamray extends AbstractSite {
public String getAffiliateLink() { public String getAffiliateLink() {
return getBaseUrl(); return getBaseUrl();
} }
public List<StreamrayModel> loadModelList() throws IOException {
return loadModelList(false);
}
public List<StreamrayModel> loadModelList(boolean withLogin) throws IOException {
String url = API_URL + "/won/compressed/";
log.debug("Fetching page {}", url);
StreamrayHttpClient client = (StreamrayHttpClient) getHttpClient();
String token;
Request.Builder builder = new Request.Builder().url(url);
if (withLogin) {
if (login()) {
token = client.getUserToken();
builder.header(AUTHORIZATION, "Bearer " + token);
} else {
return Collections.emptyList();
}
}
builder.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(REFERER, getBaseUrl() + "/")
.header(ORIGIN, getBaseUrl())
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST);
Request req = builder.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);
parseModels(json, models);
return models;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
private void parseModels(JSONObject json, List<StreamrayModel> models) {
JSONArray mapping = json.getJSONArray("mapping");
JSONArray jsonModels = json.getJSONArray("models");
int nameIdx = indexOfProperty(mapping, "stream_name");
int dateIdx = indexOfProperty(mapping, "create_date");
int genIdx = indexOfProperty(mapping, "gender");
int chatTypeIdx = indexOfProperty(mapping, "chat_type");
int favIdx = indexOfProperty(mapping, "is_favorite");
for (var i = 0; i < jsonModels.length(); i++) {
var m = jsonModels.getJSONArray(i);
String name = m.optString(nameIdx);
StreamrayModel model = createModel(name);
try {
LocalDate regDate = LocalDate.parse(m.optString(dateIdx), DateTimeFormatter.BASIC_ISO_DATE);
model.setRegDate(regDate);
} catch (DateTimeParseException e) {
model.setRegDate(LocalDate.EPOCH);
}
model.setOnlineState(mapOnlineState(m.optString(chatTypeIdx)));
String preview = getPreviewURL(name);
model.setPreview(preview);
model.setGender(m.optString(genIdx));
model.setFavorite(m.optBoolean(favIdx));
models.add(model);
}
}
public State mapOnlineState(String status) {
boolean goalShows = Config.getInstance().getSettings().streamrayRecordGoalShows;
return switch (status) {
case "0" -> OFFLINE;
case "1" -> ONLINE;
case "6" -> goalShows ? ONLINE : PRIVATE;
case "2", "3", "4", "7", "10", "11", "12", "13", "14" -> PRIVATE;
default -> UNKNOWN;
};
}
private int indexOfProperty(JSONArray mapping, String key) {
for (var i = 0; i < mapping.length(); i++) {
if (Objects.equals(key, mapping.get(i))) return i;
}
return -1;
}
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;
}
}
} }

View File

@ -39,26 +39,28 @@ public class StreamrayHttpClient extends HttpClient {
private void updateToken() { private void updateToken() {
Request req = new Request.Builder() Request req = new Request.Builder()
.url(Streamray.baseUri) .url(Streamray.BASE_URI)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, "*/*") .header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.build(); .build();
try (Response response = execute(req)) { try (Response response = execute(req)) {
// nothing to do, we just call the base URI to get the cookie
} catch (Exception ex) { } catch (Exception ex) {
// fail silently
} }
} }
private boolean checkLoginSuccess() { private boolean checkLoginSuccess() {
String token = getUserToken(); String token = getUserToken();
Request req = new Request.Builder() Request req = new Request.Builder()
.url(Streamray.apiURL + "/members/me/balance/") .url(Streamray.API_URL + "/members/me/balance/")
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON) .header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage()) .header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(AUTHORIZATION, "Bearer " + token) .header(AUTHORIZATION, "Bearer " + token)
.header(REFERER, Streamray.baseUri + "/") .header(REFERER, Streamray.BASE_URI + "/")
.header(ORIGIN, Streamray.baseUri) .header(ORIGIN, Streamray.BASE_URI)
.build(); .build();
try (Response response = execute(req)) { try (Response response = execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
@ -75,7 +77,7 @@ public class StreamrayHttpClient extends HttpClient {
public String getUserToken() { public String getUserToken() {
try { try {
Cookie cookie = getCookieJar().getCookie(HttpUrl.parse(Streamray.baseUri), "memberToken"); Cookie cookie = getCookieJar().getCookie(HttpUrl.parse(Streamray.BASE_URI), "memberToken");
String token = cookie.value(); String token = cookie.value();
return token; return token;
} catch (Exception e) { } catch (Exception e) {

View File

@ -8,15 +8,15 @@ import ctbrec.io.HttpException;
import ctbrec.recorder.download.RecordingProcess; import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import ctbrec.recorder.download.hls.FfmpegHlsDownload; import ctbrec.recorder.download.hls.FfmpegHlsDownload;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.bind.JAXBException; import javax.xml.bind.JAXBException;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
@ -24,21 +24,28 @@ import java.time.LocalDate;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import static ctbrec.Model.State.*; import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8;
@Slf4j
public class StreamrayModel extends AbstractModel { public class StreamrayModel extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(StreamrayModel.class);
private String status = null; private String status = null;
private String gender = null;
private LocalDate regDate = LocalDate.EPOCH;
private JSONObject modelInfo;
@Getter
@Setter
private String gender = null;
@Setter
private LocalDate regDate = LocalDate.EPOCH;
@Getter
@Setter
private boolean favorite = false;
private transient JSONObject modelInfo;
private transient Instant lastInfoRequest = Instant.EPOCH; private transient Instant lastInfoRequest = Instant.EPOCH;
@Override @Override
@ -48,7 +55,7 @@ public class StreamrayModel extends AbstractModel {
JSONObject json = getModelInfo(); JSONObject json = getModelInfo();
if (json.has("online")) { if (json.has("online")) {
status = json.optString("online"); status = json.optString("online");
mapOnlineState(status); setOnlineState(((Streamray) site).mapOnlineState(status));
} }
} catch (Exception e) { } catch (Exception e) {
setOnlineState(UNKNOWN); setOnlineState(UNKNOWN);
@ -57,22 +64,9 @@ public class StreamrayModel extends AbstractModel {
return onlineState == ONLINE; 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 @Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException { public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if (failFast && onlineState != UNKNOWN) { if (!failFast || onlineState == UNKNOWN) {
return onlineState;
} else {
try { try {
onlineState = isOnline(true) ? ONLINE : OFFLINE; onlineState = isOnline(true) ? ONLINE : OFFLINE;
} catch (InterruptedException e) { } catch (InterruptedException e) {
@ -81,8 +75,8 @@ public class StreamrayModel extends AbstractModel {
} catch (IOException | ExecutionException e) { } catch (IOException | ExecutionException e) {
onlineState = OFFLINE; onlineState = OFFLINE;
} }
return onlineState;
} }
return onlineState;
} }
@Override @Override
@ -97,7 +91,7 @@ public class StreamrayModel extends AbstractModel {
src.bandwidth = 0; src.bandwidth = 0;
sources.add(src); sources.add(src);
} catch (IOException e) { } catch (IOException e) {
LOG.error("Can not get stream sources for {}", getName()); log.error("Can not get stream sources for {}", getName());
} }
return sources; return sources;
} }
@ -115,9 +109,7 @@ public class StreamrayModel extends AbstractModel {
} }
private JSONObject getModelInfo() throws IOException { private JSONObject getModelInfo() throws IOException {
if (Duration.between(lastInfoRequest, Instant.now()).getSeconds() < 5) { if (modelInfo == null || Duration.between(lastInfoRequest, Instant.now()).getSeconds() < 5) {
modelInfo = Optional.ofNullable(modelInfo).orElse(loadModelInfo());
} else {
modelInfo = loadModelInfo(); modelInfo = loadModelInfo();
} }
return modelInfo; return modelInfo;
@ -134,24 +126,13 @@ public class StreamrayModel extends AbstractModel {
.build(); .build();
try (Response response = site.getHttpClient().execute(req)) { try (Response response = site.getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
JSONObject jsonResponse = new JSONObject(response.body().string()); return new JSONObject(response.body().string());
return jsonResponse;
} else { } else {
throw new HttpException(response.code(), response.message()); 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 @Override
public RecordingProcess createDownload() { public RecordingProcess createDownload() {
return new FfmpegHlsDownload(getSite().getHttpClient()); return new FfmpegHlsDownload(getSite().getHttpClient());
@ -179,18 +160,6 @@ public class StreamrayModel extends AbstractModel {
modelInfo = null; 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() { public boolean isNew() {
return ChronoUnit.DAYS.between(this.regDate, LocalDate.now()) < 30; return ChronoUnit.DAYS.between(this.regDate, LocalDate.now()) < 30;
} }