371 lines
13 KiB
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;
|
|
}
|
|
}
|