ctbrec-5.3.2-experimental/common/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java

371 lines
13 KiB
Java

package ctbrec.sites.mfc;
import static ctbrec.io.HttpConstants.*;
import static java.util.Optional.*;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import javax.xml.bind.JAXBException;
import org.jsoup.nodes.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.io.HtmlParser;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.HttpHeaderFactory;
import ctbrec.recorder.download.HttpHeaderFactoryImpl;
import ctbrec.recorder.download.StreamSource;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class MyFreeCamsModel extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(MyFreeCamsModel.class);
private int uid = -1; // undefined
private String streamUrl;
private double camScore;
private int viewerCount;
private ctbrec.sites.mfc.State state;
private int[] resolution = new int[2];
/**
* This constructor exists only for deserialization. Please don't call it directly
*/
public MyFreeCamsModel() {}
MyFreeCamsModel(MyFreeCams site) {
this.site = site;
}
@Override
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
MyFreeCamsClient.getInstance().update(this);
return state == ctbrec.sites.mfc.State.ONLINE;
}
@Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
return isOnline();
}
@Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if(state == null) {
return State.UNKNOWN;
}
switch(state) {
case ONLINE:
case RECORDING:
return ctbrec.Model.State.ONLINE;
case AWAY:
return ctbrec.Model.State.AWAY;
case PRIVATE:
return ctbrec.Model.State.PRIVATE;
case GROUP_SHOW:
return ctbrec.Model.State.GROUP;
case OFFLINE:
case CAMOFF:
case UNKNOWN:
return ctbrec.Model.State.OFFLINE;
default:
LOG.debug("State {} is not mapped", this.state);
return ctbrec.Model.State.UNKNOWN;
}
}
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
return getStreamSourceProvider().getStreamSources(updateStreamUrl());
}
private StreamSourceProvider getStreamSourceProvider() {
return new HlsStreamSourceProvider(getSite().getHttpClient());
}
private String updateStreamUrl() {
if(streamUrl == null) {
MyFreeCams mfc = (MyFreeCams) getSite();
mfc.getClient().update(this);
}
return streamUrl;
}
@Override
public void invalidateCacheEntries() {
resolution = null;
}
@Override
public void receiveTip(Double tokens) throws IOException {
String tipUrl = MyFreeCams.baseUrl + "/php/tip.php";
String initUrl = tipUrl + "?request=tip&username="+getName()+"&broadcaster_id="+getUid();
Request req = new Request.Builder()
.url(initUrl)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(CONNECTION, KEEP_ALIVE)
.build();
try(Response resp = site.getHttpClient().execute(req)) {
if(resp.isSuccessful()) {
String page = resp.body().string();
Element hiddenInput = HtmlParser.getTag(page, "input[name=token]");
String token = hiddenInput.attr("value");
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
RequestBody body = new FormBody.Builder()
.add("token", token)
.add("broadcaster_id", Integer.toString(uid))
.add("tip_value", Integer.toString(tokens.intValue()))
.add("submit_tip", "1")
.add("anonymous", "")
.add("public", "1")
.add("public_comment", "1")
.add("hide_amount", "0")
.add("silent", "")
.add("comment", "")
.add("mode", "")
.add("submit", " Confirm & Close Window")
.build();
req = new Request.Builder()
.url(tipUrl)
.post(body)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(CONNECTION, KEEP_ALIVE)
.header(REFERER, initUrl)
.build();
try(Response response = site.getHttpClient().execute(req)) {
if(!response.isSuccessful()) {
throw new HttpException(response.code(), response.message());
}
}
}
} else {
throw new HttpException(resp.code(), resp.message());
}
}
}
@Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
if (!failFast && streamUrl != null) {
try {
List<StreamSource> streamSources = getStreamSources();
Collections.sort(streamSources);
StreamSource best = streamSources.get(streamSources.size() - 1);
resolution = new int[] { best.width, best.height };
} catch (JAXBException | ParseException | PlaylistException e) {
LOG.warn("Couldn't determine stream resolution - {}", e.getMessage());
} catch (ExecutionException | IOException e) {
LOG.error("Couldn't determine stream resolution", e);
}
}
return resolution;
}
public void setStreamUrl(String streamUrl) {
this.streamUrl = streamUrl;
}
public String getStreamUrl() {
return streamUrl;
}
public double getCamScore() {
return camScore;
}
public void setCamScore(double camScore) {
this.camScore = camScore;
}
public boolean isNew() {
MyFreeCams mfc = (MyFreeCams) getSite();
SessionState sessionState = mfc.getClient().getSessionState(this);
return ofNullable(sessionState).map(SessionState::getM).map(Model::getNewModel).orElse(0) == 1;
}
public void setMfcState(ctbrec.sites.mfc.State state) {
this.state = state;
}
@Override
public void setName(String name) {
if(getName() != null && name != null && !getName().equals(name)) {
LOG.debug("Model name changed {} -> {}", getName(), name);
setUrl("https://profiles.myfreecams.com/" + name);
}
super.setName(name);
}
public void update(SessionState state, String streamUrl) {
uid = state.getUid();
setName(state.getNm());
setMfcState(ctbrec.sites.mfc.State.of(state.getVs()));
setStreamUrl(streamUrl);
setCamScore(ofNullable(state.getM()).map(Model::getCamscore).orElse(0.0));
// preview
String uidString = state.getUid().toString();
String uidStart = uidString.substring(0, 3);
String previewUrl = "https://img.mfcimg.com/photos2/"+uidStart+'/'+uidString+"/avatar.300x300.jpg";
if(MyFreeCamsModel.this.state == ctbrec.sites.mfc.State.ONLINE) {
try {
previewUrl = getLivePreviewUrl(state);
} catch(Exception e) {
LOG.error("Couldn't get live preview. Falling back to avatar", e);
}
}
setPreview(previewUrl);
// tags
ofNullable(state.getM()).map(Model::getTags).ifPresent(tags -> {
ArrayList<String> t = new ArrayList<>();
t.addAll(tags);
setTags(t);
});
// description
ofNullable(state.getM()).map(Model::getTopic).ifPresent(topic -> {
try {
setDescription(URLDecoder.decode(topic, "utf-8"));
} catch (UnsupportedEncodingException e) {
LOG.warn("Couldn't url decode topic", e);
}
});
viewerCount = ofNullable(state.getM()).map(Model::getRc).orElse(0);
}
private String getLivePreviewUrl(SessionState state) {
String previewUrl;
int userChannel = 100000000 + state.getUid();
int camserv = state.getU().getCamserv();
String server;
ServerConfig sc = ((MyFreeCams)site).getClient().getServerConfig();
String phase = state.getU().getPhase();
if(sc.isOnNgServer(state)) {
server = sc.ngVideoServers.get(Integer.toString(camserv));
camserv = toCamServ(server);
previewUrl = toPreviewUrl(camserv, phase, userChannel);
} else if(sc.isOnWzObsVideoServer(state)) {
server = sc.wzobsServers.get(Integer.toString(camserv));
camserv = toCamServ(server);
previewUrl = toPreviewUrl(camserv, phase, userChannel);
} else if(sc.isOnHtml5VideoServer(state)) {
server = sc.h5Servers.get(Integer.toString(camserv));
camserv = toCamServ(server);
previewUrl = toPreviewUrl(camserv, userChannel);
} else {
if(camserv > 500) camserv -= 500;
previewUrl = toPreviewUrl(camserv, userChannel);
}
return previewUrl;
}
private String toPreviewUrl(int camserv, String phase, int userChannel) {
return "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + phase + '_' + userChannel;
}
private String toPreviewUrl(int camserv, int userChannel) {
return "https://snap.mfcimg.com/snapimg/" + camserv + "/320x240/mfc_" + userChannel;
}
private int toCamServ(String server) {
return Integer.parseInt(server.replaceAll("[^0-9]+", ""));
}
@Override
public boolean follow() {
return ((MyFreeCams)site).getClient().follow(getUid());
}
@Override
public boolean unfollow() {
return ((MyFreeCams)site).getClient().unfollow(getUid());
}
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public int getViewerCount() {
return viewerCount;
}
public void setViewerCount(int viewerCount) {
this.viewerCount = viewerCount;
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
reader.nextName();
uid = reader.nextInt();
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
writer.name("uid").value(uid);
}
@Override
public Download createDownload() {
if(streamUrl == null) {
updateStreamUrl();
}
return super.createDownload();
// if(isHlsStream()) {
// return super.createDownload();
// } else {
// return new MyFreeCamsWebrtcDownload(uid, streamUrl, ((MyFreeCams)site).getClient());
// }
}
@Override
public HttpHeaderFactory getHttpHeaderFactory() {
HttpHeaderFactoryImpl fac = new HttpHeaderFactoryImpl();
Map<String, String> headers = new HashMap<>();
headers.put(ACCEPT, "*/*");
headers.put(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage());
headers.put(CONNECTION, KEEP_ALIVE);
if (getSite() != null) {
headers.put(ORIGIN, getSite().getBaseUrl());
headers.put(REFERER, getSite().getBaseUrl());
}
headers.put(USER_AGENT, Config.getInstance().getSettings().httpUserAgent);
fac.setMasterPlaylistHeaders(headers);
fac.setSegmentPlaylistHeaders(headers);
fac.setSegmentHeaders(headers);
return fac;
}
}