forked from j62/ctbrec
1
0
Fork 0

Abstract more stuff in the site packages

This commit is contained in:
0xboobface 2018-10-21 19:06:01 +02:00
parent 362d90b29b
commit 387661cfdf
32 changed files with 764 additions and 688 deletions

View File

@ -30,5 +30,6 @@ public interface Model {
public void invalidateCacheEntries(); public void invalidateCacheEntries();
public void receiveTip(int tokens) throws IOException; public void receiveTip(int tokens) throws IOException;
public int[] getStreamResolution(boolean failFast) throws ExecutionException; public int[] getStreamResolution(boolean failFast) throws ExecutionException;
public boolean follow() throws IOException;
public boolean unfollow() throws IOException;
} }

View File

@ -28,7 +28,6 @@ public class Settings {
public boolean determineResolution = false; public boolean determineResolution = false;
public boolean requireAuthentication = false; public boolean requireAuthentication = false;
public boolean chooseStreamQuality = false; public boolean chooseStreamQuality = false;
public boolean recordFollowed = false;
public byte[] key = null; public byte[] key = null;
public ProxyType proxyType = ProxyType.DIRECT; public ProxyType proxyType = ProxyType.DIRECT;
public String proxyHost; public String proxyHost;

View File

@ -1,44 +1,24 @@
package ctbrec.io; package ctbrec.io;
import java.io.IOException; import java.io.IOException;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Settings.ProxyType; import ctbrec.Settings.ProxyType;
import ctbrec.ui.CamrecApplication;
import ctbrec.ui.CookieJarImpl; import ctbrec.ui.CookieJarImpl;
import ctbrec.ui.HtmlParser;
import okhttp3.ConnectionPool; import okhttp3.ConnectionPool;
import okhttp3.Cookie;
import okhttp3.FormBody;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
public class HttpClient { public abstract class HttpClient {
private static final transient Logger LOG = LoggerFactory.getLogger(HttpClient.class); protected OkHttpClient client;
private static HttpClient instance = new HttpClient(); protected CookieJarImpl cookieJar = new CookieJarImpl();
protected boolean loggedIn = false;
protected int loginTries = 0;
private OkHttpClient client; protected HttpClient() {
private CookieJarImpl cookieJar = new CookieJarImpl(); reconfigure();
private boolean loggedIn = false;
private int loginTries = 0;
private String token;
private HttpClient() {
loadProxySettings();
client = new OkHttpClient.Builder()
.cookieJar(cookieJar)
.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();
} }
private void loadProxySettings() { private void loadProxySettings() {
@ -89,24 +69,11 @@ public class HttpClient {
} }
} }
public static HttpClient getInstance() {
return instance;
}
public Response execute(Request request) throws IOException { public Response execute(Request request) throws IOException {
Response resp = execute(request, false); Response resp = execute(request, false);
return resp; return resp;
} }
private void extractCsrfToken(Request request) {
try {
Cookie csrfToken = cookieJar.getCookie(request.url(), "csrftoken");
token = csrfToken.value();
} catch(NoSuchElementException e) {
LOG.trace("CSRF token not found in cookies");
}
}
public Response execute(Request req, boolean requiresLogin) throws IOException { public Response execute(Request req, boolean requiresLogin) throws IOException {
if(requiresLogin && !loggedIn) { if(requiresLogin && !loggedIn) {
boolean loginSuccessful = login(); boolean loginSuccessful = login();
@ -115,64 +82,20 @@ public class HttpClient {
} }
} }
Response resp = client.newCall(req).execute(); Response resp = client.newCall(req).execute();
extractCsrfToken(req);
return resp; return resp;
} }
public boolean login() throws IOException { public abstract boolean login() throws IOException;
try {
Request login = new Request.Builder()
.url(CamrecApplication.BASE_URI + "/auth/login/")
.build();
Response response = client.newCall(login).execute();
String content = response.body().string();
token = HtmlParser.getTag(content, "input[name=csrfmiddlewaretoken]").attr("value");
LOG.debug("csrf token is {}", token);
RequestBody body = new FormBody.Builder()
.add("username", Config.getInstance().getSettings().username)
.add("password", Config.getInstance().getSettings().password)
.add("next", "")
.add("csrfmiddlewaretoken", token)
.build();
login = new Request.Builder()
.url(CamrecApplication.BASE_URI + "/auth/login/")
.header("Referer", CamrecApplication.BASE_URI + "/auth/login/")
.post(body)
.build();
response = client.newCall(login).execute();
if(response.isSuccessful()) {
content = response.body().string();
if(content.contains("Login, Chaturbate login")) {
loggedIn = false;
} else {
loggedIn = true;
extractCsrfToken(login);
}
} else {
if(loginTries++ < 3) {
login();
} else {
throw new IOException("Login failed: " + response.code() + " " + response.message());
}
}
response.close();
} finally {
loginTries = 0;
}
return loggedIn;
}
public void reconfigure() { public void reconfigure() {
instance = new HttpClient(); loadProxySettings();
} client = new OkHttpClient.Builder()
.cookieJar(cookieJar)
public String getToken() throws IOException { .connectTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS)
if(token == null) { .readTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS)
login(); .connectionPool(new ConnectionPool(50, 10, TimeUnit.MINUTES))
} //.addInterceptor(new LoggingInterceptor())
return token; .build();
} }
public void shutdown() { public void shutdown() {

View File

@ -1,8 +1,6 @@
package ctbrec.recorder; package ctbrec.recorder;
import static ctbrec.Recording.STATUS.FINISHED; import static ctbrec.Recording.STATUS.*;
import static ctbrec.Recording.STATUS.GENERATING_PLAYLIST;
import static ctbrec.Recording.STATUS.RECORDING;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -30,32 +28,27 @@ import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.io.HttpClient;
import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException; import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException;
import ctbrec.recorder.download.Download; import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.HlsDownload; import ctbrec.recorder.download.HlsDownload;
import ctbrec.recorder.download.MergedHlsDownload; import ctbrec.recorder.download.MergedHlsDownload;
import ctbrec.sites.chaturbate.ChaturbateModelParser; import ctbrec.recorder.server.RecorderHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class LocalRecorder implements Recorder { public class LocalRecorder implements Recorder {
private static final transient Logger LOG = LoggerFactory.getLogger(LocalRecorder.class); private static final transient Logger LOG = LoggerFactory.getLogger(LocalRecorder.class);
private static final boolean IGNORE_CACHE = true; private static final boolean IGNORE_CACHE = true;
private List<Model> followedModels = Collections.synchronizedList(new ArrayList<>());
private List<Model> models = Collections.synchronizedList(new ArrayList<>()); private List<Model> models = Collections.synchronizedList(new ArrayList<>());
private Map<Model, Download> recordingProcesses = Collections.synchronizedMap(new HashMap<>()); private Map<Model, Download> recordingProcesses = Collections.synchronizedMap(new HashMap<>());
private Map<File, PlaylistGenerator> playlistGenerators = new HashMap<>(); private Map<File, PlaylistGenerator> playlistGenerators = new HashMap<>();
private Config config; private Config config;
private ProcessMonitor processMonitor; private ProcessMonitor processMonitor;
private OnlineMonitor onlineMonitor; private OnlineMonitor onlineMonitor;
private FollowedMonitor followedMonitor;
private PlaylistGeneratorTrigger playlistGenTrigger; private PlaylistGeneratorTrigger playlistGenTrigger;
private HttpClient client = HttpClient.getInstance();
private volatile boolean recording = true; private volatile boolean recording = true;
private List<File> deleteInProgress = Collections.synchronizedList(new ArrayList<>()); private List<File> deleteInProgress = Collections.synchronizedList(new ArrayList<>());
private RecorderHttpClient client = new RecorderHttpClient();
public LocalRecorder(Config config) { public LocalRecorder(Config config) {
this.config = config; this.config = config;
@ -74,11 +67,6 @@ public class LocalRecorder implements Recorder {
playlistGenTrigger.start(); playlistGenTrigger.start();
} }
if (config.getSettings().recordFollowed) {
followedMonitor = new FollowedMonitor();
followedMonitor.start();
}
LOG.debug("Recorder initialized"); LOG.debug("Recorder initialized");
LOG.info("Models to record: {}", models); LOG.info("Models to record: {}", models);
LOG.info("Saving recordings in {}", config.getSettings().recordingsDir); LOG.info("Saving recordings in {}", config.getSettings().recordingsDir);
@ -88,9 +76,6 @@ public class LocalRecorder implements Recorder {
public void startRecording(Model model) { public void startRecording(Model model) {
if (!models.contains(model)) { if (!models.contains(model)) {
LOG.info("Model {} added", model); LOG.info("Model {} added", model);
if (followedModels.contains(model)) {
followedModels.remove(model);
}
models.add(model); models.add(model);
config.getSettings().models.add(model); config.getSettings().models.add(model);
} }
@ -98,9 +83,8 @@ public class LocalRecorder implements Recorder {
@Override @Override
public void stopRecording(Model model) throws IOException { public void stopRecording(Model model) throws IOException {
if (models.contains(model) || followedModels.contains(model)) { if (models.contains(model)) {
models.remove(model); models.remove(model);
followedModels.remove(model);
config.getSettings().models.remove(model); config.getSettings().models.remove(model);
if (recordingProcesses.containsKey(model)) { if (recordingProcesses.containsKey(model)) {
stopRecordingProcess(model); stopRecordingProcess(model);
@ -118,7 +102,7 @@ public class LocalRecorder implements Recorder {
return; return;
} }
if (!models.contains(model) && !followedModels.contains(model)) { if (!models.contains(model)) {
LOG.info("Model {} has been removed. Restarting of recording cancelled.", model); LOG.info("Model {} has been removed. Restarting of recording cancelled.", model);
return; return;
} }
@ -151,15 +135,12 @@ public class LocalRecorder implements Recorder {
@Override @Override
public boolean isRecording(Model model) { public boolean isRecording(Model model) {
return models.contains(model) || followedModels.contains(model); return models.contains(model);
} }
@Override @Override
public List<Model> getModelsRecording() { public List<Model> getModelsRecording() {
List<Model> union = new ArrayList<>(); return Collections.unmodifiableList(models);
union.addAll(models);
union.addAll(followedModels);
return Collections.unmodifiableList(union);
} }
@Override @Override
@ -170,11 +151,9 @@ public class LocalRecorder implements Recorder {
onlineMonitor.running = false; onlineMonitor.running = false;
processMonitor.running = false; processMonitor.running = false;
playlistGenTrigger.running = false; playlistGenTrigger.running = false;
if (followedMonitor != null) {
followedMonitor.running = false;
}
LOG.debug("Stopping all recording processes"); LOG.debug("Stopping all recording processes");
stopRecordingProcesses(); stopRecordingProcesses();
client.shutdown();
} }
private void stopRecordingProcesses() { private void stopRecordingProcesses() {
@ -251,54 +230,6 @@ public class LocalRecorder implements Recorder {
} }
} }
private class FollowedMonitor extends Thread {
private volatile boolean running = false;
public FollowedMonitor() {
setName("FollowedMonitor");
setDaemon(true);
}
@Override
public void run() {
running = true;
while (running) {
try {
String url = "https://chaturbate.com/followed-cams/?page=1&keywords=&_=" + System.currentTimeMillis();
LOG.debug("Fetching page {}", url);
Request request = new Request.Builder().url(url).build();
Response response = client.execute(request, true);
if (response.isSuccessful()) {
List<Model> followed = ChaturbateModelParser.parseModels(response.body().string());
response.close();
followedModels.clear();
for (Model model : followed) {
if (!followedModels.contains(model) && !models.contains(model)) {
LOG.info("Model {} added", model);
followedModels.add(model);
}
}
onlineMonitor.interrupt();
} else {
int code = response.code();
response.close();
LOG.error("Couldn't retrieve followed models. HTTP status {}", code);
}
} catch (IOException e) {
LOG.error("Couldn't retrieve followed models.", e);
}
try {
if (running)
Thread.sleep(10000);
} catch (InterruptedException e) {
LOG.error("Couldn't sleep", e);
}
}
LOG.debug(getName() + " terminated");
}
}
private void finishRecording(File directory) { private void finishRecording(File directory) {
Thread t = new Thread() { Thread t = new Thread() {
@Override @Override

View File

@ -1,9 +1,12 @@
package ctbrec.recorder.server; package ctbrec.recorder.server;
import static javax.servlet.http.HttpServletResponse.*;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -33,20 +36,20 @@ public class HlsServlet extends AbstractCtbrecServlet {
File recordingsDir = new File(config.getSettings().recordingsDir); File recordingsDir = new File(config.getSettings().recordingsDir);
File requestedFile = new File(recordingsDir, request); File requestedFile = new File(recordingsDir, request);
// try { try {
// boolean isRequestAuthenticated = checkAuthentication(req, req.getRequestURI()); boolean isRequestAuthenticated = checkAuthentication(req, req.getRequestURI());
// if (!isRequestAuthenticated) { if (!isRequestAuthenticated) {
// resp.setStatus(SC_UNAUTHORIZED); resp.setStatus(SC_UNAUTHORIZED);
// String response = "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}"; String response = "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}";
// resp.getWriter().write(response); resp.getWriter().write(response);
// return; return;
// } }
// } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
// resp.setStatus(SC_UNAUTHORIZED); resp.setStatus(SC_UNAUTHORIZED);
// String response = "{\"status\": \"error\", \"msg\": \"Authentication failed\"}"; String response = "{\"status\": \"error\", \"msg\": \"Authentication failed\"}";
// resp.getWriter().write(response); resp.getWriter().write(response);
// return; return;
// } }
if (requestedFile.getCanonicalPath().startsWith(config.getSettings().recordingsDir)) { if (requestedFile.getCanonicalPath().startsWith(config.getSettings().recordingsDir)) {
if (requestedFile.getName().equals("playlist.m3u8")) { if (requestedFile.getName().equals("playlist.m3u8")) {

View File

@ -0,0 +1,13 @@
package ctbrec.recorder.server;
import java.io.IOException;
import ctbrec.io.HttpClient;
public class RecorderHttpClient extends HttpClient {
@Override
public boolean login() throws IOException {
return false;
}
}

View File

@ -1,5 +1,9 @@
package ctbrec; package ctbrec.sites;
import java.io.IOException;
import ctbrec.Model;
import ctbrec.io.HttpClient;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.ui.TabProvider; import ctbrec.ui.TabProvider;
@ -10,4 +14,9 @@ public interface Site {
public void setRecorder(Recorder recorder); public void setRecorder(Recorder recorder);
public TabProvider getTabProvider(); public TabProvider getTabProvider();
public Model createModel(String name); public Model createModel(String name);
public Integer getTokenBalance() throws IOException;
public String getBuyTokensLink();
public void login() throws IOException;
public HttpClient getHttpClient();
public void shutdown();
} }

View File

@ -1,13 +1,49 @@
package ctbrec.sites.chaturbate; package ctbrec.sites.chaturbate;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
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 com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Site; import ctbrec.Settings;
import ctbrec.io.HttpClient;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.HtmlParser;
import ctbrec.ui.TabProvider; import ctbrec.ui.TabProvider;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class Chaturbate implements Site { public class Chaturbate implements Site {
private static final transient Logger LOG = LoggerFactory.getLogger(Chaturbate.class);
public static final String BASE_URI = "https://chaturbate.com";
public static final String AFFILIATE_LINK = BASE_URI + "/in/?track=default&tour=LQps&campaign=55vTi&room=0xb00bface";
private Recorder recorder; private Recorder recorder;
private ChaturbateHttpClient httpClient = new ChaturbateHttpClient();
@Override @Override
public String getName() { public String getName() {
@ -36,9 +72,214 @@ public class Chaturbate implements Site {
@Override @Override
public Model createModel(String name) { public Model createModel(String name) {
ChaturbateModel m = new ChaturbateModel(); ChaturbateModel m = new ChaturbateModel(this);
m.setName(name); m.setName(name);
m.setUrl(getBaseUrl() + '/' + name + '/'); m.setUrl(getBaseUrl() + '/' + name + '/');
return m; return m;
} }
@Override
public Integer getTokenBalance() throws IOException {
String username = Config.getInstance().getSettings().username;
if (username == null || username.trim().isEmpty()) {
throw new IOException("Not logged in");
}
String url = "https://chaturbate.com/p/" + username + "/";
Request req = new Request.Builder().url(url).build();
Response resp = httpClient.execute(req, true);
if (resp.isSuccessful()) {
String profilePage = resp.body().string();
String tokenText = HtmlParser.getText(profilePage, "span.tokencount");
int tokens = Integer.parseInt(tokenText);
return tokens;
} else {
throw new IOException("HTTP response: " + resp.code() + " - " + resp.message());
}
}
@Override
public String getBuyTokensLink() {
return AFFILIATE_LINK;
}
@Override
public void login() {
Settings settings = Config.getInstance().getSettings();
if (settings.username != null && !settings.username.isEmpty()) {
new Thread() {
@Override
public void run() {
try {
httpClient.login();
} catch (IOException e1) {
LOG.warn("Initial login failed", e1);
}
};
}.start();
}
}
@Override
public HttpClient getHttpClient() {
return httpClient;
}
@Override
public void shutdown() {
httpClient.shutdown();
}
// #######################
private long lastRequest = System.currentTimeMillis();
LoadingCache<String, StreamInfo> streamInfoCache = CacheBuilder.newBuilder()
.initialCapacity(10_000)
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(new CacheLoader<String, StreamInfo> () {
@Override
public StreamInfo load(String model) throws Exception {
return loadStreamInfo(model);
}
});
LoadingCache<String, int[]> streamResolutionCache = CacheBuilder.newBuilder()
.initialCapacity(10_000)
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(new CacheLoader<String, int[]> () {
@Override
public int[] load(String model) throws Exception {
return loadResolution(model);
}
});
public void sendTip(String name, int tokens) throws IOException {
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
RequestBody body = new FormBody.Builder()
.add("csrfmiddlewaretoken", httpClient.getToken())
.add("tip_amount", Integer.toString(tokens))
.add("tip_room_type", "public")
.build();
Request req = new Request.Builder()
.url("https://chaturbate.com/tipping/send_tip/"+name+"/")
.post(body)
.addHeader("Referer", "https://chaturbate.com/"+name+"/")
.addHeader("X-Requested-With", "XMLHttpRequest")
.build();
try(Response response = httpClient.execute(req, true)) {
if(!response.isSuccessful()) {
throw new IOException(response.code() + " " + response.message());
}
}
}
}
StreamInfo getStreamInfo(String modelName) throws IOException, ExecutionException {
return streamInfoCache.get(modelName);
}
StreamInfo loadStreamInfo(String modelName) throws IOException, InterruptedException {
throttleRequests();
RequestBody body = new FormBody.Builder()
.add("room_slug", modelName)
.add("bandwidth", "high")
.build();
Request req = new Request.Builder()
.url("https://chaturbate.com/get_edge_hls_url_ajax/")
.post(body)
.addHeader("X-Requested-With", "XMLHttpRequest")
.build();
Response response = httpClient.execute(req);
try {
if(response.isSuccessful()) {
String content = response.body().string();
LOG.trace("Raw stream info: {}", content);
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<StreamInfo> adapter = moshi.adapter(StreamInfo.class);
StreamInfo streamInfo = adapter.fromJson(content);
streamInfoCache.put(modelName, streamInfo);
return streamInfo;
} else {
int code = response.code();
String message = response.message();
throw new IOException("Server responded with " + code + " - " + message + " headers: [" + response.headers() + "]");
}
} finally {
response.close();
}
}
public int[] getResolution(String modelName) throws ExecutionException {
return streamResolutionCache.get(modelName);
}
private int[] loadResolution(String modelName) throws IOException, ParseException, PlaylistException, ExecutionException, InterruptedException {
int[] res = new int[2];
StreamInfo streamInfo = getStreamInfo(modelName);
if(!streamInfo.url.startsWith("http")) {
return res;
}
EOFException ex = null;
for(int i=0; i<2; i++) {
try {
MasterPlaylist master = getMasterPlaylist(modelName);
for (PlaylistData playlistData : master.getPlaylists()) {
if(playlistData.hasStreamInfo() && playlistData.getStreamInfo().hasResolution()) {
int h = playlistData.getStreamInfo().getResolution().height;
int w = playlistData.getStreamInfo().getResolution().width;
if(w > res[1]) {
res[0] = w;
res[1] = h;
}
}
}
ex = null;
break; // this attempt worked, exit loop
} catch(EOFException e) {
// the cause might be, that the playlist url in streaminfo is outdated,
// so let's remove it from cache and retry in the next iteration
streamInfoCache.invalidate(modelName);
ex = e;
}
}
if(ex != null) {
throw ex;
}
streamResolutionCache.put(modelName, res);
return res;
}
private void throttleRequests() throws InterruptedException {
long now = System.currentTimeMillis();
long diff = now - lastRequest;
if(diff < 500) {
Thread.sleep(diff);
}
lastRequest = now;
}
public MasterPlaylist getMasterPlaylist(String modelName) throws IOException, ParseException, PlaylistException, ExecutionException {
StreamInfo streamInfo = getStreamInfo(modelName);
return getMasterPlaylist(streamInfo);
}
public MasterPlaylist getMasterPlaylist(StreamInfo streamInfo) throws IOException, ParseException, PlaylistException {
LOG.trace("Loading master playlist {}", streamInfo.url);
Request req = new Request.Builder().url(streamInfo.url).build();
Response response = httpClient.execute(req);
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();
}
}
} }

View File

@ -0,0 +1,91 @@
package ctbrec.sites.chaturbate;
import java.io.IOException;
import java.util.NoSuchElementException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import ctbrec.ui.HtmlParser;
import okhttp3.Cookie;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class ChaturbateHttpClient extends HttpClient {
private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateHttpClient.class);
protected String token;
private void extractCsrfToken(Request request) {
try {
Cookie csrfToken = cookieJar.getCookie(request.url(), "csrftoken");
token = csrfToken.value();
} catch(NoSuchElementException e) {
LOG.trace("CSRF token not found in cookies");
}
}
public String getToken() throws IOException {
if(token == null) {
login();
}
return token;
}
@Override
public boolean login() throws IOException {
try {
Request login = new Request.Builder()
.url(Chaturbate.BASE_URI + "/auth/login/")
.build();
Response response = client.newCall(login).execute();
String content = response.body().string();
token = HtmlParser.getTag(content, "input[name=csrfmiddlewaretoken]").attr("value");
LOG.debug("csrf token is {}", token);
RequestBody body = new FormBody.Builder()
.add("username", Config.getInstance().getSettings().username)
.add("password", Config.getInstance().getSettings().password)
.add("next", "")
.add("csrfmiddlewaretoken", token)
.build();
login = new Request.Builder()
.url(Chaturbate.BASE_URI + "/auth/login/")
.header("Referer", Chaturbate.BASE_URI + "/auth/login/")
.post(body)
.build();
response = client.newCall(login).execute();
if(response.isSuccessful()) {
content = response.body().string();
if(content.contains("Login, Chaturbate login")) {
loggedIn = false;
} else {
loggedIn = true;
extractCsrfToken(login);
}
} else {
if(loginTries++ < 3) {
login();
} else {
throw new IOException("Login failed: " + response.code() + " " + response.message());
}
}
response.close();
} finally {
loginTries = 0;
}
return loggedIn;
}
@Override
public Response execute(Request req, boolean requiresLogin) throws IOException {
Response resp = super.execute(req, requiresLogin);
extractCsrfToken(req);
return resp;
}
}

View File

@ -1,35 +1,23 @@
package ctbrec.sites.chaturbate; package ctbrec.sites.chaturbate;
import java.io.EOFException; import static ctbrec.sites.chaturbate.Chaturbate.*;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException; import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MasterPlaylist; import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData; import com.iheartradio.m3u8.data.PlaylistData;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.AbstractModel; import ctbrec.AbstractModel;
import ctbrec.io.HttpClient;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import okhttp3.FormBody;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.Response; import okhttp3.Response;
@ -37,6 +25,11 @@ import okhttp3.Response;
public class ChaturbateModel extends AbstractModel { public class ChaturbateModel extends AbstractModel {
private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateModel.class); private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateModel.class);
private Chaturbate site;
ChaturbateModel(Chaturbate site) {
this.site = site;
}
@Override @Override
public boolean isOnline() throws IOException, ExecutionException, InterruptedException { public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
@ -47,24 +40,24 @@ public class ChaturbateModel extends AbstractModel {
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
StreamInfo info; StreamInfo info;
if(ignoreCache) { if(ignoreCache) {
info = Chaturbate.INSTANCE.loadStreamInfo(getName()); info = site.loadStreamInfo(getName());
LOG.trace("Model {} room status: {}", getName(), info.room_status); LOG.trace("Model {} room status: {}", getName(), info.room_status);
} else { } else {
info = Chaturbate.INSTANCE.getStreamInfo(getName()); info = site.getStreamInfo(getName());
} }
return Objects.equals("public", info.room_status); return Objects.equals("public", info.room_status);
} }
@Override @Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException { public int[] getStreamResolution(boolean failFast) throws ExecutionException {
int[] resolution = Chaturbate.INSTANCE.streamResolutionCache.getIfPresent(getName()); int[] resolution = site.streamResolutionCache.getIfPresent(getName());
if(resolution != null) { if(resolution != null) {
return Chaturbate.INSTANCE.getResolution(getName()); return site.getResolution(getName());
} else { } else {
if(failFast) { if(failFast) {
return new int[2]; return new int[2];
} else { } else {
return Chaturbate.INSTANCE.getResolution(getName()); return site.getResolution(getName());
} }
} }
} }
@ -75,8 +68,8 @@ public class ChaturbateModel extends AbstractModel {
*/ */
@Override @Override
public void invalidateCacheEntries() { public void invalidateCacheEntries() {
Chaturbate.INSTANCE.streamInfoCache.invalidate(getName()); site.streamInfoCache.invalidate(getName());
Chaturbate.INSTANCE.streamResolutionCache.invalidate(getName()); site.streamResolutionCache.invalidate(getName());
} }
public String getOnlineState() throws IOException, ExecutionException { public String getOnlineState() throws IOException, ExecutionException {
@ -85,20 +78,20 @@ public class ChaturbateModel extends AbstractModel {
@Override @Override
public String getOnlineState(boolean failFast) throws IOException, ExecutionException { public String getOnlineState(boolean failFast) throws IOException, ExecutionException {
StreamInfo info = Chaturbate.INSTANCE.streamInfoCache.getIfPresent(getName()); StreamInfo info = site.streamInfoCache.getIfPresent(getName());
return info != null ? info.room_status : "n/a"; return info != null ? info.room_status : "n/a";
} }
public StreamInfo getStreamInfo() throws IOException, ExecutionException { public StreamInfo getStreamInfo() throws IOException, ExecutionException {
return Chaturbate.INSTANCE.getStreamInfo(getName()); return site.getStreamInfo(getName());
} }
public MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException, ExecutionException { public MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException, ExecutionException {
return Chaturbate.INSTANCE.getMasterPlaylist(getName()); return site.getMasterPlaylist(getName());
} }
@Override @Override
public void receiveTip(int tokens) throws IOException { public void receiveTip(int tokens) throws IOException {
Chaturbate.INSTANCE.sendTip(getName(), tokens); site.sendTip(getName(), tokens);
} }
@Override @Override
@ -123,167 +116,52 @@ public class ChaturbateModel extends AbstractModel {
return sources; return sources;
} }
private static class Chaturbate {
private static final transient Logger LOG = LoggerFactory.getLogger(Chaturbate.class);
public static final Chaturbate INSTANCE = new Chaturbate(HttpClient.getInstance());
private HttpClient client;
private static long lastRequest = System.currentTimeMillis();
private LoadingCache<String, StreamInfo> streamInfoCache = CacheBuilder.newBuilder()
.initialCapacity(10_000)
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(new CacheLoader<String, StreamInfo> () {
@Override @Override
public StreamInfo load(String model) throws Exception { public boolean follow() throws IOException {
return loadStreamInfo(model); return follow(true);
} }
});
private LoadingCache<String, int[]> streamResolutionCache = CacheBuilder.newBuilder()
.initialCapacity(10_000)
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(new CacheLoader<String, int[]> () {
@Override @Override
public int[] load(String model) throws Exception { public boolean unfollow() throws IOException {
return loadResolution(model); return follow(false);
}
});
public Chaturbate(HttpClient client) {
this.client = client;
} }
public void sendTip(String name, int tokens) throws IOException { private boolean follow(boolean follow) throws IOException {
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { Request req = new Request.Builder().url(getUrl()).build();
RequestBody body = new FormBody.Builder() Response resp = site.getHttpClient().execute(req);
.add("csrfmiddlewaretoken", client.getToken()) resp.close();
.add("tip_amount", Integer.toString(tokens))
.add("tip_room_type", "public")
.build();
Request req = new Request.Builder()
.url("https://chaturbate.com/tipping/send_tip/"+name+"/")
.post(body)
.addHeader("Referer", "https://chaturbate.com/"+name+"/")
.addHeader("X-Requested-With", "XMLHttpRequest")
.build();
try(Response response = client.execute(req, true)) {
if(!response.isSuccessful()) {
throw new IOException(response.code() + " " + response.message());
}
}
}
}
private StreamInfo getStreamInfo(String modelName) throws IOException, ExecutionException { String url = null;
return streamInfoCache.get(modelName); if(follow) {
} url = BASE_URI + "/follow/follow/" + getName() + "/";
private StreamInfo loadStreamInfo(String modelName) throws IOException, InterruptedException {
throttleRequests();
RequestBody body = new FormBody.Builder()
.add("room_slug", modelName)
.add("bandwidth", "high")
.build();
Request req = new Request.Builder()
.url("https://chaturbate.com/get_edge_hls_url_ajax/")
.post(body)
.addHeader("X-Requested-With", "XMLHttpRequest")
.build();
Response response = client.execute(req);
try {
if(response.isSuccessful()) {
String content = response.body().string();
LOG.trace("Raw stream info: {}", content);
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<StreamInfo> adapter = moshi.adapter(StreamInfo.class);
StreamInfo streamInfo = adapter.fromJson(content);
streamInfoCache.put(modelName, streamInfo);
return streamInfo;
} else { } else {
int code = response.code(); url = BASE_URI + "/follow/unfollow/" + getName() + "/";
String message = response.message();
throw new IOException("Server responded with " + code + " - " + message + " headers: [" + response.headers() + "]");
}
} finally {
response.close();
}
} }
public int[] getResolution(String modelName) throws ExecutionException { RequestBody body = RequestBody.create(null, new byte[0]);
return streamResolutionCache.get(modelName); req = new Request.Builder()
} .url(url)
.method("POST", body)
private int[] loadResolution(String modelName) throws IOException, ParseException, PlaylistException, ExecutionException, InterruptedException { .header("Accept", "*/*")
int[] res = new int[2]; .header("Accept-Language", "en-US,en;q=0.5")
StreamInfo streamInfo = getStreamInfo(modelName); .header("Referer", getUrl())
if(!streamInfo.url.startsWith("http")) { .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0")
return res; .header("X-CSRFToken", ((ChaturbateHttpClient)site.getHttpClient()).getToken())
} .header("X-Requested-With", "XMLHttpRequest")
.build();
EOFException ex = null; resp = site.getHttpClient().execute(req, true);
for(int i=0; i<2; i++) { if(resp.isSuccessful()) {
try { String msg = resp.body().string();
MasterPlaylist master = getMasterPlaylist(modelName); if(!msg.equalsIgnoreCase("ok")) {
for (PlaylistData playlistData : master.getPlaylists()) { LOG.debug(msg);
if(playlistData.hasStreamInfo() && playlistData.getStreamInfo().hasResolution()) { throw new IOException("Response was " + msg.substring(0, Math.min(msg.length(), 500)));
int h = playlistData.getStreamInfo().getResolution().height; } else {
int w = playlistData.getStreamInfo().getResolution().width; LOG.debug("Follow/Unfollow -> {}", msg);
if(w > res[1]) { return true;
res[0] = w;
res[1] = h;
}
}
}
ex = null;
break; // this attempt worked, exit loop
} catch(EOFException e) {
// the cause might be, that the playlist url in streaminfo is outdated,
// so let's remove it from cache and retry in the next iteration
streamInfoCache.invalidate(modelName);
ex = e;
}
}
if(ex != null) {
throw ex;
}
streamResolutionCache.put(modelName, res);
return res;
}
private void throttleRequests() throws InterruptedException {
long now = System.currentTimeMillis();
long diff = now - lastRequest;
if(diff < 500) {
Thread.sleep(diff);
}
lastRequest = now;
}
public MasterPlaylist getMasterPlaylist(String modelName) throws IOException, ParseException, PlaylistException, ExecutionException {
StreamInfo streamInfo = getStreamInfo(modelName);
return getMasterPlaylist(streamInfo);
}
public MasterPlaylist getMasterPlaylist(StreamInfo streamInfo) throws IOException, ParseException, PlaylistException {
LOG.trace("Loading master playlist {}", streamInfo.url);
Request req = new Request.Builder().url(streamInfo.url).build();
Response response = client.execute(req);
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();
} }
} else {
resp.close();
throw new IOException("HTTP status " + resp.code() + " " + resp.message());
} }
} }
} }

View File

@ -1,7 +1,5 @@
package ctbrec.sites.chaturbate; package ctbrec.sites.chaturbate;
import static ctbrec.ui.CamrecApplication.BASE_URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -16,16 +14,15 @@ import ctbrec.ui.HtmlParser;
public class ChaturbateModelParser { public class ChaturbateModelParser {
private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateModelParser.class); private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateModelParser.class);
public static List<Model> parseModels(String html) { public static List<Model> parseModels(Chaturbate chaturbate, String html) {
List<Model> models = new ArrayList<>(); List<Model> models = new ArrayList<>();
Elements cells = HtmlParser.getTags(html, "ul.list > li"); Elements cells = HtmlParser.getTags(html, "ul.list > li");
for (Element cell : cells) { for (Element cell : cells) {
String cellHtml = cell.html(); String cellHtml = cell.html();
try { try {
Model model = new ChaturbateModel(); Model model = chaturbate.createModel(HtmlParser.getText(cellHtml, "div.title > a").trim());
model.setName(HtmlParser.getText(cellHtml, "div.title > a").trim()); model.setName(HtmlParser.getText(cellHtml, "div.title > a").trim());
model.setPreview(HtmlParser.getTag(cellHtml, "a img").attr("src")); model.setPreview(HtmlParser.getTag(cellHtml, "a img").attr("src"));
model.setUrl(BASE_URI + HtmlParser.getTag(cellHtml, "a").attr("href"));
model.setDescription(HtmlParser.getText(cellHtml, "div.details ul.subject")); model.setDescription(HtmlParser.getText(cellHtml, "div.details ul.subject"));
Elements tags = HtmlParser.getTags(cellHtml, "div.details ul.subject li a"); Elements tags = HtmlParser.getTags(cellHtml, "div.details ul.subject li a");
if(tags != null) { if(tags != null) {

View File

@ -29,7 +29,7 @@ public class ChaturbateTabProvider extends TabProvider {
tabs.add(createTab("Male", BASE_URI + "/male-cams/")); tabs.add(createTab("Male", BASE_URI + "/male-cams/"));
tabs.add(createTab("Couples", BASE_URI + "/couple-cams/")); tabs.add(createTab("Couples", BASE_URI + "/couple-cams/"));
tabs.add(createTab("Trans", BASE_URI + "/trans-cams/")); tabs.add(createTab("Trans", BASE_URI + "/trans-cams/"));
FollowedTab followedTab = new FollowedTab("Followed", BASE_URI + "/followed-cams/"); FollowedTab followedTab = new FollowedTab("Followed", BASE_URI + "/followed-cams/", chaturbate);
followedTab.setRecorder(recorder); followedTab.setRecorder(recorder);
followedTab.setScene(scene); followedTab.setScene(scene);
tabs.add(followedTab); tabs.add(followedTab);
@ -37,8 +37,8 @@ public class ChaturbateTabProvider extends TabProvider {
} }
private Tab createTab(String title, String url) { private Tab createTab(String title, String url) {
ChaturbateUpdateService updateService = new ChaturbateUpdateService(url, false); ChaturbateUpdateService updateService = new ChaturbateUpdateService(url, false, chaturbate);
ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService); ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, chaturbate);
tab.setRecorder(recorder); tab.setRecorder(recorder);
return tab; return tab;
} }

View File

@ -10,7 +10,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.io.HttpClient;
import ctbrec.ui.PaginatedScheduledService; import ctbrec.ui.PaginatedScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import okhttp3.Request; import okhttp3.Request;
@ -21,10 +20,12 @@ public class ChaturbateUpdateService extends PaginatedScheduledService {
private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateUpdateService.class); private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateUpdateService.class);
private String url; private String url;
private boolean loginRequired; private boolean loginRequired;
private Chaturbate chaturbate;
public ChaturbateUpdateService(String url, boolean loginRequired) { public ChaturbateUpdateService(String url, boolean loginRequired, Chaturbate chaturbate) {
this.url = url; this.url = url;
this.loginRequired = loginRequired; this.loginRequired = loginRequired;
this.chaturbate = chaturbate;
ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() { ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override @Override
@ -46,9 +47,9 @@ public class ChaturbateUpdateService extends PaginatedScheduledService {
String url = ChaturbateUpdateService.this.url + "?page="+page+"&keywords=&_=" + System.currentTimeMillis(); String url = ChaturbateUpdateService.this.url + "?page="+page+"&keywords=&_=" + System.currentTimeMillis();
LOG.debug("Fetching page {}", url); LOG.debug("Fetching page {}", url);
Request request = new Request.Builder().url(url).build(); Request request = new Request.Builder().url(url).build();
Response response = HttpClient.getInstance().execute(request, loginRequired); Response response = chaturbate.getHttpClient().execute(request, loginRequired);
if (response.isSuccessful()) { if (response.isSuccessful()) {
List<Model> models = ChaturbateModelParser.parseModels(response.body().string()); List<Model> models = ChaturbateModelParser.parseModels(chaturbate, response.body().string());
response.close(); response.close();
return models; return models;
} else { } else {

View File

@ -37,7 +37,7 @@ public class FriendsUpdateService extends PaginatedScheduledService {
.url(url) .url(url)
.header("Referer", myFreeCams.getBaseUrl()) .header("Referer", myFreeCams.getBaseUrl())
.build(); .build();
Response resp = MyFreeCams.httpClient.newCall(req).execute(); Response resp = myFreeCams.getHttpClient().execute(req, true);
if(resp.isSuccessful()) { if(resp.isSuccessful()) {
String json = resp.body().string().substring(4); String json = resp.body().string().substring(4);
JSONObject object = new JSONObject(json); JSONObject object = new JSONObject(json);

View File

@ -1,35 +1,18 @@
package ctbrec.sites.mfc; package ctbrec.sites.mfc;
import java.io.IOException; 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.recorder.Recorder;
import ctbrec.ui.CookieJarImpl; import ctbrec.sites.Site;
import ctbrec.ui.TabProvider; 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 { public class MyFreeCams implements Site {
private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCams.class); public static final String BASE_URI = "https://www.myfreecams.com";
private Recorder recorder; private Recorder recorder;
private MyFreeCamsClient client; private MyFreeCamsClient client;
public static OkHttpClient httpClient = new OkHttpClient.Builder() private MyFreeCamsHttpClient httpClient = new MyFreeCamsHttpClient();
.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 { public MyFreeCams() throws IOException {
client = MyFreeCamsClient.getInstance(); client = MyFreeCamsClient.getInstance();
@ -39,25 +22,9 @@ public class MyFreeCams implements Site {
login(); login();
} }
@Override
public void login() throws IOException { 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 @Override
@ -67,7 +34,7 @@ public class MyFreeCams implements Site {
@Override @Override
public String getBaseUrl() { public String getBaseUrl() {
return "https://www.myfreecams.com"; return BASE_URI;
} }
@Override @Override
@ -87,9 +54,29 @@ public class MyFreeCams implements Site {
@Override @Override
public MyFreeCamsModel createModel(String name) { public MyFreeCamsModel createModel(String name) {
MyFreeCamsModel model = new MyFreeCamsModel(); MyFreeCamsModel model = new MyFreeCamsModel(this);
model.setName(name); model.setName(name);
model.setUrl("https://profiles.myfreecams.com/" + name); model.setUrl("https://profiles.myfreecams.com/" + name);
return model; return model;
} }
@Override
public Integer getTokenBalance() throws IOException {
throw new RuntimeException("Not implemented for MFC");
}
@Override
public String getBuyTokensLink() {
return "https://www.myfreecams.com/php/purchase.php?request=tokens";
}
@Override
public MyFreeCamsHttpClient getHttpClient() {
return httpClient;
}
@Override
public void shutdown() {
httpClient.shutdown();
}
} }

View File

@ -62,7 +62,7 @@ public class MyFreeCamsClient {
public void start() throws IOException { public void start() throws IOException {
running = true; running = true;
ServerConfig serverConfig = new ServerConfig(MyFreeCams.httpClient); ServerConfig 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";
@ -89,7 +89,7 @@ public class MyFreeCamsClient {
} }
private WebSocket createWebSocket(Request req) { private WebSocket createWebSocket(Request req) {
WebSocket ws = MyFreeCams.httpClient.newWebSocket(req, new WebSocketListener() { WebSocket ws = mfc.getHttpClient().newWebSocket(req, new WebSocketListener() {
@Override @Override
public void onOpen(WebSocket webSocket, Response response) { public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response); super.onOpen(webSocket, response);
@ -111,7 +111,7 @@ public class MyFreeCamsClient {
super.onClosed(webSocket, code, reason); super.onClosed(webSocket, code, reason);
LOG.trace("close: {} {}", code, reason); LOG.trace("close: {} {}", code, reason);
running = false; running = false;
MyFreeCams.httpClient.dispatcher().executorService().shutdownNow(); mfc.getHttpClient().shutdown();
} }
private StringBuilder msgBuffer = new StringBuilder(); private StringBuilder msgBuffer = new StringBuilder();
@ -190,7 +190,7 @@ public class MyFreeCamsClient {
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.debug("Requesting EXTDATA {}", url);
Response resp = MyFreeCams.httpClient.newCall(req).execute(); Response resp = mfc.getHttpClient().execute(req);
if(resp.isSuccessful()) { if(resp.isSuccessful()) {
parseExtDataSessionStates(resp.body().string()); parseExtDataSessionStates(resp.body().string());
@ -354,4 +354,14 @@ public class MyFreeCamsClient {
public void execute(Runnable r) { public void execute(Runnable r) {
executor.execute(r); executor.execute(r);
} }
public void getSessionState(ctbrec.Model model) {
for (SessionState state : sessionStates.values()) {
if(Objects.equals(state.getNm(), model.getName())) {
JsonAdapter<SessionState> adapter = moshi.adapter(SessionState.class).indent(" ");
System.out.println(adapter.toJson(state));
System.out.println("#####################");
}
}
}
} }

View File

@ -0,0 +1,52 @@
package ctbrec.sites.mfc;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
public class MyFreeCamsHttpClient extends HttpClient {
private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsHttpClient.class);
@Override
public boolean login() throws IOException {
String username = Config.getInstance().getSettings().username;
String password = Config.getInstance().getSettings().password;
RequestBody body = new FormBody.Builder()
.add("username", username)
.add("password", password)
.add("tz", "2")
.add("ss", "1920x1080")
.add("submit_login", "97")
.build();
Request req = new Request.Builder()
.url(MyFreeCams.BASE_URI + "/php/login.php")
.header("Referer", MyFreeCams.BASE_URI)
.header("Content-Type", "application/x-www-form-urlencoded")
.post(body)
.build();
Response resp = execute(req);
if(resp.isSuccessful()) {
resp.close();
return true;
} else {
resp.close();
LOG.error("Login failed {} {}", resp.code(), resp.message());
return false;
}
}
public WebSocket newWebSocket(Request req, WebSocketListener webSocketListener) {
return client.newWebSocket(req, webSocketListener);
}
}

View File

@ -32,6 +32,11 @@ public class MyFreeCamsModel extends AbstractModel {
private double camScore; private double camScore;
private State state; private State state;
private int resolution[]; private int resolution[];
private MyFreeCams site;
MyFreeCamsModel(MyFreeCams site) {
this.site = site;
}
@Override @Override
public boolean isOnline() throws IOException, ExecutionException, InterruptedException { public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
@ -78,7 +83,7 @@ public class MyFreeCamsModel extends AbstractModel {
} }
LOG.debug("Loading master playlist {}", hlsUrl); LOG.debug("Loading master playlist {}", hlsUrl);
Request req = new Request.Builder().url(hlsUrl).build(); Request req = new Request.Builder().url(hlsUrl).build();
Response response = MyFreeCams.httpClient.newCall(req).execute(); Response response = site.getHttpClient().execute(req);
try { try {
InputStream inputStream = response.body().byteStream(); InputStream inputStream = response.body().byteStream();
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
@ -97,7 +102,7 @@ public class MyFreeCamsModel extends AbstractModel {
@Override @Override
public void receiveTip(int tokens) throws IOException { public void receiveTip(int tokens) throws IOException {
throw new RuntimeException("Not implemented"); throw new RuntimeException("Not implemented for MFC");
} }
@Override @Override
@ -155,8 +160,29 @@ public class MyFreeCamsModel extends AbstractModel {
// stream url // stream url
Integer camserv = state.getU().getCamserv(); Integer camserv = state.getU().getCamserv();
if(camserv != null) { 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"; String hlsUrl = "http://video" + (camserv - 500) + ".myfreecams.com:1935/NxServer/ngrp:mfc_" + (100000000 + state.getUid()) + ".f4v_mobile/playlist.m3u8";
setStreamUrl(hlsUrl); setStreamUrl(hlsUrl);
} }
} }
@Override
public boolean follow() {
return false;
}
@Override
public boolean unfollow() {
return false;
}
} }

View File

@ -26,18 +26,17 @@ public class MyFreeCamsTabProvider extends TabProvider {
List<Tab> tabs = new ArrayList<>(); List<Tab> tabs = new ArrayList<>();
PaginatedScheduledService updateService = new OnlineCamsUpdateService(); PaginatedScheduledService updateService = new OnlineCamsUpdateService();
ThumbOverviewTab online = new ThumbOverviewTab("Online", updateService); ThumbOverviewTab online = new ThumbOverviewTab("Online", updateService, myFreeCams);
online.setRecorder(recorder); online.setRecorder(recorder);
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10)));
tabs.add(online); tabs.add(online);
updateService = new FriendsUpdateService(myFreeCams); updateService = new FriendsUpdateService(myFreeCams);
ThumbOverviewTab friends = new ThumbOverviewTab("Friends", updateService); ThumbOverviewTab friends = new ThumbOverviewTab("Friends", updateService, myFreeCams);
friends.setRecorder(recorder); friends.setRecorder(recorder);
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10)));
tabs.add(friends); tabs.add(friends);
return tabs; return tabs;
} }
} }

View File

@ -1,12 +1,13 @@
package ctbrec.sites.mfc; package ctbrec.sites.mfc;
import ctbrec.sites.Site;
import ctbrec.ui.PaginatedScheduledService; import ctbrec.ui.PaginatedScheduledService;
import ctbrec.ui.ThumbOverviewTab; import ctbrec.ui.ThumbOverviewTab;
public class OnlineModelsTab extends ThumbOverviewTab { public class OnlineModelsTab extends ThumbOverviewTab {
public OnlineModelsTab(String title, PaginatedScheduledService updateService) { public OnlineModelsTab(String title, PaginatedScheduledService updateService, Site site) {
super(title, updateService); super(title, updateService, site);
} }
} }

View File

@ -9,7 +9,7 @@ import java.util.Map;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import okhttp3.OkHttpClient; import ctbrec.io.HttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
@ -23,9 +23,9 @@ public class ServerConfig {
Map<String, String> wzobsServers; Map<String, String> wzobsServers;
Map<String, String> ngVideo; Map<String, String> ngVideo;
public ServerConfig(OkHttpClient 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();
Response resp = client.newCall(req).execute(); Response resp = client.execute(req);
String json = resp.body().string(); String json = resp.body().string();
JSONObject serverConfig = new JSONObject(json); JSONObject serverConfig = new JSONObject(json);

View File

@ -7,7 +7,6 @@ import java.io.InputStreamReader;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -20,26 +19,31 @@ import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types; import com.squareup.moshi.Types;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Site;
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;
import ctbrec.sites.Site;
import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.mfc.MyFreeCams;
import javafx.application.Application; import javafx.application.Application;
import javafx.application.HostServices; import javafx.application.HostServices;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue; 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.Scene;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import javafx.scene.control.TabPane; import javafx.scene.control.TabPane;
import javafx.scene.control.TabPane.TabClosingPolicy; import javafx.scene.control.TabPane.TabClosingPolicy;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Font;
import javafx.stage.Stage; import javafx.stage.Stage;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
@ -47,12 +51,9 @@ import okhttp3.Response;
public class CamrecApplication extends Application { public class CamrecApplication extends Application {
static final transient Logger LOG = LoggerFactory.getLogger(CamrecApplication.class); static final transient Logger LOG = LoggerFactory.getLogger(CamrecApplication.class);
public static final String BASE_URI = "https://chaturbate.com";
public static final String AFFILIATE_LINK = BASE_URI + "/in/?track=default&tour=LQps&campaign=55vTi&room=0xb00bface";
private Config config; private Config config;
private Recorder recorder; private Recorder recorder;
private HttpClient client;
static HostServices hostServices; static HostServices hostServices;
private SettingsTab settingsTab; private SettingsTab settingsTab;
private TabPane tabPane = new TabPane(); private TabPane tabPane = new TabPane();
@ -65,12 +66,13 @@ public class CamrecApplication extends Application {
loadConfig(); loadConfig();
bus = new AsyncEventBus(Executors.newSingleThreadExecutor()); bus = new AsyncEventBus(Executors.newSingleThreadExecutor());
hostServices = getHostServices(); hostServices = getHostServices();
client = HttpClient.getInstance();
createRecorder(); createRecorder();
//site = new Chaturbate(); // site = new Chaturbate();
site = new MyFreeCams(); site = new MyFreeCams();
site.setRecorder(recorder); site.setRecorder(recorder);
// TODO move this to Chaturbate class doInitialLogin(); if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
site.login();
}
createGui(primaryStage); createGui(primaryStage);
checkForUpdates(); checkForUpdates();
} }
@ -132,7 +134,7 @@ public class CamrecApplication extends Application {
public void run() { public void run() {
settingsTab.saveConfig(); settingsTab.saveConfig();
recorder.shutdown(); recorder.shutdown();
client.shutdown(); site.shutdown();
try { try {
Config.getInstance().save(); Config.getInstance().save();
LOG.info("Shutdown complete. Goodbye!"); LOG.info("Shutdown complete. Goodbye!");
@ -152,103 +154,43 @@ public class CamrecApplication extends Application {
}.start(); }.start();
}); });
// 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) {
Task<Integer> task = new Task<Integer>() {
@Override
protected Integer call() throws Exception {
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
String username = Config.getInstance().getSettings().username; String username = Config.getInstance().getSettings().username;
if (username == null || username.trim().isEmpty()) { if(username != null && !username.trim().isEmpty()) {
throw new IOException("Not logged in"); double fontSize = tabPane.getTabMaxHeight() / 2 - 1;
} Button buyTokens = new Button("Buy Tokens");
buyTokens.setFont(Font.font(fontSize));
String url = "https://chaturbate.com/p/" + username + "/"; buyTokens.setOnAction((e) -> DesktopIntergation.open(site.getBuyTokensLink()));
HttpClient client = HttpClient.getInstance(); buyTokens.setMaxHeight(tabPane.getTabMaxHeight());
Request req = new Request.Builder().url(url).build(); TokenLabel tokenBalance = new TokenLabel(site);
Response resp = client.execute(req, true); tokenPanel = new HBox(5, tokenBalance, buyTokens);
if (resp.isSuccessful()) { tokenPanel.setAlignment(Pos.BASELINE_RIGHT);
String profilePage = resp.body().string(); tokenPanel.setMaxHeight(tabPane.getTabMaxHeight());
String tokenText = HtmlParser.getText(profilePage, "span.tokencount"); tokenPanel.setMaxWidth(200);
int tokens = Integer.parseInt(tokenText); tokenBalance.setFont(Font.font(fontSize));
return tokens; HBox.setMargin(tokenBalance, new Insets(0, 5, 0, 0));
} else { HBox.setMargin(buyTokens, new Insets(0, 5, 0, 0));
throw new IOException("HTTP response: " + resp.code() + " - " + resp.message()); for (Node node : tabPane.getChildrenUnmodifiable()) {
} if(node.getStyleClass().contains("tab-header-area")) {
} else { Parent header = (Parent) node;
return 1_000_000; 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);
} }
} }
@Override
protected void done() {
try {
int tokens = get();
label.update(tokens);
} catch (InterruptedException | ExecutionException e) {
LOG.error("Couldn't retrieve account balance", e);
Platform.runLater(() -> label.setText("Tokens: error"));
} }
} }
}; tokenBalance.loadBalance();
new Thread(task).start();
}
private void doInitialLogin() {
if(config.getSettings().username != null && !config.getSettings().username.isEmpty()) {
new Thread() {
@Override
public void run() {
if(!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
try {
client.login();
} catch (IOException e1) {
LOG.warn("Initial login failed" , e1);
}
}
};
}.start();
} }
} }
private void createRecorder() { private void createRecorder() {
if(config.getSettings().localRecording) { if (config.getSettings().localRecording) {
recorder = new LocalRecorder(config); recorder = new LocalRecorder(config);
} else { } else {
recorder = new RemoteRecorder(config, client); recorder = new RemoteRecorder(config, site.getHttpClient());
} }
} }
@ -275,8 +217,8 @@ 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 = client.execute(request); Response response = site.getHttpClient().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);
JsonAdapter<List<Release>> adapter = moshi.adapter(type); JsonAdapter<List<Release>> adapter = moshi.adapter(type);
@ -284,7 +226,7 @@ public class CamrecApplication extends Application {
Release latest = releases.get(0); Release latest = releases.get(0);
Version latestVersion = latest.getVersion(); Version latestVersion = latest.getVersion();
Version ctbrecVersion = getVersion(); Version ctbrecVersion = getVersion();
if(latestVersion.compareTo(ctbrecVersion) > 0) { if (latestVersion.compareTo(ctbrecVersion) > 0) {
LOG.debug("Update available {} < {}", ctbrecVersion, latestVersion); LOG.debug("Update available {} < {}", ctbrecVersion, latestVersion);
Platform.runLater(() -> tabPane.getTabs().add(new UpdateTab(latest))); Platform.runLater(() -> tabPane.getTabs().add(new UpdateTab(latest)));
} else { } else {
@ -303,10 +245,10 @@ public class CamrecApplication extends Application {
} }
private Version getVersion() throws IOException { private Version getVersion() throws IOException {
if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) { if (Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
return Version.of("0.0.0-DEV"); return Version.of("0.0.0-DEV");
} else { } else {
try(InputStream is = getClass().getClassLoader().getResourceAsStream("version")) { try (InputStream is = getClass().getClassLoader().getResourceAsStream("version")) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is)); BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String versionString = reader.readLine(); String versionString = reader.readLine();
Version version = Version.of(versionString); Version version = Version.of(versionString);

View File

@ -2,6 +2,7 @@ package ctbrec.ui;
import ctbrec.sites.chaturbate.Chaturbate;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@ -44,7 +45,7 @@ public class DonateTabFx extends Tab {
ImageView tokenImage = new ImageView(getClass().getResource("/html/token.png").toString()); ImageView tokenImage = new ImageView(getClass().getResource("/html/token.png").toString());
Button tokenButton = new Button("Buy tokens"); Button tokenButton = new Button("Buy tokens");
tokenButton.setOnAction((e) -> { DesktopIntergation.open(CamrecApplication.AFFILIATE_LINK); }); tokenButton.setOnAction((e) -> { DesktopIntergation.open(Chaturbate.AFFILIATE_LINK); });
VBox tokenBox = new VBox(5); VBox tokenBox = new VBox(5);
tokenBox.setAlignment(Pos.TOP_CENTER); tokenBox.setAlignment(Pos.TOP_CENTER);
Label tokenDesc = new Label("If you buy tokens by using this button,\n" Label tokenDesc = new Label("If you buy tokens by using this button,\n"

View File

@ -1,5 +1,6 @@
package ctbrec.ui; package ctbrec.ui;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.chaturbate.ChaturbateUpdateService; import ctbrec.sites.chaturbate.ChaturbateUpdateService;
import javafx.concurrent.WorkerStateEvent; import javafx.concurrent.WorkerStateEvent;
import javafx.geometry.Insets; import javafx.geometry.Insets;
@ -16,8 +17,8 @@ public class FollowedTab extends ThumbOverviewTab {
private String onlineUrl; private String onlineUrl;
private String offlineUrl; private String offlineUrl;
public FollowedTab(String title, String url) { public FollowedTab(String title, String url, Chaturbate chaturbate) {
super(title, new ChaturbateUpdateService(url, true)); super(title, new ChaturbateUpdateService(url, true, chaturbate), chaturbate);
onlineUrl = url; onlineUrl = url;
offlineUrl = url + "offline/"; offlineUrl = url + "offline/";

View File

@ -126,4 +126,14 @@ public class JavaFxModel extends AbstractModel {
public int[] getStreamResolution(boolean b) throws ExecutionException { public int[] getStreamResolution(boolean b) throws ExecutionException {
return delegate.getStreamResolution(b); return delegate.getStreamResolution(b);
} }
@Override
public boolean follow() throws IOException {
return delegate.follow();
}
@Override
public boolean unfollow() throws IOException {
return delegate.unfollow();
}
} }

View File

@ -19,9 +19,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Site;
import ctbrec.io.HttpClient;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@ -266,7 +265,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
return; return;
} }
HttpClient client = HttpClient.getInstance();
Function<Model, Void> onSuccess = (m) -> { Function<Model, Void> onSuccess = (m) -> {
try { try {
recorder.switchStreamSource(m); recorder.switchStreamSource(m);
@ -281,7 +279,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
showStreamSwitchErrorDialog(t); showStreamSwitchErrorDialog(t);
return null; return null;
}; };
StreamSourceSelectionDialog.show(fxModel.getDelegate(), client, onSuccess, onFail); StreamSourceSelectionDialog.show(fxModel.getDelegate(), site.getHttpClient(), onSuccess, onFail);
} }
private void showStreamSwitchErrorDialog(Throwable throwable) { private void showStreamSwitchErrorDialog(Throwable throwable) {

View File

@ -1,7 +1,6 @@
package ctbrec.ui; package ctbrec.ui;
import static javafx.scene.control.ButtonType.NO; import static javafx.scene.control.ButtonType.*;
import static javafx.scene.control.ButtonType.YES;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@ -32,10 +31,9 @@ import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.Recording.STATUS; import ctbrec.Recording.STATUS;
import ctbrec.Site;
import ctbrec.io.HttpClient;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.recorder.download.MergedHlsDownload; import ctbrec.recorder.download.MergedHlsDownload;
import ctbrec.sites.Site;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@ -306,7 +304,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
@Override @Override
public void run() { public void run() {
try { try {
MergedHlsDownload download = new MergedHlsDownload(HttpClient.getInstance()); MergedHlsDownload download = new MergedHlsDownload(site.getHttpClient());
download.start(url.toString(), target, (progress) -> { download.start(url.toString(), target, (progress) -> {
Platform.runLater(() -> { Platform.runLater(() -> {
if (progress == 100) { if (progress == 100) {

View File

@ -13,6 +13,7 @@ import com.sun.javafx.collections.ObservableListWrapper;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Hmac; import ctbrec.Hmac;
import ctbrec.sites.chaturbate.Chaturbate;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets; import javafx.geometry.Insets;
@ -57,7 +58,6 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private CheckBox loadResolution; private CheckBox loadResolution;
private CheckBox secureCommunication = new CheckBox(); private CheckBox secureCommunication = new CheckBox();
private CheckBox chooseStreamQuality = new CheckBox(); private CheckBox chooseStreamQuality = new CheckBox();
private CheckBox autoRecordFollowed = new CheckBox();
private CheckBox multiplePlayers = new CheckBox(); private CheckBox multiplePlayers = new CheckBox();
private PasswordField password; private PasswordField password;
private RadioButton recordLocal; private RadioButton recordLocal;
@ -188,25 +188,9 @@ public class SettingsTab extends Tab implements TabSelectionListener {
layout.add(password, 1, 1); layout.add(password, 1, 1);
Button createAccount = new Button("Create new Account"); Button createAccount = new Button("Create new Account");
createAccount.setOnAction((e) -> DesktopIntergation.open(CamrecApplication.AFFILIATE_LINK)); createAccount.setOnAction((e) -> DesktopIntergation.open(Chaturbate.AFFILIATE_LINK));
layout.add(createAccount, 1, 2); layout.add(createAccount, 1, 2);
GridPane.setColumnSpan(createAccount, 2); GridPane.setColumnSpan(createAccount, 2);
l = new Label("Record all followed models");
layout.add(l, 0, 3);
autoRecordFollowed = new CheckBox();
autoRecordFollowed.setSelected(Config.getInstance().getSettings().recordFollowed);
autoRecordFollowed.setOnAction((e) -> {
Config.getInstance().getSettings().recordFollowed = autoRecordFollowed.isSelected();
showRestartRequired();
});
layout.add(autoRecordFollowed, 1, 3);
Label warning = new Label("Don't do this, if you follow many models. You have been warned ;) !");
warning.setTextFill(Color.RED);
layout.add(warning, 2, 3);
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(warning, new Insets(3, 0, 0, 0));
GridPane.setMargin(autoRecordFollowed, new Insets(3, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(username, new Insets(0, 0, 0, CHECKBOX_MARGIN)); GridPane.setMargin(username, new Insets(0, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(password, new Insets(0, 0, 0, CHECKBOX_MARGIN)); GridPane.setMargin(password, new Insets(0, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(createAccount, new Insets(0, 0, 0, CHECKBOX_MARGIN)); GridPane.setMargin(createAccount, new Insets(0, 0, 0, CHECKBOX_MARGIN));

View File

@ -45,9 +45,6 @@ import javafx.scene.text.Font;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment; import javafx.scene.text.TextAlignment;
import javafx.util.Duration; import javafx.util.Duration;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class ThumbCell extends StackPane { public class ThumbCell extends StackPane {
@ -347,9 +344,7 @@ public class ThumbCell extends StackPane {
} }
private void _startStopAction(Model model, boolean start) { private void _startStopAction(Model model, boolean start) {
new Thread() { new Thread(() -> {
@Override
public void run() {
try { try {
if(start) { if(start) {
recorder.startRecording(model); recorder.startRecording(model);
@ -370,54 +365,21 @@ public class ThumbCell extends StackPane {
} finally { } finally {
setCursor(Cursor.DEFAULT); setCursor(Cursor.DEFAULT);
} }
} }).start();
}.start();
} }
void follow(boolean follow) { void follow(boolean follow) {
setCursor(Cursor.WAIT); setCursor(Cursor.WAIT);
new Thread() { new Thread(() -> {
@Override
public void run() {
try { try {
Request req = new Request.Builder().url(model.getUrl()).build();
Response resp = HttpClient.getInstance().execute(req);
resp.close();
String url = null;
if(follow) { if(follow) {
url = CamrecApplication.BASE_URI + "/follow/follow/" + model.getName() + "/"; model.follow();
} else { } else {
url = CamrecApplication.BASE_URI + "/follow/unfollow/" + model.getName() + "/"; boolean unfollowed = model.unfollow();
} if(unfollowed) {
RequestBody body = RequestBody.create(null, new byte[0]);
req = new Request.Builder()
.url(url)
.method("POST", body)
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.5")
.header("Referer", model.getUrl())
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0")
.header("X-CSRFToken", HttpClient.getInstance().getToken())
.header("X-Requested-With", "XMLHttpRequest")
.build();
resp = HttpClient.getInstance().execute(req, true);
if(resp.isSuccessful()) {
String msg = resp.body().string();
if(!msg.equalsIgnoreCase("ok")) {
LOG.debug(msg);
throw new IOException("Response was " + msg.substring(0, Math.min(msg.length(), 500)));
} else {
LOG.debug("Follow/Unfollow -> {}", msg);
if(!follow) {
Platform.runLater(() -> thumbCellList.remove(ThumbCell.this)); Platform.runLater(() -> thumbCellList.remove(ThumbCell.this));
} }
} }
} else {
resp.close();
throw new IOException("HTTP status " + resp.code() + " " + resp.message());
}
} catch (Exception e1) { } catch (Exception e1) {
LOG.error("Couldn't follow/unfollow model {}", model.getName(), e1); LOG.error("Couldn't follow/unfollow model {}", model.getName(), e1);
Platform.runLater(() -> { Platform.runLater(() -> {
@ -430,8 +392,7 @@ public class ThumbCell extends StackPane {
} finally { } finally {
setCursor(Cursor.DEFAULT); setCursor(Cursor.DEFAULT);
} }
} }).start();
}.start();
} }
public Model getModel() { public Model getModel() {

View File

@ -29,6 +29,8 @@ import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.sites.mfc.MyFreeCamsClient;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.concurrent.Worker.State; import javafx.concurrent.Worker.State;
import javafx.concurrent.WorkerStateEvent; import javafx.concurrent.WorkerStateEvent;
@ -71,19 +73,20 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
ReentrantLock gridLock = new ReentrantLock(); ReentrantLock gridLock = new ReentrantLock();
ScrollPane scrollPane = new ScrollPane(); ScrollPane scrollPane = new ScrollPane();
boolean loginRequired; boolean loginRequired;
HttpClient client = HttpClient.getInstance();
HBox pagination; HBox pagination;
TextField pageInput = new TextField(Integer.toString(1)); TextField pageInput = new TextField(Integer.toString(1));
Button pagePrev = new Button(""); Button pagePrev = new Button("");
Button pageNext = new Button(""); Button pageNext = new Button("");
private volatile boolean updatesSuspended = false; private volatile boolean updatesSuspended = false;
ContextMenu popup; ContextMenu popup;
Site site;
private ComboBox<Integer> thumbWidth; private ComboBox<Integer> thumbWidth;
public ThumbOverviewTab(String title, PaginatedScheduledService updateService) { public ThumbOverviewTab(String title, PaginatedScheduledService updateService, Site site) {
super(title); super(title);
this.updateService = updateService; this.updateService = updateService;
this.site = site;
setClosable(false); setClosable(false);
createGui(); createGui();
initializeUpdateService(); initializeUpdateService();
@ -253,7 +256,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
} }
} }
if(!found) { if(!found) {
ThumbCell newCell = createThumbCell(this, model, recorder, client); ThumbCell newCell = createThumbCell(this, model, recorder, site.getHttpClient());
newCell.setIndex(index); newCell.setIndex(index);
positionChangedOrNew.add(newCell); positionChangedOrNew.add(newCell);
} }
@ -277,7 +280,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
} }
ThumbCell createThumbCell(ThumbOverviewTab thumbOverviewTab, Model model, Recorder recorder2, HttpClient client2) { ThumbCell createThumbCell(ThumbOverviewTab thumbOverviewTab, Model model, Recorder recorder2, HttpClient client2) {
ThumbCell newCell = new ThumbCell(this, model, recorder, client); ThumbCell newCell = new ThumbCell(this, model, recorder, site.getHttpClient());
newCell.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { newCell.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
suspendUpdates(true); suspendUpdates(true);
popup = createContextMenu(newCell); popup = createContextMenu(newCell);
@ -328,7 +331,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
MenuItem sendTip = new MenuItem("Send Tip"); MenuItem sendTip = new MenuItem("Send Tip");
sendTip.setOnAction((e) -> { sendTip.setOnAction((e) -> {
TipDialog tipDialog = new TipDialog(cell.getModel()); TipDialog tipDialog = new TipDialog(site, cell.getModel());
tipDialog.showAndWait(); tipDialog.showAndWait();
String tipText = tipDialog.getResult(); String tipText = tipDialog.getResult();
if(tipText != null) { if(tipText != null) {
@ -340,7 +343,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
event.put("event", "tokens.sent"); event.put("event", "tokens.sent");
event.put("amount", tokens); event.put("amount", tokens);
CamrecApplication.bus.post(event); CamrecApplication.bus.post(event);
} catch (IOException e1) { } catch (Exception e1) {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error"); alert.setTitle("Error");
alert.setHeaderText("Couldn't send tip"); alert.setHeaderText("Couldn't send tip");
@ -374,12 +377,17 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
} }
} }
MenuItem debug = new MenuItem("debug");
debug.setOnAction((e) -> {
MyFreeCamsClient.getInstance().getSessionState(cell.getModel());
});
ContextMenu contextMenu = new ContextMenu(); ContextMenu contextMenu = new ContextMenu();
contextMenu.setAutoHide(true); contextMenu.setAutoHide(true);
contextMenu.setHideOnEscape(true); contextMenu.setHideOnEscape(true);
contextMenu.setAutoFix(true); contextMenu.setAutoFix(true);
MenuItem followOrUnFollow = this instanceof FollowedTab ? unfollow : follow; MenuItem followOrUnFollow = this instanceof FollowedTab ? unfollow : follow;
contextMenu.getItems().addAll(openInPlayer, startStop , followOrUnFollow, copyUrl, sendTip); contextMenu.getItems().addAll(openInPlayer, startStop , followOrUnFollow, copyUrl, sendTip, debug);
return contextMenu; return contextMenu;
} }

View File

@ -1,30 +1,27 @@
package ctbrec.ui; package ctbrec.ui;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.io.HttpClient; import ctbrec.sites.Site;
import ctbrec.sites.chaturbate.Chaturbate;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
import javafx.scene.control.TextInputDialog; import javafx.scene.control.TextInputDialog;
import okhttp3.Request;
import okhttp3.Response;
public class TipDialog extends TextInputDialog { public class TipDialog extends TextInputDialog {
private static final transient Logger LOG = LoggerFactory.getLogger(TipDialog.class); private static final transient Logger LOG = LoggerFactory.getLogger(TipDialog.class);
private Site site;
public TipDialog(Model model) { public TipDialog(Site site, Model model) {
this.site = site;
setTitle("Send Tip"); setTitle("Send Tip");
loadTokenBalance(); loadTokenBalance();
setHeaderText("Loading token balance…"); setHeaderText("Loading token balance…");
@ -38,27 +35,7 @@ public class TipDialog extends TextInputDialog {
@Override @Override
protected Integer call() throws Exception { protected Integer call() throws Exception {
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
String username = Config.getInstance().getSettings().username; return site.getTokenBalance();
if (username == null || username.trim().isEmpty()) {
throw new IOException("Not logged in");
}
String url = "https://chaturbate.com/p/" + username + "/";
HttpClient client = HttpClient.getInstance();
Request req = new Request.Builder().url(url).build();
Response resp = client.execute(req, true);
if (resp.isSuccessful()) {
String profilePage = resp.body().string();
String tokenText = HtmlParser.getText(profilePage, "span.tokencount");
int tokens = Integer.parseInt(tokenText);
Map<String, Object> event = new HashMap<>();
event.put("event", "tokens");
event.put("amount", tokens);
CamrecApplication.bus.post(event);
return tokens;
} else {
throw new IOException("HTTP response: " + resp.code() + " - " + resp.message());
}
} else { } else {
return 1_000_000; return 1_000_000;
} }
@ -78,7 +55,7 @@ public class TipDialog extends TextInputDialog {
buyTokens.showAndWait(); buyTokens.showAndWait();
TipDialog.this.close(); TipDialog.this.close();
if(buyTokens.getResult() == ButtonType.YES) { if(buyTokens.getResult() == ButtonType.YES) {
DesktopIntergation.open(CamrecApplication.AFFILIATE_LINK); DesktopIntergation.open(Chaturbate.AFFILIATE_LINK);
} }
} else { } else {
getEditor().setDisable(false); getEditor().setDisable(false);

View File

@ -2,17 +2,26 @@ package ctbrec.ui;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import ctbrec.sites.Site;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.control.Label; import javafx.scene.control.Label;
public class TokenLabel extends Label { public class TokenLabel extends Label {
private static final transient Logger LOG = LoggerFactory.getLogger(TokenLabel.class);
private int tokens = -1; private int tokens = -1;
private Site site;
public TokenLabel() { public TokenLabel(Site site) {
this.site = site;
setText("Tokens: loading…"); setText("Tokens: loading…");
CamrecApplication.bus.register(new Object() { CamrecApplication.bus.register(new Object() {
@Subscribe @Subscribe
@ -42,4 +51,29 @@ public class TokenLabel extends Label {
private void updateText() { private void updateText() {
Platform.runLater(() -> setText("Tokens: " + tokens)); Platform.runLater(() -> setText("Tokens: " + tokens));
} }
public void loadBalance() {
Task<Integer> task = new Task<Integer>() {
@Override
protected Integer call() throws Exception {
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
return site.getTokenBalance();
} else {
return 1_000_000;
}
}
@Override
protected void done() {
try {
int tokens = get();
update(tokens);
} catch (InterruptedException | ExecutionException e) {
LOG.error("Couldn't retrieve account balance", e);
Platform.runLater(() -> setText("Tokens: error"));
}
}
};
new Thread(task).start();
}
} }