ctbrec-5.3.2-experimental/common/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java

358 lines
14 KiB
Java

package ctbrec.sites.chaturbate;
import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.*;
import java.io.ByteArrayInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
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.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class ChaturbateModel extends AbstractModel { // NOSONAR
private static final String PUBLIC = "public";
private static final Logger LOG = LoggerFactory.getLogger(ChaturbateModel.class);
private int[] resolution = new int[2];
private transient StreamInfo streamInfo;
private long streamInfoTimestamp = 0;
/**
* This constructor exists only for deserialization. Please don't call it directly
*/
public ChaturbateModel() {
}
ChaturbateModel(Chaturbate site) {
this.site = site;
}
@Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
String roomStatus;
if(ignoreCache) {
StreamInfo info = loadStreamInfo();
roomStatus = Optional.ofNullable(info).map(i -> i.room_status).orElse("");
LOG.trace("Model {} room status: {}", getName(), info.room_status);
} else {
StreamInfo info = getStreamInfo(true);
roomStatus = Optional.ofNullable(info).map(i -> i.room_status).orElse("");
}
return Objects.equals(PUBLIC, roomStatus);
}
@Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
if(failFast) {
return resolution;
}
try {
resolution = getResolution();
} catch(Exception e) {
throw new ExecutionException(e);
}
return resolution;
}
/**
* Invalidates the entries in StreamInfo and resolution cache for this model
* and thus causes causes the LoadingCache to update them
*/
@Override
public void invalidateCacheEntries() {
streamInfo = null;
}
public State getOnlineState() throws IOException, ExecutionException {
return getOnlineState(false);
}
@Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if(failFast) {
setOnlineStateByRoomStatus(Optional.ofNullable(streamInfo).map(si -> si.room_status).orElse("Unknown"));
} else {
try {
streamInfo = loadStreamInfo();
setOnlineStateByRoomStatus(streamInfo.room_status);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ExecutionException(e);
}
}
return onlineState;
}
private void setOnlineStateByRoomStatus(String roomStatus) {
if (roomStatus != null) {
switch (roomStatus) {
case PUBLIC:
case "Unknown":
onlineState = ONLINE;
break;
case "offline":
onlineState = OFFLINE;
break;
case "private":
case "hidden":
case "password protected":
onlineState = PRIVATE;
break;
case "away":
onlineState = AWAY;
break;
case "group":
onlineState = State.GROUP;
break;
default:
LOG.debug("Unknown show type {}", roomStatus);
onlineState = State.UNKNOWN;
}
}
}
@Override
public void receiveTip(Double tokens) throws IOException {
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
RequestBody body = new FormBody.Builder()
.add("csrfmiddlewaretoken", ((ChaturbateHttpClient)getSite().getHttpClient()).getToken())
.add("tip_amount", Integer.toString(tokens.intValue()))
.add("tip_room_type", PUBLIC)
.build();
Request req = new Request.Builder()
.url("https://chaturbate.com/tipping/send_tip/"+getName()+"/")
.post(body)
.header(REFERER, "https://chaturbate.com/"+getName()+"/")
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
try (Response response = getSite().getHttpClient().execute(req)) {
if (!response.isSuccessful()) {
throw new IOException(response.code() + " " + response.message());
}
}
}
}
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
try {
streamInfo = loadStreamInfo();
MasterPlaylist masterPlaylist = getMasterPlaylist();
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;
String masterUrl = streamInfo.url;
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
String segmentUri = baseUrl + playlist.getUri();
src.mediaPlaylistUrl = segmentUri;
if(src.mediaPlaylistUrl.contains("?")) {
src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?'));
}
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
sources.add(src);
}
}
return sources;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ExecutionException(e);
}
}
@Override
public boolean follow() throws IOException {
return follow(true);
}
@Override
public boolean unfollow() throws IOException {
return follow(false);
}
private boolean follow(boolean follow) throws IOException {
Request req = new Request.Builder()
.url(getUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
try (Response resp = site.getHttpClient().execute(req)) {
// do an initial request to get cookies
}
String url = null;
if (follow) {
url = getSite().getBaseUrl() + "/follow/follow/" + getName() + "/";
} else {
url = getSite().getBaseUrl() + "/follow/unfollow/" + getName() + "/";
}
RequestBody body = RequestBody.create(new byte[0]);
req = new Request.Builder()
.url(url)
.method("POST", body)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, "en-US,en;q=0.5")
.header(REFERER, getUrl())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header("X-CSRFToken", ((ChaturbateHttpClient)site.getHttpClient()).getToken())
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response resp2 = site.getHttpClient().execute(req)) {
if (resp2.isSuccessful()) {
String responseBody = resp2.body().string();
JSONObject json = new JSONObject(responseBody);
if (!json.has("following")) {
LOG.debug(responseBody);
throw new IOException("Response was " + responseBody.substring(0, Math.min(responseBody.length(), 500)));
} else {
LOG.debug("Follow/Unfollow -> {}", responseBody);
return json.getBoolean("following") == follow;
}
} else {
throw new HttpException(resp2.code(), resp2.message());
}
}
}
private StreamInfo getStreamInfo() throws IOException, InterruptedException {
return getStreamInfo(false);
}
private StreamInfo getStreamInfo(boolean failFast) throws IOException, InterruptedException {
if(failFast) {
return streamInfo;
} else {
return Optional.ofNullable(streamInfo).orElse(loadStreamInfo());
}
}
private StreamInfo loadStreamInfo() throws IOException, InterruptedException {
long now = System.currentTimeMillis();
long streamInfoAge = now - streamInfoTimestamp;
if (streamInfo != null && streamInfoAge < 5000) {
return streamInfo;
}
RequestBody body = new FormBody.Builder()
.add("room_slug", getName())
.add("bandwidth", "high")
.build();
Request req = new Request.Builder()
.url(getSite().getBaseUrl() + "/get_edge_hls_url_ajax/")
.post(body)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.build();
try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String content = response.body().string();
LOG.trace("Raw stream info: {}", content);
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<StreamInfo> adapter = moshi.adapter(StreamInfo.class);
streamInfo = adapter.fromJson(content);
streamInfoTimestamp = System.currentTimeMillis();
return streamInfo;
} else {
int code = response.code();
String message = response.message();
throw new HttpException(code, message);
}
}
}
private int[] getResolution() throws IOException, ParseException, PlaylistException, InterruptedException {
int[] res = new int[2];
if(!getStreamInfo().url.startsWith("http")) {
return res;
}
EOFException ex = null;
for(int i=0; i<2; i++) {
try {
MasterPlaylist master = getMasterPlaylist();
for (PlaylistData playlistData : master.getPlaylists()) {
if(playlistData.hasStreamInfo() && playlistData.getStreamInfo().hasResolution()) {
int h = playlistData.getStreamInfo().getResolution().height;
int w = playlistData.getStreamInfo().getResolution().width;
if(w > res[1]) {
res[0] = w;
res[1] = h;
}
}
}
ex = null;
break; // this attempt worked, exit loop
} catch(EOFException e) {
// the cause might be, that the playlist url in streaminfo is outdated,
// so let's remove it from cache and retry in the next iteration
ex = e;
}
}
if(ex != null) {
throw ex;
}
return res;
}
public MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException, InterruptedException {
return getMasterPlaylist(getStreamInfo());
}
private MasterPlaylist getMasterPlaylist(StreamInfo streamInfo) throws IOException, ParseException, PlaylistException {
LOG.trace("Loading master playlist {}", streamInfo.url);
Request req = new Request.Builder()
.url(streamInfo.url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = response.body().string();
LOG.trace(body);
InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8));
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse();
MasterPlaylist master = playlist.getMasterPlaylist();
return master;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
}