forked from j62/ctbrec
1
0
Fork 0

Improve MyFreeCamsClient

- Add watch dog to reestablish a broken connection
- Implemented creation of stream urls properly
- Add HD tab to display MFC HD streams
This commit is contained in:
0xboobface 2018-10-23 14:02:20 +02:00
parent 8e3d2fd565
commit f4842fcf51
15 changed files with 313 additions and 119 deletions

View File

@ -56,7 +56,8 @@ public class FriendsUpdateService extends PaginatedScheduledService {
st.setUid(uid); st.setUid(uid);
st.setLv(modelObject.getInt("lv")); st.setLv(modelObject.getInt("lv"));
st.setVs(127); st.setVs(127);
model.update(st);
model.update(st, myFreeCams.getClient().getStreamUrl(st));
} }
models.add(model); models.add(model);
} }

View File

@ -0,0 +1,40 @@
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 HDCamsUpdateService extends PaginatedScheduledService {
@Override
protected Task<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> 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 -> m.getStreamUrl().contains("/x-hls/"))
.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());
}
};
}
}

View File

@ -36,7 +36,6 @@ public class MessageTypes {
public static final int SERVERREFRESH = 27; public static final int SERVERREFRESH = 27;
public static final int SETTING = 28; public static final int SETTING = 28;
public static final int BWSTATS = 29; public static final int BWSTATS = 29;
public static final int SETGUESTNAME = 30;
public static final int TKX = 30; public static final int TKX = 30;
public static final int SETTEXTOPT = 31; public static final int SETTEXTOPT = 31;
public static final int SERVERCONFIG = 32; public static final int SERVERCONFIG = 32;
@ -61,7 +60,6 @@ public class MessageTypes {
public static final int JOINCHAN = 51; public static final int JOINCHAN = 51;
public static final int CREATECHAN = 52; public static final int CREATECHAN = 52;
public static final int INVITECHAN = 53; public static final int INVITECHAN = 53;
public static final int KICKCHAN = 54;
public static final int QUIETCHAN = 55; public static final int QUIETCHAN = 55;
public static final int BANCHAN = 56; public static final int BANCHAN = 56;
public static final int PREVIEWCHAN = 57; public static final int PREVIEWCHAN = 57;
@ -91,6 +89,9 @@ public class MessageTypes {
public static final int EXTDATA = 81; public static final int EXTDATA = 81;
public static final int NOTIFY = 84; public static final int NOTIFY = 84;
public static final int PUBLISH = 85; public static final int PUBLISH = 85;
public static final int XREQUEST = 86;
public static final int XRESPONSE = 87;
public static final int EDGECON = 88;
public static final int ZGWINVALID = 95; public static final int ZGWINVALID = 95;
public static final int CONNECTING = 96; public static final int CONNECTING = 96;
public static final int CONNECTED = 97; public static final int CONNECTED = 97;

View File

@ -112,4 +112,8 @@ public class MyFreeCams implements Site {
public boolean isSiteForModel(Model m) { public boolean isSiteForModel(Model m) {
return m instanceof MyFreeCamsModel; return m instanceof MyFreeCamsModel;
} }
public MyFreeCamsClient getClient() {
return client;
}
} }

View File

@ -44,6 +44,12 @@ public class MyFreeCamsClient {
private Map<Integer, MyFreeCamsModel> models = new HashMap<>(); private Map<Integer, MyFreeCamsModel> models = new HashMap<>();
private Lock lock = new ReentrantLock(); private Lock lock = new ReentrantLock();
private ExecutorService executor = Executors.newSingleThreadExecutor(); private ExecutorService executor = Executors.newSingleThreadExecutor();
private ServerConfig serverConfig;
@SuppressWarnings("unused")
private String tkx;
private Integer cxid;
private int[] ctx;
private String ctxenc;
private MyFreeCamsClient() { private MyFreeCamsClient() {
moshi = new Moshi.Builder().build(); moshi = new Moshi.Builder().build();
@ -62,20 +68,39 @@ public class MyFreeCamsClient {
public void start() throws IOException { public void start() throws IOException {
running = true; running = true;
ServerConfig serverConfig = new ServerConfig(mfc.getHttpClient()); serverConfig = new ServerConfig(mfc.getHttpClient());
List<String> websocketServers = new ArrayList<String>(serverConfig.wsServers.keySet()); List<String> websocketServers = new ArrayList<String>(serverConfig.wsServers.keySet());
String server = websocketServers.get((int) (Math.random()*websocketServers.size())); String server = websocketServers.get((int) (Math.random()*websocketServers.size()));
String wsUrl = "ws://" + server + ".myfreecams.com:8080/fcsl"; String wsUrl = "ws://" + server + ".myfreecams.com:8080/fcsl";
Request req = new Request.Builder()
.url(wsUrl) Thread watchDog = new Thread(() -> {
.addHeader("Origin", "http://m.myfreecams.com") while(running) {
.build(); if (ws == null) {
ws = createWebSocket(req); Request req = new Request.Builder()
.url(wsUrl)
.addHeader("Origin", "http://m.myfreecams.com")
.build();
ws = createWebSocket(req);
}
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
LOG.error("WatchDog couldn't sleep", e);
stop();
running = false;
}
}
});
watchDog.setDaemon(true);
watchDog.setName("MFC WebSocket WatchDog");
watchDog.setPriority(Thread.MIN_PRIORITY);
watchDog.start();
} }
public void stop() { public void stop() {
ws.close(1000, "Good Bye"); // terminate normally (1000)
running = false; running = false;
ws.close(1000, "Good Bye"); // terminate normally (1000)
} }
public List<MyFreeCamsModel> getModels() { public List<MyFreeCamsModel> getModels() {
@ -115,12 +140,21 @@ public class MyFreeCamsClient {
@Override @Override
public void onClosed(WebSocket webSocket, int code, String reason) { 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); super.onClosed(webSocket, code, reason);
LOG.trace("close: {} {}", code, reason); LOG.info("MFC websocket closed: {} {}", code, reason);
running = false; MyFreeCamsClient.this.ws = null;
mfc.getHttpClient().shutdown(); if(!running) {
mfc.getHttpClient().shutdown();
}
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
int code = response.code();
String message = response.message();
MyFreeCamsClient.this.ws = null;
LOG.error("MFC websocket failure: {} {}", code, message, t);
} }
private StringBuilder msgBuffer = new StringBuilder(); private StringBuilder msgBuffer = new StringBuilder();
@ -137,13 +171,15 @@ public class MyFreeCamsClient {
} }
switch (message.getType()) { switch (message.getType()) {
case NULL:
break;
case LOGIN: case LOGIN:
System.out.println("LOGIN"); // System.out.println("LOGIN");
System.out.println("Sender " + message.getSender()); // System.out.println("Sender " + message.getSender());
System.out.println("Receiver " + message.getReceiver()); // System.out.println("Receiver " + message.getReceiver());
System.out.println("Arg1 " + message.getArg1()); // System.out.println("Arg1 " + message.getArg1());
System.out.println("Arg2 " + message.getArg2()); // System.out.println("Arg2 " + message.getArg2());
System.out.println("Msg " + message.getMessage()); // System.out.println("Msg " + message.getMessage());
break; break;
case DETAILS: case DETAILS:
case ROOMHELPER: case ROOMHELPER:
@ -158,6 +194,7 @@ public class MyFreeCamsClient {
case JOINCHAN: case JOINCHAN:
case SESSIONSTATE: case SESSIONSTATE:
if(!message.getMessage().isEmpty()) { if(!message.getMessage().isEmpty()) {
//LOG.debug("SessionState: {}", message.getMessage());
JsonAdapter<SessionState> adapter = moshi.adapter(SessionState.class); JsonAdapter<SessionState> adapter = moshi.adapter(SessionState.class);
try { try {
SessionState sessionState = adapter.fromJson(message.getMessage()); SessionState sessionState = adapter.fromJson(message.getMessage());
@ -219,6 +256,21 @@ public class MyFreeCamsClient {
System.out.println("Arg2 " + message.getArg2()); System.out.println("Arg2 " + message.getArg2());
System.out.println("Msg " + message.getMessage()); System.out.println("Msg " + message.getMessage());
break; break;
case SLAVEVSHARE:
// LOG.debug("SLAVEVSHARE {}", message);
// LOG.debug("SLAVEVSHARE MSG [{}]", message.getMessage());
break;
case TKX:
json = new JSONObject(message.getMessage());
tkx = json.getString("tkx");
cxid = json.getInt("cxid");
ctxenc = URLDecoder.decode(json.getString("ctxenc"), "utf-8");
JSONArray ctxArray = json.getJSONArray("ctx");
ctx = new int[ctxArray.length()];
for (int i = 0; i < ctxArray.length(); i++) {
ctx[i] = ctxArray.getInt(i);
}
break;
default: default:
LOG.debug("Unknown message {}", message); LOG.debug("Unknown message {}", message);
break; break;
@ -240,7 +292,7 @@ public class MyFreeCamsClient {
String base = "http://www.myfreecams.com/php/FcwExtResp.php"; String base = "http://www.myfreecams.com/php/FcwExtResp.php";
String url = base + "?respkey="+respkey+"&opts="+opts+"&serv="+serv+"&type="+type; String url = base + "?respkey="+respkey+"&opts="+opts+"&serv="+serv+"&type="+type;
Request req = new Request.Builder().url(url).build(); Request req = new Request.Builder().url(url).build();
LOG.debug("Requesting EXTDATA {}", url); LOG.trace("Requesting EXTDATA {}", url);
Response resp = mfc.getHttpClient().execute(req); Response resp = mfc.getHttpClient().execute(req);
if(resp.isSuccessful()) { if(resp.isSuccessful()) {
@ -268,7 +320,7 @@ public class MyFreeCamsClient {
state.setLv(inner.getInt(idx++)); state.setLv(inner.getInt(idx++));
state.setU(new User()); state.setU(new User());
state.getU().setCamserv(inner.getInt(idx++)); state.getU().setCamserv(inner.getInt(idx++));
idx++; state.getU().setPhase(inner.getString(idx++));
state.getU().setChatColor(inner.getString(idx++)); state.getU().setChatColor(inner.getString(idx++));
state.getU().setChatFont(inner.getInt(idx++)); state.getU().setChatFont(inner.getInt(idx++));
state.getU().setChatOpt(inner.getInt(idx++)); state.getU().setChatOpt(inner.getInt(idx++));
@ -334,7 +386,12 @@ public class MyFreeCamsClient {
private void updateModel(SessionState state) { private void updateModel(SessionState state) {
// essential data not yet available // essential data not yet available
if(state.getNm() == null || state.getM() == null || state.getU() == null || state.getU().getCamserv() == null) { if(state.getNm() == null || state.getM() == null || state.getU() == null || state.getU().getCamserv() == null || state.getU().getCamserv() == 0) {
return;
}
// tokens not yet available
if(ctxenc == null) {
return; return;
} }
@ -344,7 +401,7 @@ public class MyFreeCamsClient {
model.setUid(state.getUid()); model.setUid(state.getUid());
models.put(state.getUid(), model); models.put(state.getUid(), model);
} }
model.update(state); model.update(state, getStreamUrl(state));
} }
private Message parseMessage(StringBuilder msg) throws UnsupportedEncodingException { private Message parseMessage(StringBuilder msg) throws UnsupportedEncodingException {
@ -406,7 +463,7 @@ public class MyFreeCamsClient {
try { try {
for (SessionState state : sessionStates.values()) { for (SessionState state : sessionStates.values()) {
if(Objects.equals(state.getNm(), model.getName())) { if(Objects.equals(state.getNm(), model.getName())) {
model.update(state); model.update(state, getStreamUrl(state));
return; return;
} }
} }
@ -415,6 +472,34 @@ public class MyFreeCamsClient {
} }
} }
String getStreamUrl(SessionState state) {
Integer camserv = state.getU().getCamserv();
if(camserv != null) {
int userChannel = 100000000 + state.getUid();
String streamUrl = "";
String phase = state.getU().getPhase() != null ? state.getU().getPhase() : "z";
if(serverConfig.isOnNgServer(state)) {
String server = serverConfig.ngVideoServers.get(camserv.toString());
streamUrl = "https://" + server + ".myfreecams.com:8444/x-hls/" + cxid + '/' + userChannel + '/' + ctxenc + "/mfc_" + phase + '_' + userChannel + ".m3u8";
//LOG.debug("{} {}", state.getNm(), streamUrl);
} else if(serverConfig.isOnWzObsVideoServer(state)) {
String server = serverConfig.wzobsServers.get(camserv.toString());
streamUrl = "https://"+ server + ".myfreecams.com/NxServer/ngrp:mfc_" + phase + '_' + userChannel + ".f4v_mobile/playlist.m3u8";
LOG.debug("{} isOnWzObsvideo: {}", state.getNm(), streamUrl);
} else if(serverConfig.isOnHtml5VideoServer(state)) {
String server = serverConfig.h5Servers.get(camserv.toString());
streamUrl = "https://"+ server + ".myfreecams.com/NxServer/ngrp:mfc_" + userChannel + ".f4v_mobile/playlist.m3u8";
} else {
if(camserv > 500) {
camserv -= 500;
}
streamUrl = "https://video" + camserv + ".myfreecams.com/NxServer/ngrp:mfc_" + userChannel + ".f4v_mobile/playlist.m3u8";
}
return streamUrl;
}
return null;
}
public MyFreeCamsModel getModel(int uid) { public MyFreeCamsModel getModel(int uid) {
return models.get(uid); return models.get(uid);
} }
@ -432,4 +517,8 @@ public class MyFreeCamsClient {
} }
} }
} }
public ServerConfig getServerConfig() {
return serverConfig;
}
} }

View File

@ -80,13 +80,16 @@ public class MyFreeCamsModel extends AbstractModel {
if(playlist.getStreamInfo().getResolution() != null) { if(playlist.getStreamInfo().getResolution() != null) {
src.width = playlist.getStreamInfo().getResolution().width; src.width = playlist.getStreamInfo().getResolution().width;
src.height = playlist.getStreamInfo().getResolution().height; src.height = playlist.getStreamInfo().getResolution().height;
String masterUrl = hlsUrl; } else {
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); src.width = Integer.MAX_VALUE;
String segmentUri = baseUrl + playlist.getUri(); src.height = Integer.MAX_VALUE;
src.mediaPlaylistUrl = segmentUri;
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
sources.add(src);
} }
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; return sources;
@ -100,11 +103,15 @@ public class MyFreeCamsModel extends AbstractModel {
Request req = new Request.Builder().url(hlsUrl).build(); Request req = new Request.Builder().url(hlsUrl).build();
Response response = site.getHttpClient().execute(req); Response response = site.getHttpClient().execute(req);
try { try {
InputStream inputStream = response.body().byteStream(); if(response.isSuccessful()) {
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); InputStream inputStream = response.body().byteStream();
Playlist playlist = parser.parse(); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
MasterPlaylist master = playlist.getMasterPlaylist(); Playlist playlist = parser.parse();
return master; MasterPlaylist master = playlist.getMasterPlaylist();
return master;
} else {
throw new IOException(response.code() + " " + response.message());
}
} finally { } finally {
response.close(); response.close();
} }
@ -199,9 +206,10 @@ public class MyFreeCamsModel extends AbstractModel {
this.state = state; this.state = state;
} }
public void update(SessionState state) { public void update(SessionState state, String streamUrl) {
setCamScore(state.getM().getCamscore()); setCamScore(state.getM().getCamscore());
setState(State.of(state.getVs())); setState(State.of(state.getVs()));
setStreamUrl(streamUrl);
// preview // preview
String uid = state.getUid().toString(); String uid = state.getUid().toString();
@ -209,24 +217,6 @@ public class MyFreeCamsModel extends AbstractModel {
String previewUrl = "https://img.mfcimg.com/photos2/"+uidStart+'/'+uid+"/avatar.300x300.jpg"; String previewUrl = "https://img.mfcimg.com/photos2/"+uidStart+'/'+uid+"/avatar.300x300.jpg";
setPreview(previewUrl); setPreview(previewUrl);
// stream url
Integer camserv = state.getU().getCamserv();
if(camserv != null) {
if(state.getM() != null) {
if(state.getM().getFlags() != null) {
int flags = state.getM().getFlags();
int hd = flags >> 18 & 0x1;
if(hd == 1) {
String hlsUrl = "http://video" + (camserv - 500) + ".myfreecams.com:1935/NxServer/ngrp:mfc_a_" + (100000000 + state.getUid()) + ".f4v_mobile/playlist.m3u8";
setStreamUrl(hlsUrl);
return;
}
}
}
String hlsUrl = "http://video" + (camserv - 500) + ".myfreecams.com:1935/NxServer/ngrp:mfc_" + (100000000 + state.getUid()) + ".f4v_mobile/playlist.m3u8";
setStreamUrl(hlsUrl);
}
// tags // tags
Optional.ofNullable(state.getM()).map((m) -> m.getTags()).ifPresent((tags) -> { Optional.ofNullable(state.getM()).map((m) -> m.getTags()).ifPresent((tags) -> {
ArrayList<String> t = new ArrayList<>(); ArrayList<String> t = new ArrayList<>();

View File

@ -37,6 +37,13 @@ public class MyFreeCamsTabProvider extends TabProvider {
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10)));
tabs.add(friends); tabs.add(friends);
updateService = new HDCamsUpdateService();
ThumbOverviewTab hd = new ThumbOverviewTab("HD", updateService, myFreeCams);
hd.setRecorder(recorder);
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10)));
tabs.add(hd);
return tabs; return tabs;
} }
} }

View File

@ -5,6 +5,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@ -21,7 +22,7 @@ public class ServerConfig {
Map<String, String> h5Servers; Map<String, String> h5Servers;
Map<String, String> wsServers; Map<String, String> wsServers;
Map<String, String> wzobsServers; Map<String, String> wzobsServers;
Map<String, String> ngVideo; Map<String, String> ngVideoServers;
public ServerConfig(HttpClient client) throws IOException { public ServerConfig(HttpClient client) throws IOException {
Request req = new Request.Builder().url("http://www.myfreecams.com/_js/serverconfig.js").build(); Request req = new Request.Builder().url("http://www.myfreecams.com/_js/serverconfig.js").build();
@ -35,7 +36,10 @@ public class ServerConfig {
h5Servers = parseMap(serverConfig, "h5video_servers"); h5Servers = parseMap(serverConfig, "h5video_servers");
wsServers = parseMap(serverConfig, "websocket_servers"); wsServers = parseMap(serverConfig, "websocket_servers");
wzobsServers = parseMap(serverConfig, "wzobs_servers"); wzobsServers = parseMap(serverConfig, "wzobs_servers");
ngVideo = parseMap(serverConfig, "ngvideo_servers"); ngVideoServers = parseMap(serverConfig, "ngvideo_servers");
// System.out.println("wz " + wzobsServers);
// System.out.println("ng " + ngVideoServers);
// System.out.println("h5 " + h5Servers);
} }
private static Map<String, String> parseMap(JSONObject serverConfig, String name) { private static Map<String, String> parseMap(JSONObject serverConfig, String name) {
@ -56,4 +60,18 @@ public class ServerConfig {
return result; return result;
} }
public boolean isOnNgServer(SessionState state) {
int camserv = Objects.requireNonNull(Objects.requireNonNull(state.getU()).getCamserv());
return ngVideoServers.containsKey(Integer.toString(camserv));
}
public boolean isOnWzObsVideoServer(SessionState state) {
int camserv = Objects.requireNonNull(Objects.requireNonNull(state.getU()).getCamserv());
return wzobsServers.containsKey(Integer.toString(camserv));
}
public boolean isOnHtml5VideoServer(SessionState state) {
int camserv = Objects.requireNonNull(Objects.requireNonNull(state.getU()).getCamserv());
return h5Servers.containsKey(Integer.toString(camserv)) || (camserv >= 904 && camserv <= 915 || camserv >= 940 && camserv <= 960);
}
} }

View File

@ -18,6 +18,7 @@ public class User {
private Integer photos; private Integer photos;
private Integer profile; private Integer profile;
private String status; private String status;
private String phase;
private Map<String, Object> additionalProperties = new HashMap<String, Object>(); private Map<String, Object> additionalProperties = new HashMap<String, Object>();
public Integer getAvatar() { public Integer getAvatar() {
@ -132,6 +133,14 @@ public class User {
this.additionalProperties.put(name, value); this.additionalProperties.put(name, value);
} }
public String getPhase() {
return phase;
}
public void setPhase(String phase) {
this.phase = phase;
}
public void merge(User u) { public void merge(User u) {
if (u == null) { if (u == null) {
return; return;
@ -149,6 +158,7 @@ public class User {
photos = u.photos != null ? u.photos : photos; photos = u.photos != null ? u.photos : photos;
profile = u.profile != null ? u.profile : profile; profile = u.profile != null ? u.profile : profile;
status = u.status != null ? u.status : status; status = u.status != null ? u.status : status;
phase = u.phase != null ? u.phase : phase;
additionalProperties.putAll(u.additionalProperties); additionalProperties.putAll(u.additionalProperties);
} }
} }

View File

@ -21,6 +21,7 @@ import com.squareup.moshi.Types;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Version; import ctbrec.Version;
import ctbrec.io.HttpClient;
import ctbrec.recorder.LocalRecorder; import ctbrec.recorder.LocalRecorder;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.recorder.RemoteRecorder; import ctbrec.recorder.RemoteRecorder;
@ -51,25 +52,24 @@ public class CamrecApplication extends Application {
private SettingsTab settingsTab; private SettingsTab settingsTab;
private TabPane rootPane = new TabPane(); private TabPane rootPane = new TabPane();
static EventBus bus; static EventBus bus;
private Site site;
private List<Site> sites = new ArrayList<>(); private List<Site> sites = new ArrayList<>();
public static HttpClient httpClient;
@Override @Override
public void start(Stage primaryStage) throws Exception { public void start(Stage primaryStage) throws Exception {
Chaturbate ctb = new Chaturbate(); sites.add(new Chaturbate());
sites.add(ctb); sites.add(new MyFreeCams());
site = new MyFreeCams();
sites.add(site);
loadConfig(); loadConfig();
createHttpClient();
bus = new AsyncEventBus(Executors.newSingleThreadExecutor()); bus = new AsyncEventBus(Executors.newSingleThreadExecutor());
hostServices = getHostServices(); hostServices = getHostServices();
createRecorder(); createRecorder();
for (Site site : sites) { for (Site site : sites) {
site.setRecorder(recorder); site.setRecorder(recorder);
site.init(); site.init();
} if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { site.login();
site.login(); }
} }
createGui(primaryStage); createGui(primaryStage);
checkForUpdates(); checkForUpdates();
@ -92,9 +92,9 @@ public class CamrecApplication extends Application {
} }
((SiteTab)rootPane.getTabs().get(0)).selected(); ((SiteTab)rootPane.getTabs().get(0)).selected();
RecordedModelsTab modelsTab = new RecordedModelsTab("Recording", recorder, site); RecordedModelsTab modelsTab = new RecordedModelsTab("Recording", recorder, sites);
rootPane.getTabs().add(modelsTab); rootPane.getTabs().add(modelsTab);
RecordingsTab recordingsTab = new RecordingsTab("Recordings", recorder, config, site); RecordingsTab recordingsTab = new RecordingsTab("Recordings", recorder, config, sites);
rootPane.getTabs().add(recordingsTab); rootPane.getTabs().add(recordingsTab);
settingsTab = new SettingsTab(); settingsTab = new SettingsTab();
rootPane.getTabs().add(settingsTab); rootPane.getTabs().add(settingsTab);
@ -124,7 +124,9 @@ public class CamrecApplication extends Application {
public void run() { public void run() {
settingsTab.saveConfig(); settingsTab.saveConfig();
recorder.shutdown(); recorder.shutdown();
site.shutdown(); for (Site site : sites) {
site.shutdown();
}
try { try {
Config.getInstance().save(); Config.getInstance().save();
LOG.info("Shutdown complete. Goodbye!"); LOG.info("Shutdown complete. Goodbye!");
@ -162,7 +164,7 @@ public class CamrecApplication extends Application {
if (config.getSettings().localRecording) { if (config.getSettings().localRecording) {
recorder = new LocalRecorder(config); recorder = new LocalRecorder(config);
} else { } else {
recorder = new RemoteRecorder(config, site.getHttpClient()); recorder = new RemoteRecorder(config, httpClient);
} }
} }
@ -180,6 +182,15 @@ public class CamrecApplication extends Application {
config = Config.getInstance(); config = Config.getInstance();
} }
private void createHttpClient() {
httpClient = new HttpClient() {
@Override
public boolean login() throws IOException {
return false;
}
};
}
public static void main(String[] args) { public static void main(String[] args) {
launch(args); launch(args);
} }
@ -189,7 +200,7 @@ public class CamrecApplication extends Application {
try { try {
String url = "https://api.github.com/repos/0xboobface/ctbrec/releases"; String url = "https://api.github.com/repos/0xboobface/ctbrec/releases";
Request request = new Request.Builder().url(url).build(); Request request = new Request.Builder().url(url).build();
Response response = site.getHttpClient().execute(request); Response response = httpClient.execute(request);
if (response.isSuccessful()) { if (response.isSuccessful()) {
Moshi moshi = new Moshi.Builder().build(); Moshi moshi = new Moshi.Builder().build();
Type type = Types.newParameterizedType(List.class, Release.class); Type type = Types.newParameterizedType(List.class, Release.class);

View File

@ -5,6 +5,7 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -26,6 +27,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
@ -59,7 +61,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
private ScheduledService<List<Model>> updateService; private ScheduledService<List<Model>> updateService;
private Recorder recorder; private Recorder recorder;
private Site site; private List<Site> sites;
FlowPane grid = new FlowPane(); FlowPane grid = new FlowPane();
ScrollPane scrollPane = new ScrollPane(); ScrollPane scrollPane = new ScrollPane();
@ -71,10 +73,10 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
TextField model = new TextField(); TextField model = new TextField();
Button addModelButton = new Button("Record"); Button addModelButton = new Button("Record");
public RecordedModelsTab(String title, Recorder recorder, Site site) { public RecordedModelsTab(String title, Recorder recorder, List<Site> sites) {
super(title); super(title);
this.recorder = recorder; this.recorder = recorder;
this.site = site; this.sites = sites;
createGui(); createGui();
setClosable(false); setClosable(false);
initializeUpdateService(); initializeUpdateService();
@ -126,18 +128,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
modelLabel.setPadding(new Insets(5, 0, 0, 0)); modelLabel.setPadding(new Insets(5, 0, 0, 0));
model.setPrefWidth(300); model.setPrefWidth(300);
BorderPane.setMargin(addModelBox, new Insets(5)); BorderPane.setMargin(addModelBox, new Insets(5));
addModelButton.setOnAction((e) -> { addModelButton.setOnAction((e) -> addModel(e));
Model m = site.createModel(model.getText());
try {
recorder.startRecording(m);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't add model");
alert.setContentText("The model " + m.getName() + " could not be added: " + e1.getLocalizedMessage());
alert.showAndWait();
}
});
BorderPane root = new BorderPane(); BorderPane root = new BorderPane();
root.setPadding(new Insets(5)); root.setPadding(new Insets(5));
@ -146,6 +137,43 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
setContent(root); setContent(root);
} }
private void addModel(ActionEvent e) {
String[] parts = model.getText().trim().split(":");
if (parts.length != 2) {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Wrong format");
alert.setHeaderText("Couldn't add model");
alert.setContentText("Use something like \"MyFreeCams:ModelName\"");
alert.showAndWait();
return;
}
String siteName = parts[0];
String modelName = parts[1];
for (Site site : sites) {
if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) {
try {
Model m = site.createModel(modelName);
recorder.startRecording(m);
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't add model");
alert.setContentText("The model " + modelName + " could not be added: " + e1.getLocalizedMessage());
alert.showAndWait();
}
return;
}
}
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Unknown site");
alert.setHeaderText("Couldn't add model");
alert.setContentText("The site you entered is unknown");
alert.showAndWait();
};
void initializeUpdateService() { void initializeUpdateService() {
updateService = createUpdateService(); updateService = createUpdateService();
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2))); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(2)));
@ -189,7 +217,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
return new Task<List<Model>>() { return new Task<List<Model>>() {
@Override @Override
public List<Model> call() { public List<Model> call() {
LOG.debug("Updating recorded models"); LOG.trace("Updating recorded models");
return recorder.getModelsRecording(); return recorder.getModelsRecording();
} }
}; };
@ -279,7 +307,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
showStreamSwitchErrorDialog(t); showStreamSwitchErrorDialog(t);
return null; return null;
}; };
StreamSourceSelectionDialog.show(fxModel.getDelegate(), site.getHttpClient(), onSuccess, onFail); StreamSourceSelectionDialog.show(fxModel.getDelegate(), onSuccess, onFail);
} }
private void showStreamSwitchErrorDialog(Throwable throwable) { private void showStreamSwitchErrorDialog(Throwable throwable) {

View File

@ -28,7 +28,6 @@ import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.Recording.STATUS; import ctbrec.Recording.STATUS;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
@ -66,7 +65,8 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
private ScheduledService<List<JavaFxRecording>> updateService; private ScheduledService<List<JavaFxRecording>> updateService;
private Config config; private Config config;
private Recorder recorder; private Recorder recorder;
private Site site; @SuppressWarnings("unused")
private List<Site> sites;
FlowPane grid = new FlowPane(); FlowPane grid = new FlowPane();
ScrollPane scrollPane = new ScrollPane(); ScrollPane scrollPane = new ScrollPane();
@ -74,11 +74,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
ObservableList<JavaFxRecording> observableRecordings = FXCollections.observableArrayList(); ObservableList<JavaFxRecording> observableRecordings = FXCollections.observableArrayList();
ContextMenu popup; ContextMenu popup;
public RecordingsTab(String title, Recorder recorder, Config config, Site site) { public RecordingsTab(String title, Recorder recorder, Config config, List<Site> sites) {
super(title); super(title);
this.recorder = recorder; this.recorder = recorder;
this.config = config; this.config = config;
this.site = site; this.sites = sites;
createGui(); createGui();
setClosable(false); setClosable(false);
initializeUpdateService(); initializeUpdateService();
@ -245,18 +245,19 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
contextMenu.getItems().add(openInPlayer); contextMenu.getItems().add(openInPlayer);
} }
MenuItem stopRecording = new MenuItem("Stop recording"); // TODO find a way to reenable this
stopRecording.setOnAction((e) -> { // MenuItem stopRecording = new MenuItem("Stop recording");
Model m = site.createModel(recording.getModelName()); // stopRecording.setOnAction((e) -> {
try { // Model m = site.createModel(recording.getModelName());
recorder.stopRecording(m); // try {
} catch (Exception e1) { // recorder.stopRecording(m);
showErrorDialog("Stop recording", "Couldn't stop recording of model " + m.getName(), e1); // } catch (Exception e1) {
} // showErrorDialog("Stop recording", "Couldn't stop recording of model " + m.getName(), e1);
}); // }
if(recording.getStatus() == STATUS.RECORDING) { // });
contextMenu.getItems().add(stopRecording); // if(recording.getStatus() == STATUS.RECORDING) {
} // contextMenu.getItems().add(stopRecording);
// }
MenuItem deleteRecording = new MenuItem("Delete"); MenuItem deleteRecording = new MenuItem("Delete");
deleteRecording.setOnAction((e) -> { deleteRecording.setOnAction((e) -> {
@ -304,7 +305,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
@Override @Override
public void run() { public void run() {
try { try {
MergedHlsDownload download = new MergedHlsDownload(site.getHttpClient()); MergedHlsDownload download = new MergedHlsDownload(CamrecApplication.httpClient);
download.start(url.toString(), target, (progress) -> { download.start(url.toString(), target, (progress) -> {
Platform.runLater(() -> { Platform.runLater(() -> {
if (progress == 100) { if (progress == 100) {

View File

@ -6,13 +6,12 @@ import java.util.concurrent.ExecutionException;
import java.util.function.Function; import java.util.function.Function;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.io.HttpClient;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.scene.control.ChoiceDialog; import javafx.scene.control.ChoiceDialog;
public class StreamSourceSelectionDialog { public class StreamSourceSelectionDialog {
public static void show(Model model, HttpClient client, Function<Model,Void> onSuccess, Function<Throwable, Void> onFail) { public static void show(Model model, Function<Model,Void> onSuccess, Function<Throwable, Void> onFail) {
Task<List<StreamSource>> selectStreamSource = new Task<List<StreamSource>>() { Task<List<StreamSource>> selectStreamSource = new Task<List<StreamSource>>() {
@Override @Override
protected List<StreamSource> call() throws Exception { protected List<StreamSource> call() throws Exception {

View File

@ -15,7 +15,6 @@ import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.io.HttpClient;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import javafx.animation.FadeTransition; import javafx.animation.FadeTransition;
@ -72,17 +71,14 @@ public class ThumbCell extends StackPane {
private SimpleBooleanProperty selectionProperty = new SimpleBooleanProperty(false); private SimpleBooleanProperty selectionProperty = new SimpleBooleanProperty(false);
private double imgAspectRatio = 3.0 / 4.0; private double imgAspectRatio = 3.0 / 4.0;
private HttpClient client;
private ObservableList<Node> thumbCellList; private ObservableList<Node> thumbCellList;
private boolean mouseHovering = false; private boolean mouseHovering = false;
private boolean recording = false; private boolean recording = false;
public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder, HttpClient client) { public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder) {
this.thumbCellList = parent.grid.getChildren(); this.thumbCellList = parent.grid.getChildren();
this.model = model; this.model = model;
this.recorder = recorder; this.recorder = recorder;
this.client = client;
recording = recorder.isRecording(model); recording = recorder.isRecording(model);
iv = new ImageView(); iv = new ImageView();
@ -208,7 +204,7 @@ public class ThumbCell extends StackPane {
// when we first requested the stream info, so we remove this invalid value from the "cache" // when we first requested the stream info, so we remove this invalid value from the "cache"
// so that it is requested again // so that it is requested again
if (model.isOnline() && resolution[1] == 0) { if (model.isOnline() && resolution[1] == 0) {
LOG.debug("Removing invalid resolution value for {}", model.getName()); LOG.trace("Removing invalid resolution value for {}", model.getName());
model.invalidateCacheEntries(); model.invalidateCacheEntries();
} }
} catch (ExecutionException | IOException | InterruptedException e1) { } catch (ExecutionException | IOException | InterruptedException e1) {
@ -227,7 +223,7 @@ public class ThumbCell extends StackPane {
LOG.trace("Model resolution {} {}x{}", model.getName(), resolution[0], resolution[1]); LOG.trace("Model resolution {} {}x{}", model.getName(), resolution[0], resolution[1]);
LOG.trace("Resolution queue size: {}", ThumbOverviewTab.queue.size()); LOG.trace("Resolution queue size: {}", ThumbOverviewTab.queue.size());
final int w = resolution[1]; final int w = resolution[1];
_res = w > 0 ? Integer.toString(w) : state; _res = w > 0 ? w != Integer.MAX_VALUE ? Integer.toString(w) : "HD" : state;
} else { } else {
_res = model.getOnlineState(false); _res = model.getOnlineState(false);
resolutionBackgroundColor = resolutionOfflineColor; resolutionBackgroundColor = resolutionOfflineColor;
@ -336,7 +332,7 @@ public class ThumbCell extends StackPane {
alert.showAndWait(); alert.showAndWait();
return null; return null;
}; };
StreamSourceSelectionDialog.show(model, client, onSuccess, onFail); StreamSourceSelectionDialog.show(model, onSuccess, onFail);
} else { } else {
_startStopAction(model, start); _startStopAction(model, start);
} }

View File

@ -27,7 +27,6 @@ import com.sun.javafx.collections.ObservableListWrapper;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.io.HttpClient;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import ctbrec.sites.mfc.MyFreeCamsClient; import ctbrec.sites.mfc.MyFreeCamsClient;
@ -262,7 +261,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
} }
} }
if(!found) { if(!found) {
ThumbCell newCell = createThumbCell(this, model, recorder, site.getHttpClient()); ThumbCell newCell = createThumbCell(this, model, recorder);
newCell.setIndex(index); newCell.setIndex(index);
positionChangedOrNew.add(newCell); positionChangedOrNew.add(newCell);
} }
@ -285,8 +284,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
} }
} }
ThumbCell createThumbCell(ThumbOverviewTab thumbOverviewTab, Model model, Recorder recorder, HttpClient client2) { ThumbCell createThumbCell(ThumbOverviewTab thumbOverviewTab, Model model, Recorder recorder) {
ThumbCell newCell = new ThumbCell(this, model, recorder, site.getHttpClient()); ThumbCell newCell = new ThumbCell(this, model, recorder);
newCell.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { newCell.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
suspendUpdates(true); suspendUpdates(true);
popup = createContextMenu(newCell); popup = createContextMenu(newCell);