ctbrec-5.3.2-experimental/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java

501 lines
20 KiB
Java

package ctbrec.sites.flirt4free;
import static ctbrec.io.HttpConstants.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
public class Flirt4FreeModel extends AbstractModel {
private static final transient Logger LOG = LoggerFactory.getLogger(Flirt4FreeModel.class);
private String id;
private String chatHost;
private String chatPort;
private String chatToken;
private String streamHost;
private String streamUrl;
int[] resolution = new int[2];
private Object monitor = new Object();
private boolean online = false;
private boolean isInteractiveShow = false;
private boolean isNew = false;
private String userIdt = "";
private String userIp = "0.0.0.0";
private static Semaphore requestThrottle = new Semaphore(2, true);
private static volatile long lastRequest = 0;
private long lastOnlineRequest = 0;
@Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
long now = System.currentTimeMillis();
long timeSinceLastCheck = now - lastOnlineRequest;
if (ignoreCache && timeSinceLastCheck > TimeUnit.MINUTES.toMillis(1)) {
String url = "https://ws.vs3.com/rooms/check-model-status.php?model_name=" + getName();
acquireSlot();
try {
Request request = new Request.Builder()
.url(url)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
.header(REFERER, getUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response response = getSite().getHttpClient().execute(request)) {
if (response.isSuccessful()) {
String body = response.body().string();
if (body.trim().isEmpty()) {
return false;
}
JSONObject json = new JSONObject(body);
//LOG.debug("check model status: {}", json.toString(2));
online = Objects.equals(json.optString("status"), "online");
id = String.valueOf(json.get("model_id"));
if (online) {
try {
loadStreamUrl();
} catch (Exception e) {
online = false;
onlineState = Model.State.OFFLINE;
}
}
} else {
throw new HttpException(response.code(), response.message());
}
}
} finally {
lastOnlineRequest = System.currentTimeMillis();
releaseSlot();
}
}
return online;
}
private void loadModelInfo() throws IOException, InterruptedException {
String url = getSite().getBaseUrl() + "/webservices/chat-room-interface.php?a=login_room&model_id=" + id;
LOG.trace("Loading url {}", url);
Request request = new Request.Builder()
.url(url)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
.header(REFERER, getUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response response = getSite().getHttpClient().execute(request)) {
if (response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
if (json.optString("status").equals("success")) {
// LOG.debug("chat-room-interface {}", json.toString(2));
JSONObject config = json.getJSONObject("config");
JSONObject performer = config.getJSONObject("performer");
setName(performer.optString("name_seo", "n/a"));
setDisplayName(performer.optString("name", "n/a"));
setUrl(getSite().getBaseUrl() + "/rooms/" + getName() + '/');
JSONObject room = config.getJSONObject("room");
chatHost = room.getString("host");
chatPort = room.getString("port_to_be");
chatToken = json.getString("token_enc");
JSONObject user = config.getJSONObject("user");
userIp = user.getString("ip");
} else {
LOG.trace("Loading model info failed. Assuming model {} is offline", getName());
online = false;
onlineState = Model.State.OFFLINE;
}
} else {
throw new HttpException(response.code(), response.message());
}
}
}
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
return getStreamSources(true);
}
private List<StreamSource> getStreamSources(boolean withWebsocket) throws IOException, ExecutionException, ParseException, PlaylistException {
MasterPlaylist masterPlaylist = null;
try {
if (withWebsocket) {
acquireSlot();
try {
loadStreamUrl();
} finally {
releaseSlot();
}
}
masterPlaylist = getMasterPlaylist();
} catch (InterruptedException e) {
throw new ExecutionException(e);
}
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 = "https://manifest.vscdns.com/" + playlist.getUri();
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
sources.add(src);
}
}
return sources;
}
public MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException, InterruptedException {
LOG.trace("Loading master playlist {}", streamUrl);
Request req = new Request.Builder()
.url(streamUrl)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
.header(REFERER, getUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
acquireSlot();
try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) {
InputStream inputStream = response.body().byteStream();
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());
}
} finally {
releaseSlot();
}
}
private void loadStreamUrl() throws IOException, InterruptedException {
loadModelInfo();
Objects.requireNonNull(chatHost, "chatHost is null");
String h = chatHost.replaceAll("chat", "chat-vip");
String url = "https://" + h + "/chat?token=" + URLEncoder.encode(chatToken, "utf-8") + "&port_to_be=" + chatPort;
LOG.trace("Opening chat websocket {}", url);
Request req = new Request.Builder()
.url(url)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
.header(REFERER, getUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
getSite().getHttpClient().newWebSocket(req, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
LOG.trace("Chat websocket for {} opened", getName());
}
@Override
public void onMessage(WebSocket webSocket, String text) {
LOG.trace("Chat wbesocket for {}: {}", getName(), text);
JSONObject json = new JSONObject(text);
//LOG.debug("WS {}", text);
if (json.optString("command").equals("8011")) {
JSONObject data = json.getJSONObject("data");
streamHost = data.getString("stream_host"); // TODO look, if the stream_host is equal to the one encoded in base64 in some of the ajax requests (parameters)
online = true;
isInteractiveShow = data.optString("devices").equals("1");
if(data.optString("room_state").equals("P")) {
onlineState = Model.State.PRIVATE;
online = false;
}
if(data.optString("room_state").equals("0") && data.optString("login_group_id").equals("14")) {
onlineState = Model.State.GROUP;
online = false;
}
try {
resolution[0] = Integer.parseInt(data.getString("stream_width"));
resolution[1] = Integer.parseInt(data.getString("stream_height"));
} catch(Exception e) {
LOG.warn("Couldn't determine stream resolution", e);
}
webSocket.close(1000, "");
}
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
LOG.error("Chat websocket for {} failed", getName(), t);
synchronized (monitor) {
monitor.notify();
}
response.close();
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
LOG.trace("Chat websocket for {} closed {} {}", getName(), code, reason);
synchronized (monitor) {
monitor.notify();
}
}
});
synchronized (monitor) {
monitor.wait(10_000);
if (streamHost == null) {
throw new RuntimeException("Couldn't determine streaming server for model " + getName());
} else {
streamUrl = "https://manifest.vscdns.com/manifest.m3u8.m3u8?key=nil&provider=level3&secure=true&host=" + streamHost + "&model_id=" + id;
}
}
}
@Override
public void invalidateCacheEntries() {
}
@Override
public void receiveTip(Double tokens) throws IOException {
try {
// if(tokens < 50 || tokens > 750000) {
// throw new RuntimeException("Tip amount has to be between 50 and 750000");
// }
// make sure we are logged in and all necessary model data is available
getSite().login();
acquireSlot();
try {
loadStreamUrl();
} catch (InterruptedException e) {
throw new IOException("Couldn't send tip", e);
} finally {
releaseSlot();
}
// send the tip
int giftId = isInteractiveShow ? 775 : 171;
int amount = tokens.intValue();
LOG.debug("Sending tip of {} to {}", amount, getName());
String url = "https://ws.vs3.com/rooms/send-tip.php?" +
"gift_id=" + giftId +
"&num_credits=" + amount +
"&userId=" + getUserIdt() +
"&username=" + Config.getInstance().getSettings().flirt4freeUsername +
"&userIP=" + userIp +
"&anonymous=N&response_type=json" +
"&t=" + System.currentTimeMillis();
LOG.debug("Trying to send tip: {}", url);
Request req = new Request.Builder()
.url(url)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
.header(REFERER, getUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(REFERER, getUrl())
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
if (json.optInt("success") != 1) {
String msg = json.optString("message");
if (json.has("error_message")) {
msg = json.getString("error_message");
}
LOG.error("Sending tip failed: {}", msg);
LOG.debug("Response: {}", json.toString(2));
throw new IOException(msg);
}
} else {
throw new HttpException(response.code(), response.message());
}
}
} catch (InterruptedException e) {
throw new IOException("Couldn't acquire request slot", e);
}
}
private String getUserIdt() throws IOException, InterruptedException {
if (userIdt.isEmpty()) {
acquireSlot();
try {
Request req = new Request.Builder()
.url(getUrl())
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
.header(REFERER, getUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = response.body().string();
Matcher m = Pattern.compile("idt\\s*:\\s*'(.*?)',").matcher(body);
if (m.find()) {
userIdt = m.group(1);
} else {
throw new IOException("userIdt not found on HTML page");
}
} else {
throw new HttpException(response.code(), response.message());
}
}
} finally {
releaseSlot();
}
}
return userIdt;
}
@Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
if(failFast) {
return resolution;
} else {
if(streamUrl != null) {
try {
List<StreamSource> streamSources = getStreamSources(false);
Collections.sort(streamSources);
StreamSource best = streamSources.get(streamSources.size()-1);
resolution = new int[] {best.width, best.height};
} catch (IOException | ParseException | PlaylistException e) {
throw new ExecutionException("Couldn't determine stream resolution", e);
}
}
return resolution;
}
}
@Override
public boolean follow() throws IOException {
try {
return changeFavoriteStatus(true);
} catch (InterruptedException e) {
throw new IOException("Couldn't change follow status for model " + getName(), e);
}
}
@Override
public boolean unfollow() throws IOException {
try {
isOnline(true);
return changeFavoriteStatus(false);
} catch (ExecutionException | InterruptedException e) {
throw new IOException("Couldn't change follow status for model " + getName(), e);
}
}
private boolean changeFavoriteStatus(boolean add) throws IOException, InterruptedException {
getSite().login();
acquireSlot();
try {
loadModelInfo();
} finally {
releaseSlot();
}
String url = getSite().getBaseUrl() + "/external.php?a=" +
(add ? "add_favorite" : "delete_favorite") +
"&id=" + id +
"&name=" + getDisplayName() +
"&t=" + System.currentTimeMillis();
LOG.debug("Sending follow/unfollow request: {}", url);
Request req = new Request.Builder()
.url(url)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
.header(REFERER, getUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = response.body().string();
LOG.debug("Follow/Unfollow response: {}", body);
return Objects.equals(body, "1");
} else {
throw new HttpException(response.code(), response.message());
}
}
}
public void setId(String id) {
this.id = id;
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
reader.nextName();
id = reader.nextString();
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
writer.name("id").value(id);
}
public void setStreamUrl(String streamUrl) {
this.streamUrl = streamUrl;
}
public void setOnline(boolean b) {
online = b;
}
public boolean isNew() {
return isNew;
}
public void setNew(boolean isNew) {
this.isNew = isNew;
}
private void acquireSlot() throws InterruptedException {
//LOG.debug("Acquire: {}", requestThrottle.availablePermits());
requestThrottle.acquire();
long now = System.currentTimeMillis();
long millisSinceLastRequest = now - lastRequest;
if(millisSinceLastRequest < 500) {
//LOG.debug("Sleeping: {}", (500-millisSinceLastRequest));
Thread.sleep(500 - millisSinceLastRequest);
}
}
private void releaseSlot() {
lastRequest = System.currentTimeMillis();
requestThrottle.release();
//LOG.debug("Release: {}", requestThrottle.availablePermits());
}
}