From 387661cfdfdc822f3524e2ca44fce8e4cab8ed38 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 21 Oct 2018 19:06:01 +0200 Subject: [PATCH] Abstract more stuff in the site packages --- src/main/java/ctbrec/Model.java | 3 +- src/main/java/ctbrec/Settings.java | 1 - src/main/java/ctbrec/io/HttpClient.java | 109 ++------ .../java/ctbrec/recorder/LocalRecorder.java | 85 +----- .../ctbrec/recorder/server/HlsServlet.java | 31 ++- .../recorder/server/RecorderHttpClient.java | 13 + src/main/java/ctbrec/{ => sites}/Site.java | 11 +- .../ctbrec/sites/chaturbate/Chaturbate.java | 245 +++++++++++++++++- .../chaturbate/ChaturbateHttpClient.java | 91 +++++++ .../sites/chaturbate/ChaturbateModel.java | 238 +++++------------ .../chaturbate/ChaturbateModelParser.java | 7 +- .../chaturbate/ChaturbateTabProvider.java | 6 +- .../chaturbate/ChaturbateUpdateService.java | 9 +- .../sites/mfc/FriendsUpdateService.java | 2 +- .../java/ctbrec/sites/mfc/MyFreeCams.java | 67 ++--- .../ctbrec/sites/mfc/MyFreeCamsClient.java | 18 +- .../sites/mfc/MyFreeCamsHttpClient.java | 52 ++++ .../ctbrec/sites/mfc/MyFreeCamsModel.java | 30 ++- .../sites/mfc/MyFreeCamsTabProvider.java | 5 +- .../ctbrec/sites/mfc/OnlineModelsTab.java | 5 +- .../java/ctbrec/sites/mfc/ServerConfig.java | 6 +- .../java/ctbrec/ui/CamrecApplication.java | 152 ++++------- src/main/java/ctbrec/ui/DonateTabFx.java | 3 +- src/main/java/ctbrec/ui/FollowedTab.java | 5 +- src/main/java/ctbrec/ui/JavaFxModel.java | 10 + .../java/ctbrec/ui/RecordedModelsTab.java | 6 +- src/main/java/ctbrec/ui/RecordingsTab.java | 8 +- src/main/java/ctbrec/ui/SettingsTab.java | 20 +- src/main/java/ctbrec/ui/ThumbCell.java | 119 +++------ src/main/java/ctbrec/ui/ThumbOverviewTab.java | 22 +- src/main/java/ctbrec/ui/TipDialog.java | 37 +-- src/main/java/ctbrec/ui/TokenLabel.java | 36 ++- 32 files changed, 764 insertions(+), 688 deletions(-) create mode 100644 src/main/java/ctbrec/recorder/server/RecorderHttpClient.java rename src/main/java/ctbrec/{ => sites}/Site.java (51%) create mode 100644 src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java create mode 100644 src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java diff --git a/src/main/java/ctbrec/Model.java b/src/main/java/ctbrec/Model.java index 63869b78..4f3b4dbc 100644 --- a/src/main/java/ctbrec/Model.java +++ b/src/main/java/ctbrec/Model.java @@ -30,5 +30,6 @@ public interface Model { public void invalidateCacheEntries(); public void receiveTip(int tokens) throws IOException; public int[] getStreamResolution(boolean failFast) throws ExecutionException; - + public boolean follow() throws IOException; + public boolean unfollow() throws IOException; } \ No newline at end of file diff --git a/src/main/java/ctbrec/Settings.java b/src/main/java/ctbrec/Settings.java index fbab0adb..58f0a39d 100644 --- a/src/main/java/ctbrec/Settings.java +++ b/src/main/java/ctbrec/Settings.java @@ -28,7 +28,6 @@ public class Settings { public boolean determineResolution = false; public boolean requireAuthentication = false; public boolean chooseStreamQuality = false; - public boolean recordFollowed = false; public byte[] key = null; public ProxyType proxyType = ProxyType.DIRECT; public String proxyHost; diff --git a/src/main/java/ctbrec/io/HttpClient.java b/src/main/java/ctbrec/io/HttpClient.java index 57129ac4..533db368 100644 --- a/src/main/java/ctbrec/io/HttpClient.java +++ b/src/main/java/ctbrec/io/HttpClient.java @@ -1,44 +1,24 @@ package ctbrec.io; import java.io.IOException; -import java.util.NoSuchElementException; import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import ctbrec.Config; import ctbrec.Settings.ProxyType; -import ctbrec.ui.CamrecApplication; import ctbrec.ui.CookieJarImpl; -import ctbrec.ui.HtmlParser; import okhttp3.ConnectionPool; -import okhttp3.Cookie; -import okhttp3.FormBody; import okhttp3.OkHttpClient; import okhttp3.Request; -import okhttp3.RequestBody; import okhttp3.Response; -public class HttpClient { - private static final transient Logger LOG = LoggerFactory.getLogger(HttpClient.class); - private static HttpClient instance = new HttpClient(); +public abstract class HttpClient { + protected OkHttpClient client; + protected CookieJarImpl cookieJar = new CookieJarImpl(); + protected boolean loggedIn = false; + protected int loginTries = 0; - private OkHttpClient client; - private CookieJarImpl cookieJar = new CookieJarImpl(); - 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(); + protected HttpClient() { + reconfigure(); } private void loadProxySettings() { @@ -89,24 +69,11 @@ public class HttpClient { } } - public static HttpClient getInstance() { - return instance; - } - public Response execute(Request request) throws IOException { Response resp = execute(request, false); 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 { if(requiresLogin && !loggedIn) { boolean loginSuccessful = login(); @@ -115,64 +82,20 @@ public class HttpClient { } } Response resp = client.newCall(req).execute(); - extractCsrfToken(req); return resp; } - public 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 abstract boolean login() throws IOException; public void reconfigure() { - instance = new HttpClient(); - } - - public String getToken() throws IOException { - if(token == null) { - login(); - } - return token; + 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(); } public void shutdown() { diff --git a/src/main/java/ctbrec/recorder/LocalRecorder.java b/src/main/java/ctbrec/recorder/LocalRecorder.java index ff9bfeda..aef0d998 100644 --- a/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -1,8 +1,6 @@ package ctbrec.recorder; -import static ctbrec.Recording.STATUS.FINISHED; -import static ctbrec.Recording.STATUS.GENERATING_PLAYLIST; -import static ctbrec.Recording.STATUS.RECORDING; +import static ctbrec.Recording.STATUS.*; import java.io.File; import java.io.IOException; @@ -30,32 +28,27 @@ import com.iheartradio.m3u8.PlaylistException; import ctbrec.Config; import ctbrec.Model; import ctbrec.Recording; -import ctbrec.io.HttpClient; import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException; import ctbrec.recorder.download.Download; import ctbrec.recorder.download.HlsDownload; import ctbrec.recorder.download.MergedHlsDownload; -import ctbrec.sites.chaturbate.ChaturbateModelParser; -import okhttp3.Request; -import okhttp3.Response; +import ctbrec.recorder.server.RecorderHttpClient; public class LocalRecorder implements Recorder { private static final transient Logger LOG = LoggerFactory.getLogger(LocalRecorder.class); private static final boolean IGNORE_CACHE = true; - private List followedModels = Collections.synchronizedList(new ArrayList<>()); private List models = Collections.synchronizedList(new ArrayList<>()); private Map recordingProcesses = Collections.synchronizedMap(new HashMap<>()); private Map playlistGenerators = new HashMap<>(); private Config config; private ProcessMonitor processMonitor; private OnlineMonitor onlineMonitor; - private FollowedMonitor followedMonitor; private PlaylistGeneratorTrigger playlistGenTrigger; - private HttpClient client = HttpClient.getInstance(); private volatile boolean recording = true; private List deleteInProgress = Collections.synchronizedList(new ArrayList<>()); + private RecorderHttpClient client = new RecorderHttpClient(); public LocalRecorder(Config config) { this.config = config; @@ -74,11 +67,6 @@ public class LocalRecorder implements Recorder { playlistGenTrigger.start(); } - if (config.getSettings().recordFollowed) { - followedMonitor = new FollowedMonitor(); - followedMonitor.start(); - } - LOG.debug("Recorder initialized"); LOG.info("Models to record: {}", models); LOG.info("Saving recordings in {}", config.getSettings().recordingsDir); @@ -88,9 +76,6 @@ public class LocalRecorder implements Recorder { public void startRecording(Model model) { if (!models.contains(model)) { LOG.info("Model {} added", model); - if (followedModels.contains(model)) { - followedModels.remove(model); - } models.add(model); config.getSettings().models.add(model); } @@ -98,9 +83,8 @@ public class LocalRecorder implements Recorder { @Override public void stopRecording(Model model) throws IOException { - if (models.contains(model) || followedModels.contains(model)) { + if (models.contains(model)) { models.remove(model); - followedModels.remove(model); config.getSettings().models.remove(model); if (recordingProcesses.containsKey(model)) { stopRecordingProcess(model); @@ -118,7 +102,7 @@ public class LocalRecorder implements Recorder { return; } - if (!models.contains(model) && !followedModels.contains(model)) { + if (!models.contains(model)) { LOG.info("Model {} has been removed. Restarting of recording cancelled.", model); return; } @@ -151,15 +135,12 @@ public class LocalRecorder implements Recorder { @Override public boolean isRecording(Model model) { - return models.contains(model) || followedModels.contains(model); + return models.contains(model); } @Override public List getModelsRecording() { - List union = new ArrayList<>(); - union.addAll(models); - union.addAll(followedModels); - return Collections.unmodifiableList(union); + return Collections.unmodifiableList(models); } @Override @@ -170,11 +151,9 @@ public class LocalRecorder implements Recorder { onlineMonitor.running = false; processMonitor.running = false; playlistGenTrigger.running = false; - if (followedMonitor != null) { - followedMonitor.running = false; - } LOG.debug("Stopping all recording processes"); stopRecordingProcesses(); + client.shutdown(); } 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 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) { Thread t = new Thread() { @Override diff --git a/src/main/java/ctbrec/recorder/server/HlsServlet.java b/src/main/java/ctbrec/recorder/server/HlsServlet.java index a36239ec..4e425f9e 100644 --- a/src/main/java/ctbrec/recorder/server/HlsServlet.java +++ b/src/main/java/ctbrec/recorder/server/HlsServlet.java @@ -1,9 +1,12 @@ package ctbrec.recorder.server; +import static javax.servlet.http.HttpServletResponse.*; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; @@ -33,20 +36,20 @@ public class HlsServlet extends AbstractCtbrecServlet { File recordingsDir = new File(config.getSettings().recordingsDir); File requestedFile = new File(recordingsDir, request); - // try { - // boolean isRequestAuthenticated = checkAuthentication(req, req.getRequestURI()); - // if (!isRequestAuthenticated) { - // resp.setStatus(SC_UNAUTHORIZED); - // String response = "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}"; - // resp.getWriter().write(response); - // return; - // } - // } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { - // resp.setStatus(SC_UNAUTHORIZED); - // String response = "{\"status\": \"error\", \"msg\": \"Authentication failed\"}"; - // resp.getWriter().write(response); - // return; - // } + try { + boolean isRequestAuthenticated = checkAuthentication(req, req.getRequestURI()); + if (!isRequestAuthenticated) { + resp.setStatus(SC_UNAUTHORIZED); + String response = "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}"; + resp.getWriter().write(response); + return; + } + } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) { + resp.setStatus(SC_UNAUTHORIZED); + String response = "{\"status\": \"error\", \"msg\": \"Authentication failed\"}"; + resp.getWriter().write(response); + return; + } if (requestedFile.getCanonicalPath().startsWith(config.getSettings().recordingsDir)) { if (requestedFile.getName().equals("playlist.m3u8")) { diff --git a/src/main/java/ctbrec/recorder/server/RecorderHttpClient.java b/src/main/java/ctbrec/recorder/server/RecorderHttpClient.java new file mode 100644 index 00000000..22027ba9 --- /dev/null +++ b/src/main/java/ctbrec/recorder/server/RecorderHttpClient.java @@ -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; + } +} diff --git a/src/main/java/ctbrec/Site.java b/src/main/java/ctbrec/sites/Site.java similarity index 51% rename from src/main/java/ctbrec/Site.java rename to src/main/java/ctbrec/sites/Site.java index 5565f7ce..c10815dd 100644 --- a/src/main/java/ctbrec/Site.java +++ b/src/main/java/ctbrec/sites/Site.java @@ -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.ui.TabProvider; @@ -10,4 +14,9 @@ public interface Site { public void setRecorder(Recorder recorder); public TabProvider getTabProvider(); public Model createModel(String name); + public Integer getTokenBalance() throws IOException; + public String getBuyTokensLink(); + public void login() throws IOException; + public HttpClient getHttpClient(); + public void shutdown(); } diff --git a/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java b/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java index e97887e4..6f31e5ac 100644 --- a/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java +++ b/src/main/java/ctbrec/sites/chaturbate/Chaturbate.java @@ -1,13 +1,49 @@ 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.Site; +import ctbrec.Settings; +import ctbrec.io.HttpClient; import ctbrec.recorder.Recorder; +import ctbrec.sites.Site; +import ctbrec.ui.HtmlParser; import ctbrec.ui.TabProvider; +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; 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 ChaturbateHttpClient httpClient = new ChaturbateHttpClient(); @Override public String getName() { @@ -36,9 +72,214 @@ public class Chaturbate implements Site { @Override public Model createModel(String name) { - ChaturbateModel m = new ChaturbateModel(); + ChaturbateModel m = new ChaturbateModel(this); m.setName(name); m.setUrl(getBaseUrl() + '/' + name + '/'); 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 streamInfoCache = CacheBuilder.newBuilder() + .initialCapacity(10_000) + .maximumSize(10_000) + .expireAfterWrite(5, TimeUnit.MINUTES) + .build(new CacheLoader () { + @Override + public StreamInfo load(String model) throws Exception { + return loadStreamInfo(model); + } + }); + + LoadingCache streamResolutionCache = CacheBuilder.newBuilder() + .initialCapacity(10_000) + .maximumSize(10_000) + .expireAfterWrite(5, TimeUnit.MINUTES) + .build(new CacheLoader () { + @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 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(); + } + } } diff --git a/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java b/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java new file mode 100644 index 00000000..33cdbcd8 --- /dev/null +++ b/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java @@ -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; + } +} diff --git a/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java index 311c9fbd..febb5887 100644 --- a/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java +++ b/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java @@ -1,35 +1,23 @@ package ctbrec.sites.chaturbate; -import java.io.EOFException; +import static ctbrec.sites.chaturbate.Chaturbate.*; + import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.List; 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.AbstractModel; -import ctbrec.io.HttpClient; import ctbrec.recorder.download.StreamSource; -import okhttp3.FormBody; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; @@ -37,6 +25,11 @@ import okhttp3.Response; public class ChaturbateModel extends AbstractModel { private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateModel.class); + private Chaturbate site; + + ChaturbateModel(Chaturbate site) { + this.site = site; + } @Override 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 { StreamInfo info; if(ignoreCache) { - info = Chaturbate.INSTANCE.loadStreamInfo(getName()); + info = site.loadStreamInfo(getName()); LOG.trace("Model {} room status: {}", getName(), info.room_status); } else { - info = Chaturbate.INSTANCE.getStreamInfo(getName()); + info = site.getStreamInfo(getName()); } return Objects.equals("public", info.room_status); } @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { - int[] resolution = Chaturbate.INSTANCE.streamResolutionCache.getIfPresent(getName()); + int[] resolution = site.streamResolutionCache.getIfPresent(getName()); if(resolution != null) { - return Chaturbate.INSTANCE.getResolution(getName()); + return site.getResolution(getName()); } else { if(failFast) { return new int[2]; } else { - return Chaturbate.INSTANCE.getResolution(getName()); + return site.getResolution(getName()); } } } @@ -75,8 +68,8 @@ public class ChaturbateModel extends AbstractModel { */ @Override public void invalidateCacheEntries() { - Chaturbate.INSTANCE.streamInfoCache.invalidate(getName()); - Chaturbate.INSTANCE.streamResolutionCache.invalidate(getName()); + site.streamInfoCache.invalidate(getName()); + site.streamResolutionCache.invalidate(getName()); } public String getOnlineState() throws IOException, ExecutionException { @@ -85,20 +78,20 @@ public class ChaturbateModel extends AbstractModel { @Override 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"; } public StreamInfo getStreamInfo() throws IOException, ExecutionException { - return Chaturbate.INSTANCE.getStreamInfo(getName()); + return site.getStreamInfo(getName()); } public MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException, ExecutionException { - return Chaturbate.INSTANCE.getMasterPlaylist(getName()); + return site.getMasterPlaylist(getName()); } @Override public void receiveTip(int tokens) throws IOException { - Chaturbate.INSTANCE.sendTip(getName(), tokens); + site.sendTip(getName(), tokens); } @Override @@ -123,167 +116,52 @@ public class ChaturbateModel extends AbstractModel { return sources; } - private static class Chaturbate { - private static final transient Logger LOG = LoggerFactory.getLogger(Chaturbate.class); + @Override + public boolean follow() throws IOException { + return follow(true); + } - public static final Chaturbate INSTANCE = new Chaturbate(HttpClient.getInstance()); + @Override + public boolean unfollow() throws IOException { + return follow(false); + } - private HttpClient client; + private boolean follow(boolean follow) throws IOException { + Request req = new Request.Builder().url(getUrl()).build(); + Response resp = site.getHttpClient().execute(req); + resp.close(); - private static long lastRequest = System.currentTimeMillis(); - - private LoadingCache streamInfoCache = CacheBuilder.newBuilder() - .initialCapacity(10_000) - .maximumSize(10_000) - .expireAfterWrite(5, TimeUnit.MINUTES) - .build(new CacheLoader () { - @Override - public StreamInfo load(String model) throws Exception { - return loadStreamInfo(model); - } - }); - - private LoadingCache streamResolutionCache = CacheBuilder.newBuilder() - .initialCapacity(10_000) - .maximumSize(10_000) - .expireAfterWrite(5, TimeUnit.MINUTES) - .build(new CacheLoader () { - @Override - public int[] load(String model) throws Exception { - return loadResolution(model); - } - }); - - public Chaturbate(HttpClient client) { - this.client = client; + String url = null; + if(follow) { + url = BASE_URI + "/follow/follow/" + getName() + "/"; + } else { + url = BASE_URI + "/follow/unfollow/" + getName() + "/"; } - public void sendTip(String name, int tokens) throws IOException { - if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { - RequestBody body = new FormBody.Builder() - .add("csrfmiddlewaretoken", client.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 = client.execute(req, true)) { - if(!response.isSuccessful()) { - throw new IOException(response.code() + " " + response.message()); - } - } - } - } - - private StreamInfo getStreamInfo(String modelName) throws IOException, ExecutionException { - return streamInfoCache.get(modelName); - } - - 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 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 = 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(); + 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", getUrl()) + .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0") + .header("X-CSRFToken", ((ChaturbateHttpClient)site.getHttpClient()).getToken()) + .header("X-Requested-With", "XMLHttpRequest") + .build(); + resp = site.getHttpClient().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); + return true; } + } else { + resp.close(); + throw new IOException("HTTP status " + resp.code() + " " + resp.message()); } } } diff --git a/src/main/java/ctbrec/sites/chaturbate/ChaturbateModelParser.java b/src/main/java/ctbrec/sites/chaturbate/ChaturbateModelParser.java index a3fc0579..dec5690d 100644 --- a/src/main/java/ctbrec/sites/chaturbate/ChaturbateModelParser.java +++ b/src/main/java/ctbrec/sites/chaturbate/ChaturbateModelParser.java @@ -1,7 +1,5 @@ package ctbrec.sites.chaturbate; -import static ctbrec.ui.CamrecApplication.BASE_URI; - import java.util.ArrayList; import java.util.List; @@ -16,16 +14,15 @@ import ctbrec.ui.HtmlParser; public class ChaturbateModelParser { private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateModelParser.class); - public static List parseModels(String html) { + public static List parseModels(Chaturbate chaturbate, String html) { List models = new ArrayList<>(); Elements cells = HtmlParser.getTags(html, "ul.list > li"); for (Element cell : cells) { String cellHtml = cell.html(); 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.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")); Elements tags = HtmlParser.getTags(cellHtml, "div.details ul.subject li a"); if(tags != null) { diff --git a/src/main/java/ctbrec/sites/chaturbate/ChaturbateTabProvider.java b/src/main/java/ctbrec/sites/chaturbate/ChaturbateTabProvider.java index 2aa5674e..5c683ada 100644 --- a/src/main/java/ctbrec/sites/chaturbate/ChaturbateTabProvider.java +++ b/src/main/java/ctbrec/sites/chaturbate/ChaturbateTabProvider.java @@ -29,7 +29,7 @@ public class ChaturbateTabProvider extends TabProvider { tabs.add(createTab("Male", BASE_URI + "/male-cams/")); tabs.add(createTab("Couples", BASE_URI + "/couple-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.setScene(scene); tabs.add(followedTab); @@ -37,8 +37,8 @@ public class ChaturbateTabProvider extends TabProvider { } private Tab createTab(String title, String url) { - ChaturbateUpdateService updateService = new ChaturbateUpdateService(url, false); - ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService); + ChaturbateUpdateService updateService = new ChaturbateUpdateService(url, false, chaturbate); + ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, chaturbate); tab.setRecorder(recorder); return tab; } diff --git a/src/main/java/ctbrec/sites/chaturbate/ChaturbateUpdateService.java b/src/main/java/ctbrec/sites/chaturbate/ChaturbateUpdateService.java index b13d81f3..3d2dfe90 100644 --- a/src/main/java/ctbrec/sites/chaturbate/ChaturbateUpdateService.java +++ b/src/main/java/ctbrec/sites/chaturbate/ChaturbateUpdateService.java @@ -10,7 +10,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Model; -import ctbrec.io.HttpClient; import ctbrec.ui.PaginatedScheduledService; import javafx.concurrent.Task; import okhttp3.Request; @@ -21,10 +20,12 @@ public class ChaturbateUpdateService extends PaginatedScheduledService { private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateUpdateService.class); private String url; private boolean loginRequired; + private Chaturbate chaturbate; - public ChaturbateUpdateService(String url, boolean loginRequired) { + public ChaturbateUpdateService(String url, boolean loginRequired, Chaturbate chaturbate) { this.url = url; this.loginRequired = loginRequired; + this.chaturbate = chaturbate; ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() { @Override @@ -46,9 +47,9 @@ public class ChaturbateUpdateService extends PaginatedScheduledService { String url = ChaturbateUpdateService.this.url + "?page="+page+"&keywords=&_=" + System.currentTimeMillis(); LOG.debug("Fetching page {}", url); 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()) { - List models = ChaturbateModelParser.parseModels(response.body().string()); + List models = ChaturbateModelParser.parseModels(chaturbate, response.body().string()); response.close(); return models; } else { diff --git a/src/main/java/ctbrec/sites/mfc/FriendsUpdateService.java b/src/main/java/ctbrec/sites/mfc/FriendsUpdateService.java index 8db70760..ec7d6763 100644 --- a/src/main/java/ctbrec/sites/mfc/FriendsUpdateService.java +++ b/src/main/java/ctbrec/sites/mfc/FriendsUpdateService.java @@ -37,7 +37,7 @@ public class FriendsUpdateService extends PaginatedScheduledService { .url(url) .header("Referer", myFreeCams.getBaseUrl()) .build(); - Response resp = MyFreeCams.httpClient.newCall(req).execute(); + Response resp = myFreeCams.getHttpClient().execute(req, true); if(resp.isSuccessful()) { String json = resp.body().string().substring(4); JSONObject object = new JSONObject(json); diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCams.java b/src/main/java/ctbrec/sites/mfc/MyFreeCams.java index b37aa473..dea59ff1 100644 --- a/src/main/java/ctbrec/sites/mfc/MyFreeCams.java +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCams.java @@ -1,35 +1,18 @@ package ctbrec.sites.mfc; import java.io.IOException; -import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ctbrec.Config; -import ctbrec.Site; import ctbrec.recorder.Recorder; -import ctbrec.ui.CookieJarImpl; +import ctbrec.sites.Site; import ctbrec.ui.TabProvider; -import okhttp3.ConnectionPool; -import okhttp3.FormBody; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; public class MyFreeCams implements Site { - private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCams.class); + public static final String BASE_URI = "https://www.myfreecams.com"; private Recorder recorder; private MyFreeCamsClient client; - public static OkHttpClient httpClient = new OkHttpClient.Builder() - .connectTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS) - .readTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS) - .connectionPool(new ConnectionPool(50, 10, TimeUnit.MINUTES)) - .cookieJar(new CookieJarImpl()) - .build(); + private MyFreeCamsHttpClient httpClient = new MyFreeCamsHttpClient(); public MyFreeCams() throws IOException { client = MyFreeCamsClient.getInstance(); @@ -39,25 +22,9 @@ public class MyFreeCams implements Site { login(); } + @Override 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 @@ -67,7 +34,7 @@ public class MyFreeCams implements Site { @Override public String getBaseUrl() { - return "https://www.myfreecams.com"; + return BASE_URI; } @Override @@ -87,9 +54,29 @@ public class MyFreeCams implements Site { @Override public MyFreeCamsModel createModel(String name) { - MyFreeCamsModel model = new MyFreeCamsModel(); + MyFreeCamsModel model = new MyFreeCamsModel(this); model.setName(name); model.setUrl("https://profiles.myfreecams.com/" + name); 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(); + } } diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index 18d8296f..69a25c7e 100644 --- a/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -62,7 +62,7 @@ public class MyFreeCamsClient { public void start() throws IOException { running = true; - ServerConfig serverConfig = new ServerConfig(MyFreeCams.httpClient); + ServerConfig serverConfig = new ServerConfig(mfc.getHttpClient()); List websocketServers = new ArrayList(serverConfig.wsServers.keySet()); String server = websocketServers.get((int) (Math.random()*websocketServers.size())); String wsUrl = "ws://" + server + ".myfreecams.com:8080/fcsl"; @@ -89,7 +89,7 @@ public class MyFreeCamsClient { } private WebSocket createWebSocket(Request req) { - WebSocket ws = MyFreeCams.httpClient.newWebSocket(req, new WebSocketListener() { + WebSocket ws = mfc.getHttpClient().newWebSocket(req, new WebSocketListener() { @Override public void onOpen(WebSocket webSocket, Response response) { super.onOpen(webSocket, response); @@ -111,7 +111,7 @@ public class MyFreeCamsClient { super.onClosed(webSocket, code, reason); LOG.trace("close: {} {}", code, reason); running = false; - MyFreeCams.httpClient.dispatcher().executorService().shutdownNow(); + mfc.getHttpClient().shutdown(); } private StringBuilder msgBuffer = new StringBuilder(); @@ -190,7 +190,7 @@ public class MyFreeCamsClient { String url = base + "?respkey="+respkey+"&opts="+opts+"&serv="+serv+"&type="+type; Request req = new Request.Builder().url(url).build(); LOG.debug("Requesting EXTDATA {}", url); - Response resp = MyFreeCams.httpClient.newCall(req).execute(); + Response resp = mfc.getHttpClient().execute(req); if(resp.isSuccessful()) { parseExtDataSessionStates(resp.body().string()); @@ -354,4 +354,14 @@ public class MyFreeCamsClient { public void execute(Runnable r) { executor.execute(r); } + + public void getSessionState(ctbrec.Model model) { + for (SessionState state : sessionStates.values()) { + if(Objects.equals(state.getNm(), model.getName())) { + JsonAdapter adapter = moshi.adapter(SessionState.class).indent(" "); + System.out.println(adapter.toJson(state)); + System.out.println("#####################"); + } + } + } } diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java b/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java new file mode 100644 index 00000000..e72f591d --- /dev/null +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java @@ -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); + } +} diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index 63fa48f8..358eacad 100644 --- a/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -32,6 +32,11 @@ public class MyFreeCamsModel extends AbstractModel { private double camScore; private State state; private int resolution[]; + private MyFreeCams site; + + MyFreeCamsModel(MyFreeCams site) { + this.site = site; + } @Override public boolean isOnline() throws IOException, ExecutionException, InterruptedException { @@ -78,7 +83,7 @@ public class MyFreeCamsModel extends AbstractModel { } LOG.debug("Loading master playlist {}", hlsUrl); Request req = new Request.Builder().url(hlsUrl).build(); - Response response = MyFreeCams.httpClient.newCall(req).execute(); + Response response = site.getHttpClient().execute(req); try { InputStream inputStream = response.body().byteStream(); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); @@ -97,7 +102,7 @@ public class MyFreeCamsModel extends AbstractModel { @Override public void receiveTip(int tokens) throws IOException { - throw new RuntimeException("Not implemented"); + throw new RuntimeException("Not implemented for MFC"); } @Override @@ -155,8 +160,29 @@ public class MyFreeCamsModel extends AbstractModel { // 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); } } + + @Override + public boolean follow() { + return false; + } + + @Override + public boolean unfollow() { + return false; + } } diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCamsTabProvider.java b/src/main/java/ctbrec/sites/mfc/MyFreeCamsTabProvider.java index 97d719db..e81cd019 100644 --- a/src/main/java/ctbrec/sites/mfc/MyFreeCamsTabProvider.java +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCamsTabProvider.java @@ -26,18 +26,17 @@ public class MyFreeCamsTabProvider extends TabProvider { List tabs = new ArrayList<>(); PaginatedScheduledService updateService = new OnlineCamsUpdateService(); - ThumbOverviewTab online = new ThumbOverviewTab("Online", updateService); + ThumbOverviewTab online = new ThumbOverviewTab("Online", updateService, myFreeCams); online.setRecorder(recorder); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); tabs.add(online); updateService = new FriendsUpdateService(myFreeCams); - ThumbOverviewTab friends = new ThumbOverviewTab("Friends", updateService); + ThumbOverviewTab friends = new ThumbOverviewTab("Friends", updateService, myFreeCams); friends.setRecorder(recorder); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); tabs.add(friends); - return tabs; } } diff --git a/src/main/java/ctbrec/sites/mfc/OnlineModelsTab.java b/src/main/java/ctbrec/sites/mfc/OnlineModelsTab.java index 49d47fe8..91f69b18 100644 --- a/src/main/java/ctbrec/sites/mfc/OnlineModelsTab.java +++ b/src/main/java/ctbrec/sites/mfc/OnlineModelsTab.java @@ -1,12 +1,13 @@ package ctbrec.sites.mfc; +import ctbrec.sites.Site; import ctbrec.ui.PaginatedScheduledService; import ctbrec.ui.ThumbOverviewTab; public class OnlineModelsTab extends ThumbOverviewTab { - public OnlineModelsTab(String title, PaginatedScheduledService updateService) { - super(title, updateService); + public OnlineModelsTab(String title, PaginatedScheduledService updateService, Site site) { + super(title, updateService, site); } } diff --git a/src/main/java/ctbrec/sites/mfc/ServerConfig.java b/src/main/java/ctbrec/sites/mfc/ServerConfig.java index d6095fda..973b4767 100644 --- a/src/main/java/ctbrec/sites/mfc/ServerConfig.java +++ b/src/main/java/ctbrec/sites/mfc/ServerConfig.java @@ -9,7 +9,7 @@ import java.util.Map; import org.json.JSONArray; import org.json.JSONObject; -import okhttp3.OkHttpClient; +import ctbrec.io.HttpClient; import okhttp3.Request; import okhttp3.Response; @@ -23,9 +23,9 @@ public class ServerConfig { Map wzobsServers; Map 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(); - Response resp = client.newCall(req).execute(); + Response resp = client.execute(req); String json = resp.body().string(); JSONObject serverConfig = new JSONObject(json); diff --git a/src/main/java/ctbrec/ui/CamrecApplication.java b/src/main/java/ctbrec/ui/CamrecApplication.java index 02e42c48..5ff0bb9a 100644 --- a/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/src/main/java/ctbrec/ui/CamrecApplication.java @@ -7,7 +7,6 @@ import java.io.InputStreamReader; import java.lang.reflect.Type; import java.util.List; import java.util.Objects; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import org.slf4j.Logger; @@ -20,26 +19,31 @@ import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import ctbrec.Config; -import ctbrec.Site; import ctbrec.Version; -import ctbrec.io.HttpClient; import ctbrec.recorder.LocalRecorder; import ctbrec.recorder.Recorder; import ctbrec.recorder.RemoteRecorder; +import ctbrec.sites.Site; import ctbrec.sites.mfc.MyFreeCams; import javafx.application.Application; import javafx.application.HostServices; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; -import javafx.concurrent.Task; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Alert; +import javafx.scene.control.Button; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.TabPane.TabClosingPolicy; import javafx.scene.image.Image; import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.text.Font; import javafx.stage.Stage; import okhttp3.Request; import okhttp3.Response; @@ -47,12 +51,9 @@ import okhttp3.Response; public class CamrecApplication extends Application { 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 Recorder recorder; - private HttpClient client; static HostServices hostServices; private SettingsTab settingsTab; private TabPane tabPane = new TabPane(); @@ -65,12 +66,13 @@ public class CamrecApplication extends Application { loadConfig(); bus = new AsyncEventBus(Executors.newSingleThreadExecutor()); hostServices = getHostServices(); - client = HttpClient.getInstance(); createRecorder(); - //site = new Chaturbate(); + // site = new Chaturbate(); site = new MyFreeCams(); site.setRecorder(recorder); - // TODO move this to Chaturbate class doInitialLogin(); + if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { + site.login(); + } createGui(primaryStage); checkForUpdates(); } @@ -132,7 +134,7 @@ public class CamrecApplication extends Application { public void run() { settingsTab.saveConfig(); recorder.shutdown(); - client.shutdown(); + site.shutdown(); try { Config.getInstance().save(); LOG.info("Shutdown complete. Goodbye!"); @@ -152,103 +154,43 @@ public class CamrecApplication extends Application { }.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 task = new Task() { - @Override - protected Integer call() throws Exception { - if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { - 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 + "/"; - 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); - return tokens; - } else { - throw new IOException("HTTP response: " + resp.code() + " - " + resp.message()); - } - } else { - return 1_000_000; - } - } - - @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")); - } - } - }; - 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); + 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(site.getBuyTokensLink())); + buyTokens.setMaxHeight(tabPane.getTabMaxHeight()); + TokenLabel tokenBalance = new TokenLabel(site); + tokenPanel = new HBox(5, tokenBalance, buyTokens); + 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); } } - }; - }.start(); + + } + } + tokenBalance.loadBalance(); } } private void createRecorder() { - if(config.getSettings().localRecording) { + if (config.getSettings().localRecording) { recorder = new LocalRecorder(config); } else { - recorder = new RemoteRecorder(config, client); + recorder = new RemoteRecorder(config, site.getHttpClient()); } } @@ -275,8 +217,8 @@ public class CamrecApplication extends Application { try { String url = "https://api.github.com/repos/0xboobface/ctbrec/releases"; Request request = new Request.Builder().url(url).build(); - Response response = client.execute(request); - if(response.isSuccessful()) { + Response response = site.getHttpClient().execute(request); + if (response.isSuccessful()) { Moshi moshi = new Moshi.Builder().build(); Type type = Types.newParameterizedType(List.class, Release.class); JsonAdapter> adapter = moshi.adapter(type); @@ -284,7 +226,7 @@ public class CamrecApplication extends Application { Release latest = releases.get(0); Version latestVersion = latest.getVersion(); Version ctbrecVersion = getVersion(); - if(latestVersion.compareTo(ctbrecVersion) > 0) { + if (latestVersion.compareTo(ctbrecVersion) > 0) { LOG.debug("Update available {} < {}", ctbrecVersion, latestVersion); Platform.runLater(() -> tabPane.getTabs().add(new UpdateTab(latest))); } else { @@ -303,10 +245,10 @@ public class CamrecApplication extends Application { } 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"); } else { - try(InputStream is = getClass().getClassLoader().getResourceAsStream("version")) { + try (InputStream is = getClass().getClassLoader().getResourceAsStream("version")) { BufferedReader reader = new BufferedReader(new InputStreamReader(is)); String versionString = reader.readLine(); Version version = Version.of(versionString); diff --git a/src/main/java/ctbrec/ui/DonateTabFx.java b/src/main/java/ctbrec/ui/DonateTabFx.java index 50125693..f165c4b5 100644 --- a/src/main/java/ctbrec/ui/DonateTabFx.java +++ b/src/main/java/ctbrec/ui/DonateTabFx.java @@ -2,6 +2,7 @@ package ctbrec.ui; +import ctbrec.sites.chaturbate.Chaturbate; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; @@ -44,7 +45,7 @@ public class DonateTabFx extends Tab { ImageView tokenImage = new ImageView(getClass().getResource("/html/token.png").toString()); 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); tokenBox.setAlignment(Pos.TOP_CENTER); Label tokenDesc = new Label("If you buy tokens by using this button,\n" diff --git a/src/main/java/ctbrec/ui/FollowedTab.java b/src/main/java/ctbrec/ui/FollowedTab.java index 446f6c28..92d412f1 100644 --- a/src/main/java/ctbrec/ui/FollowedTab.java +++ b/src/main/java/ctbrec/ui/FollowedTab.java @@ -1,5 +1,6 @@ package ctbrec.ui; +import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.chaturbate.ChaturbateUpdateService; import javafx.concurrent.WorkerStateEvent; import javafx.geometry.Insets; @@ -16,8 +17,8 @@ public class FollowedTab extends ThumbOverviewTab { private String onlineUrl; private String offlineUrl; - public FollowedTab(String title, String url) { - super(title, new ChaturbateUpdateService(url, true)); + public FollowedTab(String title, String url, Chaturbate chaturbate) { + super(title, new ChaturbateUpdateService(url, true, chaturbate), chaturbate); onlineUrl = url; offlineUrl = url + "offline/"; diff --git a/src/main/java/ctbrec/ui/JavaFxModel.java b/src/main/java/ctbrec/ui/JavaFxModel.java index ef48491b..ef6bda02 100644 --- a/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/src/main/java/ctbrec/ui/JavaFxModel.java @@ -126,4 +126,14 @@ public class JavaFxModel extends AbstractModel { public int[] getStreamResolution(boolean b) throws ExecutionException { return delegate.getStreamResolution(b); } + + @Override + public boolean follow() throws IOException { + return delegate.follow(); + } + + @Override + public boolean unfollow() throws IOException { + return delegate.unfollow(); + } } diff --git a/src/main/java/ctbrec/ui/RecordedModelsTab.java b/src/main/java/ctbrec/ui/RecordedModelsTab.java index c8054536..ff5d9e07 100644 --- a/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -19,9 +19,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Model; -import ctbrec.Site; -import ctbrec.io.HttpClient; import ctbrec.recorder.Recorder; +import ctbrec.sites.Site; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -266,7 +265,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { return; } - HttpClient client = HttpClient.getInstance(); Function onSuccess = (m) -> { try { recorder.switchStreamSource(m); @@ -281,7 +279,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener { showStreamSwitchErrorDialog(t); return null; }; - StreamSourceSelectionDialog.show(fxModel.getDelegate(), client, onSuccess, onFail); + StreamSourceSelectionDialog.show(fxModel.getDelegate(), site.getHttpClient(), onSuccess, onFail); } private void showStreamSwitchErrorDialog(Throwable throwable) { diff --git a/src/main/java/ctbrec/ui/RecordingsTab.java b/src/main/java/ctbrec/ui/RecordingsTab.java index 2e490a6a..4f4dfb54 100644 --- a/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/src/main/java/ctbrec/ui/RecordingsTab.java @@ -1,7 +1,6 @@ package ctbrec.ui; -import static javafx.scene.control.ButtonType.NO; -import static javafx.scene.control.ButtonType.YES; +import static javafx.scene.control.ButtonType.*; import java.io.File; import java.io.FileNotFoundException; @@ -32,10 +31,9 @@ import ctbrec.Config; import ctbrec.Model; import ctbrec.Recording; import ctbrec.Recording.STATUS; -import ctbrec.Site; -import ctbrec.io.HttpClient; import ctbrec.recorder.Recorder; import ctbrec.recorder.download.MergedHlsDownload; +import ctbrec.sites.Site; import javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; @@ -306,7 +304,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener { @Override public void run() { try { - MergedHlsDownload download = new MergedHlsDownload(HttpClient.getInstance()); + MergedHlsDownload download = new MergedHlsDownload(site.getHttpClient()); download.start(url.toString(), target, (progress) -> { Platform.runLater(() -> { if (progress == 100) { diff --git a/src/main/java/ctbrec/ui/SettingsTab.java b/src/main/java/ctbrec/ui/SettingsTab.java index 4cfb8804..8fecb826 100644 --- a/src/main/java/ctbrec/ui/SettingsTab.java +++ b/src/main/java/ctbrec/ui/SettingsTab.java @@ -13,6 +13,7 @@ import com.sun.javafx.collections.ObservableListWrapper; import ctbrec.Config; import ctbrec.Hmac; +import ctbrec.sites.chaturbate.Chaturbate; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; @@ -57,7 +58,6 @@ public class SettingsTab extends Tab implements TabSelectionListener { private CheckBox loadResolution; private CheckBox secureCommunication = new CheckBox(); private CheckBox chooseStreamQuality = new CheckBox(); - private CheckBox autoRecordFollowed = new CheckBox(); private CheckBox multiplePlayers = new CheckBox(); private PasswordField password; private RadioButton recordLocal; @@ -188,25 +188,9 @@ public class SettingsTab extends Tab implements TabSelectionListener { layout.add(password, 1, 1); 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); 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(password, new Insets(0, 0, 0, CHECKBOX_MARGIN)); GridPane.setMargin(createAccount, new Insets(0, 0, 0, CHECKBOX_MARGIN)); diff --git a/src/main/java/ctbrec/ui/ThumbCell.java b/src/main/java/ctbrec/ui/ThumbCell.java index 625ed520..00a3febb 100644 --- a/src/main/java/ctbrec/ui/ThumbCell.java +++ b/src/main/java/ctbrec/ui/ThumbCell.java @@ -45,9 +45,6 @@ import javafx.scene.text.Font; import javafx.scene.text.Text; import javafx.scene.text.TextAlignment; import javafx.util.Duration; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; public class ThumbCell extends StackPane { @@ -347,91 +344,55 @@ public class ThumbCell extends StackPane { } private void _startStopAction(Model model, boolean start) { - new Thread() { - @Override - public void run() { - try { - if(start) { - recorder.startRecording(model); - setRecording(true); - } else { - recorder.stopRecording(model); - setRecording(false); - } - } catch (Exception e1) { - LOG.error("Couldn't start/stop recording", e1); - Platform.runLater(() -> { - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Error"); - alert.setHeaderText("Couldn't start/stop recording"); - alert.setContentText("I/O error while starting/stopping the recording: " + e1.getLocalizedMessage()); - alert.showAndWait(); - }); - } finally { - setCursor(Cursor.DEFAULT); + new Thread(() -> { + try { + if(start) { + recorder.startRecording(model); + setRecording(true); + } else { + recorder.stopRecording(model); + setRecording(false); } + } catch (Exception e1) { + LOG.error("Couldn't start/stop recording", e1); + Platform.runLater(() -> { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText("Couldn't start/stop recording"); + alert.setContentText("I/O error while starting/stopping the recording: " + e1.getLocalizedMessage()); + alert.showAndWait(); + }); + } finally { + setCursor(Cursor.DEFAULT); } - }.start(); + }).start(); } void follow(boolean follow) { setCursor(Cursor.WAIT); - new Thread() { - @Override - public void run() { - try { - Request req = new Request.Builder().url(model.getUrl()).build(); - Response resp = HttpClient.getInstance().execute(req); - resp.close(); - - String url = null; - if(follow) { - url = CamrecApplication.BASE_URI + "/follow/follow/" + model.getName() + "/"; - } else { - url = CamrecApplication.BASE_URI + "/follow/unfollow/" + model.getName() + "/"; + new Thread(() -> { + try { + if(follow) { + model.follow(); + } else { + boolean unfollowed = model.unfollow(); + if(unfollowed) { + Platform.runLater(() -> thumbCellList.remove(ThumbCell.this)); } - - 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)); - } - } - } else { - resp.close(); - throw new IOException("HTTP status " + resp.code() + " " + resp.message()); - } - } catch (Exception e1) { - LOG.error("Couldn't follow/unfollow model {}", model.getName(), e1); - Platform.runLater(() -> { - Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); - alert.setTitle("Error"); - alert.setHeaderText("Couldn't follow/unfollow model"); - alert.setContentText("I/O error while following/unfollowing model " + model.getName() + ": " + e1.getLocalizedMessage()); - alert.showAndWait(); - }); - } finally { - setCursor(Cursor.DEFAULT); } + } catch (Exception e1) { + LOG.error("Couldn't follow/unfollow model {}", model.getName(), e1); + Platform.runLater(() -> { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText("Couldn't follow/unfollow model"); + alert.setContentText("I/O error while following/unfollowing model " + model.getName() + ": " + e1.getLocalizedMessage()); + alert.showAndWait(); + }); + } finally { + setCursor(Cursor.DEFAULT); } - }.start(); + }).start(); } public Model getModel() { diff --git a/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/src/main/java/ctbrec/ui/ThumbOverviewTab.java index 0e50e23e..07ff06bf 100644 --- a/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -29,6 +29,8 @@ import ctbrec.Config; import ctbrec.Model; import ctbrec.io.HttpClient; import ctbrec.recorder.Recorder; +import ctbrec.sites.Site; +import ctbrec.sites.mfc.MyFreeCamsClient; import javafx.collections.ObservableList; import javafx.concurrent.Worker.State; import javafx.concurrent.WorkerStateEvent; @@ -71,19 +73,20 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { ReentrantLock gridLock = new ReentrantLock(); ScrollPane scrollPane = new ScrollPane(); boolean loginRequired; - HttpClient client = HttpClient.getInstance(); HBox pagination; TextField pageInput = new TextField(Integer.toString(1)); Button pagePrev = new Button("◀"); Button pageNext = new Button("▶"); private volatile boolean updatesSuspended = false; ContextMenu popup; + Site site; private ComboBox thumbWidth; - public ThumbOverviewTab(String title, PaginatedScheduledService updateService) { + public ThumbOverviewTab(String title, PaginatedScheduledService updateService, Site site) { super(title); this.updateService = updateService; + this.site = site; setClosable(false); createGui(); initializeUpdateService(); @@ -253,7 +256,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } } if(!found) { - ThumbCell newCell = createThumbCell(this, model, recorder, client); + ThumbCell newCell = createThumbCell(this, model, recorder, site.getHttpClient()); newCell.setIndex(index); 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 newCell = new ThumbCell(this, model, recorder, client); + ThumbCell newCell = new ThumbCell(this, model, recorder, site.getHttpClient()); newCell.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { suspendUpdates(true); popup = createContextMenu(newCell); @@ -328,7 +331,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { MenuItem sendTip = new MenuItem("Send Tip"); sendTip.setOnAction((e) -> { - TipDialog tipDialog = new TipDialog(cell.getModel()); + TipDialog tipDialog = new TipDialog(site, cell.getModel()); tipDialog.showAndWait(); String tipText = tipDialog.getResult(); if(tipText != null) { @@ -340,7 +343,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { event.put("event", "tokens.sent"); event.put("amount", tokens); CamrecApplication.bus.post(event); - } catch (IOException e1) { + } catch (Exception e1) { Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); alert.setTitle("Error"); 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.setAutoHide(true); contextMenu.setHideOnEscape(true); contextMenu.setAutoFix(true); 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; } diff --git a/src/main/java/ctbrec/ui/TipDialog.java b/src/main/java/ctbrec/ui/TipDialog.java index 8e01cf72..c296e040 100644 --- a/src/main/java/ctbrec/ui/TipDialog.java +++ b/src/main/java/ctbrec/ui/TipDialog.java @@ -1,30 +1,27 @@ package ctbrec.ui; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; import java.util.concurrent.ExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ctbrec.Config; import ctbrec.Model; -import ctbrec.io.HttpClient; +import ctbrec.sites.Site; +import ctbrec.sites.chaturbate.Chaturbate; import javafx.application.Platform; import javafx.concurrent.Task; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.control.TextInputDialog; -import okhttp3.Request; -import okhttp3.Response; public class TipDialog extends TextInputDialog { 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"); loadTokenBalance(); setHeaderText("Loading token balance…"); @@ -38,27 +35,7 @@ public class TipDialog extends TextInputDialog { @Override protected Integer call() throws Exception { if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { - 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 + "/"; - 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 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()); - } + return site.getTokenBalance(); } else { return 1_000_000; } @@ -78,7 +55,7 @@ public class TipDialog extends TextInputDialog { buyTokens.showAndWait(); TipDialog.this.close(); if(buyTokens.getResult() == ButtonType.YES) { - DesktopIntergation.open(CamrecApplication.AFFILIATE_LINK); + DesktopIntergation.open(Chaturbate.AFFILIATE_LINK); } } else { getEditor().setDisable(false); diff --git a/src/main/java/ctbrec/ui/TokenLabel.java b/src/main/java/ctbrec/ui/TokenLabel.java index 073fb038..a3123a34 100644 --- a/src/main/java/ctbrec/ui/TokenLabel.java +++ b/src/main/java/ctbrec/ui/TokenLabel.java @@ -2,17 +2,26 @@ package ctbrec.ui; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ExecutionException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.eventbus.Subscribe; +import ctbrec.sites.Site; import javafx.application.Platform; +import javafx.concurrent.Task; import javafx.scene.control.Label; public class TokenLabel extends Label { + private static final transient Logger LOG = LoggerFactory.getLogger(TokenLabel.class); private int tokens = -1; + private Site site; - public TokenLabel() { + public TokenLabel(Site site) { + this.site = site; setText("Tokens: loading…"); CamrecApplication.bus.register(new Object() { @Subscribe @@ -42,4 +51,29 @@ public class TokenLabel extends Label { private void updateText() { Platform.runLater(() -> setText("Tokens: " + tokens)); } + + public void loadBalance() { + Task task = new Task() { + @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(); + } }