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 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 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 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 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; } }