diff --git a/pom.xml b/pom.xml index 5049ecf4..b2050d21 100644 --- a/pom.xml +++ b/pom.xml @@ -140,6 +140,11 @@ moshi 1.5.0 + + org.json + json + 20180130 + org.slf4j slf4j-api diff --git a/src/main/java/ctbrec/io/HttpClient.java b/src/main/java/ctbrec/io/HttpClient.java index 7a3dd0ca..57129ac4 100644 --- a/src/main/java/ctbrec/io/HttpClient.java +++ b/src/main/java/ctbrec/io/HttpClient.java @@ -9,8 +9,8 @@ import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.Settings.ProxyType; -import ctbrec.ui.CookieJarImpl; import ctbrec.ui.CamrecApplication; +import ctbrec.ui.CookieJarImpl; import ctbrec.ui.HtmlParser; import okhttp3.ConnectionPool; import okhttp3.Cookie; @@ -34,8 +34,8 @@ public class HttpClient { loadProxySettings(); client = new OkHttpClient.Builder() .cookieJar(cookieJar) - .connectTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.SECONDS) - .readTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.SECONDS) + .connectTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS) + .readTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS) .connectionPool(new ConnectionPool(50, 10, TimeUnit.MINUTES)) //.addInterceptor(new LoggingInterceptor()) .build(); diff --git a/src/main/java/ctbrec/recorder/download/StreamSource.java b/src/main/java/ctbrec/recorder/download/StreamSource.java index 2ed7a8f8..1968df81 100644 --- a/src/main/java/ctbrec/recorder/download/StreamSource.java +++ b/src/main/java/ctbrec/recorder/download/StreamSource.java @@ -4,6 +4,7 @@ import java.text.DecimalFormat; public class StreamSource implements Comparable { public int bandwidth; + public int width; public int height; public String mediaPlaylistUrl; @@ -15,6 +16,14 @@ public class StreamSource implements Comparable { this.bandwidth = bandwidth; } + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + public int getHeight() { return height; } diff --git a/src/main/java/ctbrec/sites/mfc/Fcext.java b/src/main/java/ctbrec/sites/mfc/Fcext.java new file mode 100644 index 00000000..6d61238b --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/Fcext.java @@ -0,0 +1,46 @@ +package ctbrec.sites.mfc; + +import java.util.HashMap; +import java.util.Map; + +public class Fcext { + + private String sm; + private Integer sfw; + private Map additionalProperties = new HashMap(); + + public String getSm() { + return sm; + } + + public void setSm(String sm) { + this.sm = sm; + } + + public Integer getSfw() { + return sfw; + } + + public void setSfw(Integer sfw) { + this.sfw = sfw; + } + + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + + public void merge(Fcext fcext) { + if(fcext == null) { + return; + } + + sm = fcext.sm != null ? fcext.sm : sm; + sfw = fcext.sfw != null ? fcext.sfw : sfw; + additionalProperties.putAll(fcext.additionalProperties); + } + +} diff --git a/src/main/java/ctbrec/sites/mfc/FriendsUpdateService.java b/src/main/java/ctbrec/sites/mfc/FriendsUpdateService.java new file mode 100644 index 00000000..8db70760 --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/FriendsUpdateService.java @@ -0,0 +1,92 @@ +package ctbrec.sites.mfc; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Model; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import okhttp3.Response; + +public class FriendsUpdateService extends PaginatedScheduledService { + + private static final transient Logger LOG = LoggerFactory.getLogger(FriendsUpdateService.class); + private MyFreeCams myFreeCams; + + public FriendsUpdateService(MyFreeCams myFreeCams) { + this.myFreeCams = myFreeCams; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + List models = new ArrayList<>(); + String url = myFreeCams.getBaseUrl() + "/php/manage_lists2.php?passcode=&list_type=friends&data_mode=online&get_user_list=1"; + Request req = new Request.Builder() + .url(url) + .header("Referer", myFreeCams.getBaseUrl()) + .build(); + Response resp = MyFreeCams.httpClient.newCall(req).execute(); + if(resp.isSuccessful()) { + String json = resp.body().string().substring(4); + JSONObject object = new JSONObject(json); + for (String key : object.keySet()) { + int uid = Integer.parseInt(key); + MyFreeCamsModel model = MyFreeCamsClient.getInstance().getModel(uid); + if(model == null) { + JSONObject modelObject = object.getJSONObject(key); + String name = modelObject.getString("u"); + model = myFreeCams.createModel(name); + SessionState st = new SessionState(); + st.setM(new ctbrec.sites.mfc.Model()); + st.getM().setCamscore(0.0); + st.setU(new User()); + st.setUid(uid); + st.setLv(modelObject.getInt("lv")); + st.setVs(127); + model.update(st); + } + models.add(model); + } + } else { + LOG.error("Couldn't load friends list {} {}", resp.code(), resp.message()); + resp.close(); + } + return models.stream() + .sorted((a, b) -> { + try { + if(a.isOnline() && b.isOnline() || !a.isOnline() && !b.isOnline()) { + return a.getName().compareTo(b.getName()); + } else { + if(a.isOnline()) { + return -1; + } + if(b.isOnline()) { + return 1; + } + } + } catch (IOException | ExecutionException | InterruptedException e) { + LOG.warn("Couldn't sort friends list", e); + return 0; + } + return 0; + }) + .skip((page-1) * 50) + .limit(50) + .collect(Collectors.toList()); + } + }; + } + +} diff --git a/src/main/java/ctbrec/sites/mfc/Message.java b/src/main/java/ctbrec/sites/mfc/Message.java new file mode 100644 index 00000000..c1ee53ea --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/Message.java @@ -0,0 +1,73 @@ +package ctbrec.sites.mfc; + +public class Message { + private int type; + private int sender; + private int receiver; + private int arg1; + private int arg2; + private String message; + + public Message(int type, int sender, int receiver, int arg1, int arg2, String message) { + super(); + this.type = type; + this.sender = sender; + this.receiver = receiver; + this.arg1 = arg1; + this.arg2 = arg2; + this.message = message; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + public int getSender() { + return sender; + } + + public void setSender(int sender) { + this.sender = sender; + } + + public int getReceiver() { + return receiver; + } + + public void setReceiver(int receiver) { + this.receiver = receiver; + } + + public int getArg1() { + return arg1; + } + + public void setArg1(int arg1) { + this.arg1 = arg1; + } + + public int getArg2() { + return arg2; + } + + public void setArg2(int arg2) { + this.arg2 = arg2; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return type + " " + sender + " " + receiver + " " + arg1 + " " + arg2 + " " + message; + } +} diff --git a/src/main/java/ctbrec/sites/mfc/MessageTypes.java b/src/main/java/ctbrec/sites/mfc/MessageTypes.java new file mode 100644 index 00000000..34d85fec --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/MessageTypes.java @@ -0,0 +1,99 @@ +package ctbrec.sites.mfc; + +public class MessageTypes { + public static final int CLIENT_DISCONNECTED = -5; + public static final int CLIENT_MODELSLOADED = -4; + public static final int CLIENT_CONNECTED = -3; + public static final int ANY = -2; + public static final int UNKNOWN = -1; + public static final int NULL = 0; + public static final int LOGIN = 1; + public static final int ADDFRIEND = 2; + public static final int PMESG = 3; + public static final int STATUS = 4; + public static final int DETAILS = 5; + public static final int TOKENINC = 6; + public static final int ADDIGNORE = 7; + public static final int PRIVACY = 8; + public static final int ADDFRIENDREQ = 9; + public static final int USERNAMELOOKUP = 10; + public static final int ZBAN = 11; + public static final int BROADCASTNEWS = 12; + public static final int ANNOUNCE = 13; + public static final int MANAGELIST = 14; + public static final int INBOX = 15; + public static final int GWCONNECT = 16; + public static final int RELOADSETTINGS = 17; + public static final int HIDEUSERS = 18; + public static final int RULEVIOLATION = 19; + public static final int SESSIONSTATE = 20; + public static final int REQUESTPVT = 21; + public static final int ACCEPTPVT = 22; + public static final int REJECTPVT = 23; + public static final int ENDSESSION = 24; + public static final int TXPROFILE = 25; + public static final int STARTVOYEUR = 26; + public static final int SERVERREFRESH = 27; + public static final int SETTING = 28; + public static final int BWSTATS = 29; + public static final int SETGUESTNAME = 30; + public static final int TKX = 30; + public static final int SETTEXTOPT = 31; + public static final int SERVERCONFIG = 32; + public static final int MODELGROUP = 33; + public static final int REQUESTGRP = 34; + public static final int STATUSGRP = 35; + public static final int GROUPCHAT = 36; + public static final int CLOSEGRP = 37; + public static final int UCR = 38; + public static final int MYUCR = 39; + public static final int SLAVECON = 40; + public static final int SLAVECMD = 41; + public static final int SLAVEFRIEND = 42; + public static final int SLAVEVSHARE = 43; + public static final int ROOMDATA = 44; + public static final int NEWSITEM = 45; + public static final int GUESTCOUNT = 46; + public static final int PRELOGINQ = 47; + public static final int MODELGROUPSZ = 48; + public static final int ROOMHELPER = 49; + public static final int CMESG = 50; + public static final int JOINCHAN = 51; + public static final int CREATECHAN = 52; + public static final int INVITECHAN = 53; + public static final int KICKCHAN = 54; + public static final int QUIETCHAN = 55; + public static final int BANCHAN = 56; + public static final int PREVIEWCHAN = 57; + public static final int SHUTDOWN = 58; + public static final int LISTBANS = 59; + public static final int UNBAN = 60; + public static final int SETWELCOME = 61; + public static final int CHANOP = 62; + public static final int LISTCHAN = 63; + public static final int TAGS = 64; + public static final int SETPCODE = 65; + public static final int SETMINTIP = 66; + public static final int UEOPT = 67; + public static final int HDVIDEO = 68; + public static final int METRICS = 69; + public static final int OFFERCAM = 70; + public static final int REQUESTCAM = 71; + public static final int MYWEBCAM = 72; + public static final int MYCAMSTATE = 73; + public static final int PMHISTORY = 74; + public static final int CHATFLASH = 75; + public static final int TRUEPVT = 76; + public static final int BOOKMARKS = 77; + public static final int EVENT = 78; + public static final int STATEDUMP = 79; + public static final int RECOMMEND = 80; + public static final int EXTDATA = 81; + public static final int NOTIFY = 84; + public static final int PUBLISH = 85; + public static final int ZGWINVALID = 95; + public static final int CONNECTING = 96; + public static final int CONNECTED = 97; + public static final int DISCONNECTED = 98; + public static final int LOGOUT = 99; +} diff --git a/src/main/java/ctbrec/sites/mfc/Model.java b/src/main/java/ctbrec/sites/mfc/Model.java new file mode 100644 index 00000000..7a46df38 --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/Model.java @@ -0,0 +1,168 @@ +package ctbrec.sites.mfc; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class Model { + + private Double camscore; + private String continent; + private Integer flags; + private Boolean hidecs; + private Integer kbit; + private Integer lastnews; + private Integer mg; + private Integer missmfc; + private Integer newModel; + private Integer rank; + private Integer rc; + private Integer sfw; + private String topic; + private Map additionalProperties = new HashMap(); + private Set tags = new HashSet<>(); + + public Double getCamscore() { + return camscore; + } + + public void setCamscore(Double camscore) { + this.camscore = camscore; + } + + public String getContinent() { + return continent; + } + + public void setContinent(String continent) { + this.continent = continent; + } + + public Integer getFlags() { + return flags; + } + + public void setFlags(Integer flags) { + this.flags = flags; + } + + public Boolean getHidecs() { + return hidecs; + } + + public void setHidecs(Boolean hidecs) { + this.hidecs = hidecs; + } + + public Integer getKbit() { + return kbit; + } + + public void setKbit(Integer kbit) { + this.kbit = kbit; + } + + public Integer getLastnews() { + return lastnews; + } + + public void setLastnews(Integer lastnews) { + this.lastnews = lastnews; + } + + public Integer getMg() { + return mg; + } + + public void setMg(Integer mg) { + this.mg = mg; + } + + public Integer getMissmfc() { + return missmfc; + } + + public void setMissmfc(Integer missmfc) { + this.missmfc = missmfc; + } + + public Integer getNewModel() { + return newModel; + } + + public void setNewModel(Integer newModel) { + this.newModel = newModel; + } + + public Integer getRank() { + return rank; + } + + public void setRank(Integer rank) { + this.rank = rank; + } + + public Integer getRc() { + return rc; + } + + public void setRc(Integer rc) { + this.rc = rc; + } + + public Integer getSfw() { + return sfw; + } + + public void setSfw(Integer sfw) { + this.sfw = sfw; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + + public Set getTags() { + return tags; + } + + public void setTags(Set tags) { + this.tags = tags; + } + + public void merge(Model m) { + if(m == null) { + return; + } + camscore = m.camscore != null ? m.camscore : camscore; + continent = m.continent != null ? m.continent : continent; + flags = m.flags != null ? m.flags : flags; + hidecs = m.hidecs != null ? m.hidecs : hidecs; + kbit = m.kbit != null ? m.kbit : kbit; + lastnews = m.lastnews != null ? m.lastnews : lastnews; + mg = m.mg != null ? m.mg : mg; + missmfc = m.missmfc != null ? m.missmfc : missmfc; + newModel = m.newModel != null ? m.newModel : newModel; + rank = m.rank != null ? m.rank : rank; + rc = m.rc != null ? m.rc : rc; + sfw = m.sfw != null ? m.sfw : sfw; + topic = m.topic != null ? m.topic : topic; + additionalProperties.putAll(m.additionalProperties); + tags.addAll(m.tags); + } + + +} diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCams.java b/src/main/java/ctbrec/sites/mfc/MyFreeCams.java new file mode 100644 index 00000000..b37aa473 --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCams.java @@ -0,0 +1,95 @@ +package ctbrec.sites.mfc; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Site; +import ctbrec.recorder.Recorder; +import ctbrec.ui.CookieJarImpl; +import ctbrec.ui.TabProvider; +import okhttp3.ConnectionPool; +import okhttp3.FormBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class MyFreeCams implements Site { + + private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCams.class); + + private Recorder recorder; + private MyFreeCamsClient client; + public static OkHttpClient httpClient = new OkHttpClient.Builder() + .connectTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS) + .readTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS) + .connectionPool(new ConnectionPool(50, 10, TimeUnit.MINUTES)) + .cookieJar(new CookieJarImpl()) + .build(); + + public MyFreeCams() throws IOException { + client = MyFreeCamsClient.getInstance(); + client.setSite(this); + client.start(); + + login(); + } + + public void login() throws IOException { + RequestBody body = new FormBody.Builder() + .add("username", "affenhubert") + .add("password", "hampel81") + .add("tz", "2") + .add("ss", "1920x1080") + .add("submit_login", "97") + .build(); + Request req = new Request.Builder() + .url(getBaseUrl() + "/php/login.php") + .header("Referer", getBaseUrl()) + .header("Content-Type", "application/x-www-form-urlencoded") + .post(body) + .build(); + Response resp = httpClient.newCall(req).execute(); + if(!resp.isSuccessful()) { + LOG.error("Login failed {} {}", resp.code(), resp.message()); + } + resp.close(); + } + + @Override + public String getName() { + return "MyFreeCams"; + } + + @Override + public String getBaseUrl() { + return "https://www.myfreecams.com"; + } + + @Override + public String getAffiliateLink() { + return ""; + } + + @Override + public void setRecorder(Recorder recorder) { + this.recorder = recorder; + } + + @Override + public TabProvider getTabProvider() { + return new MyFreeCamsTabProvider(client, recorder, this); + } + + @Override + public MyFreeCamsModel createModel(String name) { + MyFreeCamsModel model = new MyFreeCamsModel(); + model.setName(name); + model.setUrl("https://profiles.myfreecams.com/" + name); + return model; + } +} diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java new file mode 100644 index 00000000..18d8296f --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -0,0 +1,357 @@ +package ctbrec.sites.mfc; + +import static ctbrec.sites.mfc.MessageTypes.*; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; + +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +public class MyFreeCamsClient { + + private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsClient.class); + + private static MyFreeCamsClient instance; + private MyFreeCams mfc; + private WebSocket ws; + private Moshi moshi; + private volatile boolean running = false; + + private Map sessionStates = new HashMap<>(); + private Map models = new HashMap<>(); + private Lock lock = new ReentrantLock(); + private ExecutorService executor = Executors.newSingleThreadExecutor(); + + private MyFreeCamsClient() { + moshi = new Moshi.Builder().build(); + } + + public static synchronized MyFreeCamsClient getInstance() { + if (instance == null) { + instance = new MyFreeCamsClient(); + } + return instance; + } + + public void setSite(MyFreeCams mfc) { + this.mfc = mfc; + } + + public void start() throws IOException { + running = true; + ServerConfig serverConfig = new ServerConfig(MyFreeCams.httpClient); + List websocketServers = new ArrayList(serverConfig.wsServers.keySet()); + String server = websocketServers.get((int) (Math.random()*websocketServers.size())); + String wsUrl = "ws://" + server + ".myfreecams.com:8080/fcsl"; + Request req = new Request.Builder() + .url(wsUrl) + .addHeader("Origin", "http://m.myfreecams.com") + .build(); + ws = createWebSocket(req); + } + + public void stop() { + ws.close(1000, "Good Bye"); // terminate normally (1000) + running = false; + } + + public List getModels() { + lock.lock(); + try { + LOG.trace("Models: {}", models.size()); + return new ArrayList<>(this.models.values()); + } finally { + lock.unlock(); + } + } + + private WebSocket createWebSocket(Request req) { + WebSocket ws = MyFreeCams.httpClient.newWebSocket(req, new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, Response response) { + super.onOpen(webSocket, response); + try { + LOG.trace("open: [{}]", response.body().string()); + webSocket.send("hello fcserver\n"); + // TxCmd Sending - nType: 1, nTo: 0, nArg1: 20080909, nArg2: 0, sMsg:guest:guest + webSocket.send("1 0 0 20080909 0 guest:guest\n"); + startKeepAlive(webSocket); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + // TODO decide what todo: is this the end of the session + // or do we have to reconnect to keep things running? + super.onClosed(webSocket, code, reason); + LOG.trace("close: {} {}", code, reason); + running = false; + MyFreeCams.httpClient.dispatcher().executorService().shutdownNow(); + } + + private StringBuilder msgBuffer = new StringBuilder(); + + @Override + public void onMessage(WebSocket webSocket, String text) { + super.onMessage(webSocket, text); + msgBuffer.append(text); + Message message; + try { + message = parseMessage(msgBuffer); + if (message != null) { + msgBuffer.setLength(0); + } + + switch (message.getType()) { + case LOGIN: + LOG.trace("login"); + break; + case DETAILS: + case ROOMHELPER: + case ADDFRIEND: + case ADDIGNORE: + case CMESG: + case PMESG: + case TXPROFILE: + case USERNAMELOOKUP: + case MYCAMSTATE: + case MYWEBCAM: + case JOINCHAN: + case SESSIONSTATE: + if(!message.getMessage().isEmpty()) { + JsonAdapter adapter = moshi.adapter(SessionState.class); + try { + SessionState sessionState = adapter.fromJson(message.getMessage()); + updateSessionState(sessionState); + } catch (IOException e) { + LOG.error("Couldn't parse session state message", e); + } + } + break; + case TAGS: + JSONObject json = new JSONObject(message.getMessage()); + String[] names = JSONObject.getNames(json); + Integer uid = Integer.parseInt(names[0]); + SessionState sessionState = sessionStates.get(uid); + if (sessionState != null) { + JSONArray tags = json.getJSONArray(names[0]); + for (Object obj : tags) { + sessionState.getM().getTags().add((String) obj); + } + } + break; + case EXTDATA: + requestExtData(message.getMessage()); + break; + default: + LOG.trace("Unknown message {}", message); + break; + } + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + private void requestExtData(String message) { + try { + JSONObject json = new JSONObject(message); + long respkey = json.getInt("respkey"); + long opts = json.getInt("opts"); + long serv = json.getInt("serv"); + long type = json.getInt("type"); + String base = "http://www.myfreecams.com/php/FcwExtResp.php"; + String url = base + "?respkey="+respkey+"&opts="+opts+"&serv="+serv+"&type="+type; + Request req = new Request.Builder().url(url).build(); + LOG.debug("Requesting EXTDATA {}", url); + Response resp = MyFreeCams.httpClient.newCall(req).execute(); + + if(resp.isSuccessful()) { + parseExtDataSessionStates(resp.body().string()); + } + } catch(Exception e) { + LOG.warn("Couldn't request EXTDATA", e); + } + } + + private void parseExtDataSessionStates(String json) { + JSONObject object = new JSONObject(json); + if(object.has("type") && object.getInt("type") == 21) { + JSONArray outer = object.getJSONArray("rdata"); + for (int i = 1; i < outer.length(); i++) { + JSONArray inner = outer.getJSONArray(i); + try { + SessionState state = new SessionState(); + int idx = 0; + state.setNm(inner.getString(idx++)); + state.setSid(inner.getInt(idx++)); + state.setUid(inner.getInt(idx++)); + state.setVs(inner.getInt(idx++)); + state.setPid(inner.getInt(idx++)); + state.setLv(inner.getInt(idx++)); + state.setU(new User()); + state.getU().setCamserv(inner.getInt(idx++)); + idx++; + state.getU().setChatColor(inner.getString(idx++)); + state.getU().setChatFont(inner.getInt(idx++)); + state.getU().setChatOpt(inner.getInt(idx++)); + state.getU().setCreation(inner.getInt(idx++)); + state.getU().setAvatar(inner.getInt(idx++)); + state.getU().setProfile(inner.getInt(idx++)); + state.getU().setPhotos(inner.getInt(idx++)); + state.getU().setBlurb(inner.getString(idx++)); + state.setM(new Model()); + state.getM().setNewModel(inner.getInt(idx++)); + state.getM().setMissmfc(inner.getInt(idx++)); + state.getM().setCamscore(inner.getDouble(idx++)); + state.getM().setContinent(inner.getString(idx++)); + state.getM().setFlags(inner.getInt(idx++)); + state.getM().setRank(inner.getInt(idx++)); + state.getM().setRc(inner.getInt(idx++)); + state.getM().setTopic(inner.getString(idx++)); + state.getM().setHidecs(inner.getInt(idx++) == 1); + updateSessionState(state); + } catch(Exception e) { + LOG.warn("Couldn't parse session state {}", inner.toString()); + } + } + } else if(object.has("type") && object.getInt("type") == 20) { + // TODO parseTags(); + } + } + + private void updateSessionState(SessionState newState) { + if (newState.getUid() <= 0) { + return; + } + SessionState storedState = sessionStates.get(newState.getUid()); + if (storedState != null) { + storedState.merge(newState); + updateModel(storedState); + } else { + lock.lock(); + try { + sessionStates.put(newState.getUid(), newState); + updateModel(newState); + } finally { + lock.unlock(); + } + } + } + + private void updateModel(SessionState state) { + // essential data not yet available + if(state.getNm() == null || state.getM() == null || state.getU() == null || state.getU().getCamserv() == null) { + return; + } + + MyFreeCamsModel model = models.get(state.getUid()); + if(model == null) { + model = mfc.createModel(state.getNm()); + models.put(state.getUid(), model); + } + model.update(state); + } + + private Message parseMessage(StringBuilder msg) throws UnsupportedEncodingException { + if (msg.length() < 4) { + // packet size not transmitted completely + return null; + } else { + int packetLength = Integer.parseInt(msg.substring(0, 4)); + if (packetLength > msg.length() - 4) { + // packet not complete + return null; + } else { + msg.delete(0, 4); + int type = parseNextInt(msg); + int sender = parseNextInt(msg); + int receiver = parseNextInt(msg); + int arg1 = parseNextInt(msg); + int arg2 = parseNextInt(msg); + return new Message(type, sender, receiver, arg1, arg2, URLDecoder.decode(msg.toString(), "utf-8")); + } + } + } + + private int parseNextInt(StringBuilder s) { + int nextSpace = s.indexOf(" "); + int i = Integer.parseInt(s.substring(0, nextSpace)); + s.delete(0, nextSpace + 1); + return i; + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + super.onMessage(webSocket, bytes); + LOG.debug("msgb: {}", bytes.hex()); + } + }); + return ws; + } + + private void startKeepAlive(WebSocket ws) { + Thread keepAlive = new Thread(() -> { + while(running) { + LOG.trace("--> NULL to keep the connection alive"); + try { + ws.send("0 0 0 0 0 -\n"); + Thread.sleep(TimeUnit.SECONDS.toMillis(15)); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + keepAlive.setName("KeepAlive"); + keepAlive.setDaemon(true); + keepAlive.start(); + } + + public void update(MyFreeCamsModel model) { + lock.lock(); + try { + for (SessionState state : sessionStates.values()) { + if(Objects.equals(state.getNm(), model.getName())) { + model.update(state); + return; + } + } + } finally { + lock.unlock(); + } + } + + public MyFreeCamsModel getModel(int uid) { + return models.get(uid); + } + + public void execute(Runnable r) { + executor.execute(r); + } +} diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java new file mode 100644 index 00000000..63fa48f8 --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -0,0 +1,162 @@ +package ctbrec.sites.mfc; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; + +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.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 ctbrec.AbstractModel; +import ctbrec.recorder.download.StreamSource; +import okhttp3.Request; +import okhttp3.Response; + +public class MyFreeCamsModel extends AbstractModel { + + private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsModel.class); + + private String hlsUrl; + private double camScore; + private State state; + private int resolution[]; + + @Override + public boolean isOnline() throws IOException, ExecutionException, InterruptedException { + MyFreeCamsClient.getInstance().update(this); + return state == State.ONLINE; + } + + @Override + public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { + return isOnline(); + } + + @Override + public String getOnlineState(boolean failFast) throws IOException, ExecutionException { + return state != null ? state.toString() : "offline"; + } + + @Override + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + MasterPlaylist masterPlaylist = getMasterPlaylist(); + List sources = new ArrayList<>(); + for (PlaylistData playlist : masterPlaylist.getPlaylists()) { + if (playlist.hasStreamInfo()) { + StreamSource src = new StreamSource(); + src.bandwidth = playlist.getStreamInfo().getBandwidth(); + if(playlist.getStreamInfo().getResolution() != null) { + src.width = playlist.getStreamInfo().getResolution().width; + src.height = playlist.getStreamInfo().getResolution().height; + String masterUrl = hlsUrl; + String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); + String segmentUri = baseUrl + playlist.getUri(); + src.mediaPlaylistUrl = segmentUri; + LOG.trace("Media playlist {}", src.mediaPlaylistUrl); + sources.add(src); + } + } + } + return sources; + } + + private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException { + if(hlsUrl == null) { + throw new IllegalStateException("Stream url unknown"); + } + LOG.debug("Loading master playlist {}", hlsUrl); + Request req = new Request.Builder().url(hlsUrl).build(); + Response response = MyFreeCams.httpClient.newCall(req).execute(); + try { + InputStream inputStream = response.body().byteStream(); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); + Playlist playlist = parser.parse(); + MasterPlaylist master = playlist.getMasterPlaylist(); + return master; + } finally { + response.close(); + } + } + + @Override + public void invalidateCacheEntries() { + resolution = null; + } + + @Override + public void receiveTip(int tokens) throws IOException { + throw new RuntimeException("Not implemented"); + } + + @Override + public int[] getStreamResolution(boolean failFast) throws ExecutionException { + if(resolution == null) { + if(failFast || hlsUrl == null) { + return new int[2]; + } + MyFreeCamsClient.getInstance().execute(()->{ + try { + List streamSources = getStreamSources(); + Collections.sort(streamSources); + StreamSource best = streamSources.get(streamSources.size()-1); + resolution = new int[] {best.width, best.height}; + } catch (ExecutionException | IOException | ParseException | PlaylistException e) { + LOG.error("Couldn't determine stream resolution", e); + } + }); + return new int[2]; + } else { + return resolution; + } + } + + public void setStreamUrl(String hlsUrl) { + this.hlsUrl = hlsUrl; + } + + public String getStreamUrl() { + return hlsUrl; + } + + public double getCamScore() { + return camScore; + } + + public void setCamScore(double camScore) { + this.camScore = camScore; + } + + public void setState(State state) { + this.state = state; + } + + public void update(SessionState state) { + setCamScore(state.getM().getCamscore()); + setState(State.of(state.getVs())); + + // preview + String uid = state.getUid().toString(); + String uidStart = uid.substring(0, 3); + String previewUrl = "https://img.mfcimg.com/photos2/"+uidStart+'/'+uid+"/avatar.300x300.jpg"; + setPreview(previewUrl); + + // stream url + Integer camserv = state.getU().getCamserv(); + if(camserv != null) { + String hlsUrl = "http://video" + (camserv - 500) + ".myfreecams.com:1935/NxServer/ngrp:mfc_" + (100000000 + state.getUid()) + ".f4v_mobile/playlist.m3u8"; + setStreamUrl(hlsUrl); + } + } +} diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCamsTabProvider.java b/src/main/java/ctbrec/sites/mfc/MyFreeCamsTabProvider.java new file mode 100644 index 00000000..97d719db --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCamsTabProvider.java @@ -0,0 +1,43 @@ +package ctbrec.sites.mfc; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import ctbrec.recorder.Recorder; +import ctbrec.ui.PaginatedScheduledService; +import ctbrec.ui.TabProvider; +import ctbrec.ui.ThumbOverviewTab; +import javafx.scene.Scene; +import javafx.scene.control.Tab; +import javafx.util.Duration; + +public class MyFreeCamsTabProvider extends TabProvider { + private Recorder recorder; + private MyFreeCams myFreeCams; + + public MyFreeCamsTabProvider(MyFreeCamsClient client, Recorder recorder, MyFreeCams myFreeCams) { + this.recorder = recorder; + this.myFreeCams = myFreeCams; + } + + @Override + public List getTabs(Scene scene) { + List tabs = new ArrayList<>(); + + PaginatedScheduledService updateService = new OnlineCamsUpdateService(); + ThumbOverviewTab online = new ThumbOverviewTab("Online", updateService); + online.setRecorder(recorder); + updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); + tabs.add(online); + + updateService = new FriendsUpdateService(myFreeCams); + ThumbOverviewTab friends = new ThumbOverviewTab("Friends", updateService); + friends.setRecorder(recorder); + updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); + tabs.add(friends); + + + return tabs; + } +} diff --git a/src/main/java/ctbrec/sites/mfc/OnlineCamsUpdateService.java b/src/main/java/ctbrec/sites/mfc/OnlineCamsUpdateService.java new file mode 100644 index 00000000..6ecaf5ba --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/OnlineCamsUpdateService.java @@ -0,0 +1,39 @@ +package ctbrec.sites.mfc; + + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +import ctbrec.Model; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; + +public class OnlineCamsUpdateService extends PaginatedScheduledService { + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + MyFreeCamsClient client = MyFreeCamsClient.getInstance(); + int modelsPerPage = 50; + return client.getModels().stream() + .filter((m) -> m.getPreview() != null) + .filter((m) -> m.getStreamUrl() != null) + .filter((m) -> { + try { + return m.isOnline(); + } catch(Exception e) { + return false; + } + }) + .sorted((m1,m2) -> (int)(m2.getCamScore() - m1.getCamScore())) + .skip( (page-1) * modelsPerPage) + .limit(modelsPerPage) + .collect(Collectors.toList()); + } + }; + } + +} diff --git a/src/main/java/ctbrec/sites/mfc/OnlineModelsTab.java b/src/main/java/ctbrec/sites/mfc/OnlineModelsTab.java new file mode 100644 index 00000000..49d47fe8 --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/OnlineModelsTab.java @@ -0,0 +1,12 @@ +package ctbrec.sites.mfc; + +import ctbrec.ui.PaginatedScheduledService; +import ctbrec.ui.ThumbOverviewTab; + +public class OnlineModelsTab extends ThumbOverviewTab { + + public OnlineModelsTab(String title, PaginatedScheduledService updateService) { + super(title, updateService); + + } +} diff --git a/src/main/java/ctbrec/sites/mfc/ServerConfig.java b/src/main/java/ctbrec/sites/mfc/ServerConfig.java new file mode 100644 index 00000000..d6095fda --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/ServerConfig.java @@ -0,0 +1,59 @@ +package ctbrec.sites.mfc; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.json.JSONArray; +import org.json.JSONObject; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class ServerConfig { + + List ajaxServers; + List videoServers; + List chatServers; + Map h5Servers; + Map wsServers; + Map wzobsServers; + Map ngVideo; + + public ServerConfig(OkHttpClient client) throws IOException { + Request req = new Request.Builder().url("http://www.myfreecams.com/_js/serverconfig.js").build(); + Response resp = client.newCall(req).execute(); + String json = resp.body().string(); + + JSONObject serverConfig = new JSONObject(json); + ajaxServers = parseList(serverConfig, "ajax_servers"); + videoServers = parseList(serverConfig, "video_servers"); + chatServers = parseList(serverConfig, "chat_servers"); + h5Servers = parseMap(serverConfig, "h5video_servers"); + wsServers = parseMap(serverConfig, "websocket_servers"); + wzobsServers = parseMap(serverConfig, "wzobs_servers"); + ngVideo = parseMap(serverConfig, "ngvideo_servers"); + } + + private static Map parseMap(JSONObject serverConfig, String name) { + JSONObject servers = serverConfig.getJSONObject(name); + Map result = new HashMap<>(); + for (String key : servers.keySet()) { + result.put(key, servers.getString(key)); + } + return result; + } + + private static List parseList(JSONObject serverConfig, String name) { + JSONArray servers = serverConfig.getJSONArray(name); + List result = new ArrayList<>(servers.length()); + for (Object server : servers) { + result.add((String) server); + } + return result; + } + +} diff --git a/src/main/java/ctbrec/sites/mfc/SessionState.java b/src/main/java/ctbrec/sites/mfc/SessionState.java new file mode 100644 index 00000000..e87c882e --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/SessionState.java @@ -0,0 +1,128 @@ +package ctbrec.sites.mfc; + +import java.util.HashMap; +import java.util.Map; + +public class SessionState { + + private Integer lv; + private String nm; + private Integer pid; + private Integer sid; + private Integer uid; + private Integer vs; + private User u; + private Model m; + private X x; + private Map additionalProperties = new HashMap(); + + public Integer getLv() { + return lv; + } + + public void setLv(Integer lv) { + this.lv = lv; + } + + public String getNm() { + return nm; + } + + public void setNm(String nm) { + this.nm = nm; + } + + public Integer getPid() { + return pid; + } + + public void setPid(Integer pid) { + this.pid = pid; + } + + public Integer getSid() { + return sid; + } + + public void setSid(Integer sid) { + this.sid = sid; + } + + public Integer getUid() { + return uid; + } + + public void setUid(Integer uid) { + this.uid = uid; + } + + public Integer getVs() { + return vs; + } + + public void setVs(Integer vs) { + this.vs = vs; + } + + public User getU() { + return u; + } + + public void setU(User u) { + this.u = u; + } + + public Model getM() { + return m; + } + + public void setM(Model m) { + this.m = m; + } + + public X getX() { + return x; + } + + public void setX(X x) { + this.x = x; + } + + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + + @Override + public String toString() { + return Integer.toString(uid) + " u:" + u + " m:" + m + " x:" + x + " " + nm; + } + + public void merge(SessionState newState) { + lv = newState.lv != null ? newState.lv : lv; + nm = newState.nm != null ? newState.nm : nm; + pid = newState.pid != null ? newState.pid : pid; + sid = newState.sid != null ? newState.sid : sid; + vs = newState.vs != null ? newState.vs : vs; + additionalProperties.putAll(newState.additionalProperties); + + if (u != null) { + u.merge(newState.u); + } else { + u = newState.u; + } + if (m != null) { + m.merge(newState.m); + } else { + m = newState.m; + } + if (x != null) { + x.merge(newState.x); + } else { + x = newState.x; + } + } +} diff --git a/src/main/java/ctbrec/sites/mfc/Share.java b/src/main/java/ctbrec/sites/mfc/Share.java new file mode 100644 index 00000000..ab6d5808 --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/Share.java @@ -0,0 +1,116 @@ +package ctbrec.sites.mfc; + +import java.util.HashMap; +import java.util.Map; + +public class Share { + + private Integer albums; + private Integer follows; + private Integer tmAlbum; + private Integer things; + private Integer clubs; + private Integer collections; + private Integer stores; + private Integer goals; + private Integer polls; + private Map additionalProperties = new HashMap(); + + public Integer getAlbums() { + return albums; + } + + public void setAlbums(Integer albums) { + this.albums = albums; + } + + public Integer getFollows() { + return follows; + } + + public void setFollows(Integer follows) { + this.follows = follows; + } + + public Integer getTmAlbum() { + return tmAlbum; + } + + public void setTmAlbum(Integer tmAlbum) { + this.tmAlbum = tmAlbum; + } + + public Integer getThings() { + return things; + } + + public void setThings(Integer things) { + this.things = things; + } + + public Integer getClubs() { + return clubs; + } + + public void setClubs(Integer clubs) { + this.clubs = clubs; + } + + public Integer getCollections() { + return collections; + } + + public void setCollections(Integer collections) { + this.collections = collections; + } + + public Integer getStores() { + return stores; + } + + public void setStores(Integer stores) { + this.stores = stores; + } + + public Integer getGoals() { + return goals; + } + + public void setGoals(Integer goals) { + this.goals = goals; + } + + public Integer getPolls() { + return polls; + } + + public void setPolls(Integer polls) { + this.polls = polls; + } + + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + + public void merge(Share share) { + if (share == null) { + return; + } + + albums = share.albums != null ? share.albums : albums; + follows = share.follows != null ? share.follows : follows; + tmAlbum = share.tmAlbum != null ? share.tmAlbum : tmAlbum; + things = share.things != null ? share.things : things; + clubs = share.clubs != null ? share.clubs : clubs; + collections = share.collections != null ? share.collections : collections; + stores = share.stores != null ? share.stores : stores; + goals = share.goals != null ? share.goals : goals; + polls = share.polls != null ? share.polls : polls; + additionalProperties.putAll(share.additionalProperties); + } + +} diff --git a/src/main/java/ctbrec/sites/mfc/State.java b/src/main/java/ctbrec/sites/mfc/State.java new file mode 100644 index 00000000..11c3dea7 --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/State.java @@ -0,0 +1,56 @@ +package ctbrec.sites.mfc; + +import java.util.Optional; + +public enum State { + ONLINE("online"), + CAMOFF("online - cam off"), + RECORDING("recording"), + INCLUDE("include"), + EXCLUDE("exclude"), + DELETE("delete"), + AWAY("away"), + PRIVATE("private"), + GROUP_SHOW("group_show"), + OFFLINE("offline"), + UNKNOWN("unknown"); + + String literal; + State(String literal) { + this.literal = literal; + } + + public static State of(Integer vs) { + Integer s = Optional.ofNullable(vs).orElse(Integer.MAX_VALUE); + switch (s) { + case 0: + return ONLINE; + case 90: + return CAMOFF; + case -4: + return RECORDING; + case -3: + return INCLUDE; + case -2: + return EXCLUDE; + case -1: + return DELETE; + case 2: + return AWAY; + case 12: + case 91: + return PRIVATE; + case 13: + return GROUP_SHOW; + case 127: + return OFFLINE; + default: + return UNKNOWN; + } + } + + @Override + public String toString() { + return literal; + } +} diff --git a/src/main/java/ctbrec/sites/mfc/User.java b/src/main/java/ctbrec/sites/mfc/User.java new file mode 100644 index 00000000..d1f78dc2 --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/User.java @@ -0,0 +1,154 @@ +package ctbrec.sites.mfc; + +import java.util.HashMap; +import java.util.Map; + +public class User { + + private Integer avatar; + private String blurb; + private Integer camserv; + private String chatColor; + private Integer chatFont; + private Integer chatOpt; + private String country; + private Integer creation; + private String ethnic; + private String occupation; + private Integer photos; + private Integer profile; + private String status; + private Map additionalProperties = new HashMap(); + + public Integer getAvatar() { + return avatar; + } + + public void setAvatar(Integer avatar) { + this.avatar = avatar; + } + + public String getBlurb() { + return blurb; + } + + public void setBlurb(String blurb) { + this.blurb = blurb; + } + + public Integer getCamserv() { + return camserv; + } + + public void setCamserv(Integer camserv) { + this.camserv = camserv; + } + + public String getChatColor() { + return chatColor; + } + + public void setChatColor(String chatColor) { + this.chatColor = chatColor; + } + + public Integer getChatFont() { + return chatFont; + } + + public void setChatFont(Integer chatFont) { + this.chatFont = chatFont; + } + + public Integer getChatOpt() { + return chatOpt; + } + + public void setChatOpt(Integer chatOpt) { + this.chatOpt = chatOpt; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public Integer getCreation() { + return creation; + } + + public void setCreation(Integer creation) { + this.creation = creation; + } + + public String getEthnic() { + return ethnic; + } + + public void setEthnic(String ethnic) { + this.ethnic = ethnic; + } + + public String getOccupation() { + return occupation; + } + + public void setOccupation(String occupation) { + this.occupation = occupation; + } + + public Integer getPhotos() { + return photos; + } + + public void setPhotos(Integer photos) { + this.photos = photos; + } + + public Integer getProfile() { + return profile; + } + + public void setProfile(Integer profile) { + this.profile = profile; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + + public void merge(User u) { + if (u == null) { + return; + } + avatar = u.avatar != null ? u.avatar : avatar; + blurb = u.blurb != null ? u.blurb : blurb; + camserv = u.camserv != null ? u.camserv : camserv; + chatColor = u.chatColor != null ? u.chatColor : chatColor; + chatFont = u.chatFont != null ? u.chatFont : chatFont; + chatOpt = u.chatOpt != null ? u.chatOpt : chatOpt; + country = u.country != null ? u.country : country; + creation = u.creation != null ? u.creation : creation; + ethnic = u.ethnic != null ? u.ethnic : ethnic; + occupation = u.occupation != null ? u.occupation : occupation; + photos = u.photos != null ? u.photos : photos; + profile = u.profile != null ? u.profile : profile; + status = u.status != null ? u.status : status; + additionalProperties.putAll(u.additionalProperties); + } +} diff --git a/src/main/java/ctbrec/sites/mfc/X.java b/src/main/java/ctbrec/sites/mfc/X.java new file mode 100644 index 00000000..db5175db --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/X.java @@ -0,0 +1,46 @@ +package ctbrec.sites.mfc; + +import java.util.HashMap; +import java.util.Map; + +public class X { + + private Fcext fcext; + private Share share; + private Map additionalProperties = new HashMap(); + + public Fcext getFcext() { + return fcext; + } + + public void setFcext(Fcext fcext) { + this.fcext = fcext; + } + + public Share getShare() { + return share; + } + + public void setShare(Share share) { + this.share = share; + } + + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + + public void merge(X x) { + if(x == null) { + return; + } + fcext.merge(x.fcext); + share.merge(x.share); + additionalProperties.putAll(x.additionalProperties); + + } + +} diff --git a/src/main/java/ctbrec/ui/CamrecApplication.java b/src/main/java/ctbrec/ui/CamrecApplication.java index 3ffbe289..02e42c48 100644 --- a/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/src/main/java/ctbrec/ui/CamrecApplication.java @@ -26,27 +26,20 @@ import ctbrec.io.HttpClient; import ctbrec.recorder.LocalRecorder; import ctbrec.recorder.Recorder; import ctbrec.recorder.RemoteRecorder; -import ctbrec.sites.chaturbate.Chaturbate; +import ctbrec.sites.mfc.MyFreeCams; import javafx.application.Application; import javafx.application.HostServices; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.Task; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Alert; -import javafx.scene.control.Button; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.TabPane.TabClosingPolicy; import javafx.scene.image.Image; import javafx.scene.layout.HBox; -import javafx.scene.layout.StackPane; -import javafx.scene.text.Font; import javafx.stage.Stage; import okhttp3.Request; import okhttp3.Response; @@ -74,9 +67,10 @@ public class CamrecApplication extends Application { hostServices = getHostServices(); client = HttpClient.getInstance(); createRecorder(); - site = new Chaturbate(); + //site = new Chaturbate(); + site = new MyFreeCams(); site.setRecorder(recorder); - doInitialLogin(); + // TODO move this to Chaturbate class doInitialLogin(); createGui(primaryStage); checkForUpdates(); } @@ -158,37 +152,38 @@ public class CamrecApplication extends Application { }.start(); }); - String username = Config.getInstance().getSettings().username; - if(username != null && !username.trim().isEmpty()) { - double fontSize = tabPane.getTabMaxHeight() / 2 - 1; - Button buyTokens = new Button("Buy Tokens"); - buyTokens.setFont(Font.font(fontSize)); - buyTokens.setOnAction((e) -> DesktopIntergation.open(AFFILIATE_LINK)); - buyTokens.setMaxHeight(tabPane.getTabMaxHeight()); - TokenLabel tokenBalance = new TokenLabel(); - tokenPanel = new HBox(5, tokenBalance, buyTokens); - //tokenPanel.setBackground(new Background(new BackgroundFill(Color.GREEN, CornerRadii.EMPTY, new Insets(0)))); - tokenPanel.setAlignment(Pos.BASELINE_RIGHT); - tokenPanel.setMaxHeight(tabPane.getTabMaxHeight()); - tokenPanel.setMaxWidth(200); - tokenBalance.setFont(Font.font(fontSize)); - HBox.setMargin(tokenBalance, new Insets(0, 5, 0, 0)); - HBox.setMargin(buyTokens, new Insets(0, 5, 0, 0)); - for (Node node : tabPane.getChildrenUnmodifiable()) { - if(node.getStyleClass().contains("tab-header-area")) { - Parent header = (Parent) node; - for (Node nd : header.getChildrenUnmodifiable()) { - if(nd.getStyleClass().contains("tab-header-background")) { - StackPane pane = (StackPane) nd; - StackPane.setAlignment(tokenPanel, Pos.CENTER_RIGHT); - pane.getChildren().add(tokenPanel); - } - } - - } - } - loadTokenBalance(tokenBalance); - } + // TODO think about a solution, which works for all sites + // String username = Config.getInstance().getSettings().username; + // if(username != null && !username.trim().isEmpty()) { + // double fontSize = tabPane.getTabMaxHeight() / 2 - 1; + // Button buyTokens = new Button("Buy Tokens"); + // buyTokens.setFont(Font.font(fontSize)); + // buyTokens.setOnAction((e) -> DesktopIntergation.open(AFFILIATE_LINK)); + // buyTokens.setMaxHeight(tabPane.getTabMaxHeight()); + // TokenLabel tokenBalance = new TokenLabel(); + // tokenPanel = new HBox(5, tokenBalance, buyTokens); + // //tokenPanel.setBackground(new Background(new BackgroundFill(Color.GREEN, CornerRadii.EMPTY, new Insets(0)))); + // tokenPanel.setAlignment(Pos.BASELINE_RIGHT); + // tokenPanel.setMaxHeight(tabPane.getTabMaxHeight()); + // tokenPanel.setMaxWidth(200); + // tokenBalance.setFont(Font.font(fontSize)); + // HBox.setMargin(tokenBalance, new Insets(0, 5, 0, 0)); + // HBox.setMargin(buyTokens, new Insets(0, 5, 0, 0)); + // for (Node node : tabPane.getChildrenUnmodifiable()) { + // if(node.getStyleClass().contains("tab-header-area")) { + // Parent header = (Parent) node; + // for (Node nd : header.getChildrenUnmodifiable()) { + // if(nd.getStyleClass().contains("tab-header-background")) { + // StackPane pane = (StackPane) nd; + // StackPane.setAlignment(tokenPanel, Pos.CENTER_RIGHT); + // pane.getChildren().add(tokenPanel); + // } + // } + // + // } + // } + // loadTokenBalance(tokenBalance); + // } } private void loadTokenBalance(TokenLabel label) { diff --git a/src/main/java/ctbrec/ui/TabProvider.java b/src/main/java/ctbrec/ui/TabProvider.java index 1437a0e2..2f760081 100644 --- a/src/main/java/ctbrec/ui/TabProvider.java +++ b/src/main/java/ctbrec/ui/TabProvider.java @@ -1,14 +1,11 @@ package ctbrec.ui; -import java.util.Collections; import java.util.List; import javafx.scene.Scene; import javafx.scene.control.Tab; -public class TabProvider { +public abstract class TabProvider { - public List getTabs(Scene scene) { - return Collections.emptyList(); - } + public abstract List getTabs(Scene scene); } diff --git a/src/main/java/ctbrec/ui/ThumbCell.java b/src/main/java/ctbrec/ui/ThumbCell.java index c5d00020..625ed520 100644 --- a/src/main/java/ctbrec/ui/ThumbCell.java +++ b/src/main/java/ctbrec/ui/ThumbCell.java @@ -74,6 +74,7 @@ public class ThumbCell extends StackPane { private final Color colorHighlight = Color.WHITE; private final Color colorRecording = new Color(0.8, 0.28, 0.28, 1); private SimpleBooleanProperty selectionProperty = new SimpleBooleanProperty(false); + private double imgAspectRatio = 3.0 / 4.0; private HttpClient client; @@ -91,6 +92,8 @@ public class ThumbCell extends StackPane { iv = new ImageView(); setImage(model.getPreview()); iv.setSmooth(true); + iv.setPreserveRatio(true); + iv.setStyle("-fx-background-color: #000"); getChildren().add(iv); nameBackground = new Rectangle(); @@ -208,17 +211,11 @@ public class ThumbCell extends StackPane { // the model is online, but the resolution is 0. probably something went wrong // when we first requested the stream info, so we remove this invalid value from the "cache" // so that it is requested again - try { - if (model.isOnline() && resolution[1] == 0) { - LOG.debug("Removing invalid resolution value for {}", model.getName()); - model.invalidateCacheEntries(); - } - } catch (IOException | ExecutionException | InterruptedException e) { - LOG.error("Coulnd't get resolution for model {}", model, e); + if (model.isOnline() && resolution[1] == 0) { + LOG.debug("Removing invalid resolution value for {}", model.getName()); + model.invalidateCacheEntries(); } - } catch (ExecutionException e1) { - LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1); - } catch (IOException e1) { + } catch (ExecutionException | IOException | InterruptedException e1) { LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1); } finally { ThumbOverviewTab.resolutionProcessing.remove(model); @@ -226,11 +223,11 @@ public class ThumbCell extends StackPane { }); } - private void updateResolutionTag(int[] resolution) throws IOException, ExecutionException { + private void updateResolutionTag(int[] resolution) throws IOException, ExecutionException, InterruptedException { String _res = "n/a"; Paint resolutionBackgroundColor = resolutionOnlineColor; String state = model.getOnlineState(false); - if ("public".equals(state)) { + if (model.isOnline()) { LOG.trace("Model resolution {} {}x{}", model.getName(), resolution[0], resolution[1]); LOG.trace("Resolution queue size: {}", ThumbOverviewTab.queue.size()); final int w = resolution[1]; @@ -262,6 +259,8 @@ public class ThumbCell extends StackPane { @Override public void changed(ObservableValue observable, Number oldValue, Number newValue) { if(newValue.doubleValue() == 1.0) { + imgAspectRatio = img.getHeight() / img.getWidth(); + setThumbWidth(width); iv.setImage(img); } } @@ -283,26 +282,33 @@ public class ThumbCell extends StackPane { } void startPlayer() { - try { - if(model.isOnline(true)) { - List sources = model.getStreamSources(); - Collections.sort(sources); - StreamSource best = sources.get(sources.size()-1); - LOG.debug("Playing {}", best.getMediaPlaylistUrl()); - Player.play(best.getMediaPlaylistUrl()); - } else { - Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION); - alert.setTitle("Room not public"); - alert.setHeaderText("Room is currently not public"); - alert.showAndWait(); + new Thread(() -> { + try { + if(model.isOnline(true)) { + List sources = model.getStreamSources(); + Collections.sort(sources); + StreamSource best = sources.get(sources.size()-1); + LOG.debug("Playing {}", best.getMediaPlaylistUrl()); + Player.play(best.getMediaPlaylistUrl()); + } else { + Platform.runLater(() -> { + Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION); + alert.setTitle("Room not public"); + alert.setHeaderText("Room is currently not public"); + alert.showAndWait(); + }); + } + } catch (IOException | ExecutionException | ParseException | PlaylistException | InterruptedException e1) { + LOG.error("Couldn't get stream information for model {}", model, e1); + Platform.runLater(() -> { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText("Couldn't determine stream URL"); + alert.setContentText(e1.getLocalizedMessage()); + alert.showAndWait(); + }); } - } catch (IOException | ExecutionException | InterruptedException | ParseException | PlaylistException e1) { - LOG.error("Couldn't get stream information for model {}", model, e1); - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Error"); - alert.setHeaderText("Couldn't determine stream URL"); - alert.showAndWait(); - } + }).start(); } private void setRecording(boolean recording) { @@ -491,7 +497,7 @@ public class ThumbCell extends StackPane { } public void setThumbWidth(int width) { - int height = width * 3 / 4; + int height = (int) (width * imgAspectRatio); setSize(width, height); } diff --git a/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/src/main/java/ctbrec/ui/ThumbOverviewTab.java index d043eb28..0e50e23e 100644 --- a/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -62,7 +62,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { static BlockingQueue queue = new LinkedBlockingQueue<>(); static ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 10, TimeUnit.MINUTES, queue); - PaginatedScheduledService updateService; + protected PaginatedScheduledService updateService; Recorder recorder; List filteredThumbCells = Collections.synchronizedList(new ArrayList<>()); List selectedThumbCells = Collections.synchronizedList(new ArrayList<>()); @@ -215,9 +215,14 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { if(updatesSuspended) { return; } + List models = updateService.getValue(); + updateGrid(models); + + } + + protected void updateGrid(List models) { gridLock.lock(); try { - List models = updateService.getValue(); ObservableList nodes = grid.getChildren(); // first remove models, which are not in the updated list @@ -269,7 +274,6 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } finally { gridLock.unlock(); } - } ThumbCell createThumbCell(ThumbOverviewTab thumbOverviewTab, Model model, Recorder recorder2, HttpClient client2) { @@ -439,6 +443,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } else { alert.setContentText(event.getSource().getException().getLocalizedMessage()); } + LOG.error("Couldn't update model list", event.getSource().getException()); } else { alert.setContentText(event.getEventType().toString()); } diff --git a/src/main/java/org/taktik/mpegts/Streamer.java b/src/main/java/org/taktik/mpegts/Streamer.java index ccc0a515..ee5dbcce 100644 --- a/src/main/java/org/taktik/mpegts/Streamer.java +++ b/src/main/java/org/taktik/mpegts/Streamer.java @@ -190,7 +190,7 @@ public class Streamer { Long sleepNanosPrevious = null; if (lastPcrValue != null && lastPcrTime != null) { if (pcrValue <= lastPcrValue) { - log.error("PCR discontinuity ! " + packet.getPid()); + log.trace("PCR discontinuity ! " + packet.getPid()); resetState = true; } else { sleepNanosPrevious = ((pcrValue - lastPcrValue) / 27 * 1000) - (pcrTime - lastPcrTime);