From 9f9d726e5c2b83d51d3165f98bff859fc7defc55 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 25 Oct 2018 16:19:47 +0200 Subject: [PATCH 01/42] Add site Camsoda Add new package and classes for camsoda.com --- .../java/ctbrec/sites/camsoda/Camsoda.java | 111 ++++++++++++++++++ .../ctbrec/sites/camsoda/CamsodaModel.java | 76 ++++++++++++ .../sites/camsoda/CamsodaTabProvider.java | 17 +++ .../java/ctbrec/ui/CamrecApplication.java | 2 + 4 files changed, 206 insertions(+) create mode 100644 src/main/java/ctbrec/sites/camsoda/Camsoda.java create mode 100644 src/main/java/ctbrec/sites/camsoda/CamsodaModel.java create mode 100644 src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java diff --git a/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/src/main/java/ctbrec/sites/camsoda/Camsoda.java new file mode 100644 index 00000000..4cf02389 --- /dev/null +++ b/src/main/java/ctbrec/sites/camsoda/Camsoda.java @@ -0,0 +1,111 @@ +package ctbrec.sites.camsoda; + +import java.io.IOException; + +import ctbrec.Model; +import ctbrec.io.HttpClient; +import ctbrec.recorder.Recorder; +import ctbrec.sites.Site; +import ctbrec.ui.TabProvider; +import javafx.scene.Node; + +public class Camsoda implements Site { + + private Recorder recorder; + private HttpClient httpClient; + + @Override + public String getName() { + return "CamSoda"; + } + + @Override + public String getBaseUrl() { + return "https://www.camsoda.com"; + } + + @Override + public String getAffiliateLink() { + return ""; + } + + @Override + public void setRecorder(Recorder recorder) { + this.recorder = recorder; + } + + @Override + public TabProvider getTabProvider() { + return new CamsodaTabProvider(); + } + + @Override + public Model createModel(String name) { + CamsodaModel model = new CamsodaModel(); + model.setName(name); + model.setUrl(getBaseUrl() + "/" + name); + return model; + } + + @Override + public Integer getTokenBalance() throws IOException { + return 0; + } + + @Override + public String getBuyTokensLink() { + return getBaseUrl(); + } + + @Override + public void login() throws IOException { + httpClient.login(); + } + + @Override + public HttpClient getHttpClient() { + return httpClient; + } + + @Override + public void init() throws IOException { + httpClient = new HttpClient() { + @Override + public boolean login() throws IOException { + return false; + } + + }; + } + + @Override + public void shutdown() { + httpClient.shutdown(); + } + + @Override + public boolean supportsTips() { + return false; + } + + @Override + public boolean supportsFollow() { + return false; + } + + @Override + public boolean isSiteForModel(Model m) { + return m instanceof CamsodaModel; + } + + @Override + public Node getConfigurationGui() { + return null; + } + + @Override + public boolean credentialsAvailable() { + return false; + } + +} diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java new file mode 100644 index 00000000..dc1b2c24 --- /dev/null +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -0,0 +1,76 @@ +package ctbrec.sites.camsoda; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.PlaylistException; + +import ctbrec.AbstractModel; +import ctbrec.recorder.download.StreamSource; +import ctbrec.sites.Site; + +public class CamsodaModel extends AbstractModel { + + @Override + public boolean isOnline() throws IOException, ExecutionException, InterruptedException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { + // TODO Auto-generated method stub + return false; + } + + @Override + public String getOnlineState(boolean failFast) throws IOException, ExecutionException { + // TODO Auto-generated method stub + return null; + } + + @Override + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void invalidateCacheEntries() { + // TODO Auto-generated method stub + + } + + @Override + public void receiveTip(int tokens) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public int[] getStreamResolution(boolean failFast) throws ExecutionException { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean follow() throws IOException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean unfollow() throws IOException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void setSite(Site site) { + // TODO Auto-generated method stub + + } + +} diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java b/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java new file mode 100644 index 00000000..0a4d205b --- /dev/null +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java @@ -0,0 +1,17 @@ +package ctbrec.sites.camsoda; + +import java.util.Collections; +import java.util.List; + +import ctbrec.ui.TabProvider; +import javafx.scene.Scene; +import javafx.scene.control.Tab; + +public class CamsodaTabProvider extends TabProvider { + + @Override + public List getTabs(Scene scene) { + return Collections.emptyList(); + } + +} diff --git a/src/main/java/ctbrec/ui/CamrecApplication.java b/src/main/java/ctbrec/ui/CamrecApplication.java index 8771fdd9..7cbaacf4 100644 --- a/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/src/main/java/ctbrec/ui/CamrecApplication.java @@ -26,6 +26,7 @@ import ctbrec.recorder.LocalRecorder; import ctbrec.recorder.Recorder; import ctbrec.recorder.RemoteRecorder; import ctbrec.sites.Site; +import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.mfc.MyFreeCams; import javafx.application.Application; @@ -59,6 +60,7 @@ public class CamrecApplication extends Application { public void start(Stage primaryStage) throws Exception { sites.add(new Chaturbate()); sites.add(new MyFreeCams()); + sites.add(new Camsoda()); loadConfig(); createHttpClient(); bus = new AsyncEventBus(Executors.newSingleThreadExecutor()); From 6be09079a3a33b5e0446eff250e08e8124029794 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 26 Oct 2018 00:59:14 +0200 Subject: [PATCH 02/42] Improve logging --- src/main/java/ctbrec/ui/ThumbCell.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/ctbrec/ui/ThumbCell.java b/src/main/java/ctbrec/ui/ThumbCell.java index 64342f3c..c50d91f2 100644 --- a/src/main/java/ctbrec/ui/ThumbCell.java +++ b/src/main/java/ctbrec/ui/ThumbCell.java @@ -1,5 +1,6 @@ package ctbrec.ui; +import java.io.EOFException; import java.io.IOException; import java.util.Collections; import java.util.List; @@ -207,8 +208,14 @@ public class ThumbCell extends StackPane { LOG.trace("Removing invalid resolution value for {}", model.getName()); model.invalidateCacheEntries(); } - } catch (ExecutionException | IOException | InterruptedException e1) { + } catch (IOException | InterruptedException e1) { LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1); + } catch(ExecutionException e) { + if(e.getCause() instanceof EOFException) { + LOG.warn("Couldn't update resolution tag for model {}. Playlist empty", model.getName()); + } else { + LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e); + } } finally { ThumbOverviewTab.resolutionProcessing.remove(model); } From 6ca7d43069c351a563988ff4b9d9857a1df51e1b Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 26 Oct 2018 01:03:13 +0200 Subject: [PATCH 03/42] Implemented camsoda --- src/main/java/ctbrec/AbstractModel.java | 5 + .../java/ctbrec/sites/camsoda/Camsoda.java | 11 +- .../ctbrec/sites/camsoda/CamsodaModel.java | 135 +++++++++++++++--- .../sites/camsoda/CamsodaTabProvider.java | 29 +++- .../sites/camsoda/CamsodaUpdateService.java | 97 +++++++++++++ .../sites/chaturbate/ChaturbateModel.java | 5 - 6 files changed, 252 insertions(+), 30 deletions(-) create mode 100644 src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java diff --git a/src/main/java/ctbrec/AbstractModel.java b/src/main/java/ctbrec/AbstractModel.java index 267f9661..d68aa190 100644 --- a/src/main/java/ctbrec/AbstractModel.java +++ b/src/main/java/ctbrec/AbstractModel.java @@ -20,6 +20,11 @@ public abstract class AbstractModel implements Model { private List tags = new ArrayList<>(); private int streamUrlIndex = -1; + @Override + public boolean isOnline() throws IOException, ExecutionException, InterruptedException { + return isOnline(false); + } + @Override public String getUrl() { return url; diff --git a/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/src/main/java/ctbrec/sites/camsoda/Camsoda.java index 4cf02389..c0ce5337 100644 --- a/src/main/java/ctbrec/sites/camsoda/Camsoda.java +++ b/src/main/java/ctbrec/sites/camsoda/Camsoda.java @@ -5,12 +5,13 @@ import java.io.IOException; import ctbrec.Model; import ctbrec.io.HttpClient; import ctbrec.recorder.Recorder; -import ctbrec.sites.Site; +import ctbrec.sites.AbstractSite; import ctbrec.ui.TabProvider; import javafx.scene.Node; -public class Camsoda implements Site { +public class Camsoda extends AbstractSite { + public static final String BASE_URI = "https://www.camsoda.com"; private Recorder recorder; private HttpClient httpClient; @@ -21,7 +22,7 @@ public class Camsoda implements Site { @Override public String getBaseUrl() { - return "https://www.camsoda.com"; + return BASE_URI; } @Override @@ -36,7 +37,7 @@ public class Camsoda implements Site { @Override public TabProvider getTabProvider() { - return new CamsodaTabProvider(); + return new CamsodaTabProvider(this, recorder); } @Override @@ -44,6 +45,7 @@ public class Camsoda implements Site { CamsodaModel model = new CamsodaModel(); model.setName(name); model.setUrl(getBaseUrl() + "/" + name); + model.setSite(this); return model; } @@ -107,5 +109,4 @@ public class Camsoda implements Site { public boolean credentialsAvailable() { return false; } - } diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index dc1b2c24..ede30e93 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -1,46 +1,148 @@ package ctbrec.sites.camsoda; import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.ExecutionException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.iheartradio.m3u8.Encoding; +import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; +import com.iheartradio.m3u8.PlaylistParser; +import com.iheartradio.m3u8.data.MasterPlaylist; +import com.iheartradio.m3u8.data.Playlist; +import com.iheartradio.m3u8.data.PlaylistData; +import com.iheartradio.m3u8.data.StreamInfo; import ctbrec.AbstractModel; import ctbrec.recorder.download.StreamSource; import ctbrec.sites.Site; +import okhttp3.Request; +import okhttp3.Response; public class CamsodaModel extends AbstractModel { - @Override - public boolean isOnline() throws IOException, ExecutionException, InterruptedException { - // TODO Auto-generated method stub - return false; + private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaModel.class); + private String streamUrl; + private Site site; + private List streamSources = null; + private int[] resolution; + private String status = "n/a"; + + public String getStreamUrl() throws IOException { + if(streamUrl == null) { + // load model + loadModel(); + } + return streamUrl; + } + + private void loadModel() throws IOException { + String modelUrl = site.getBaseUrl() + "/api/v1/user/" + getName(); + Request req = new Request.Builder().url(modelUrl).build(); + Response response = site.getHttpClient().execute(req); + try { + JSONObject result = new JSONObject(response.body().string()); + if(result.getBoolean("status")) { + JSONObject chat = result.getJSONObject("user").getJSONObject("chat"); + status = chat.getString("status"); + if(chat.has("edge_servers")) { + String edgeServer = chat.getJSONArray("edge_servers").getString(0); + String streamName = chat.getString("stream_name"); + streamUrl = "https://" + edgeServer + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"; + } + + } else { + throw new IOException("Result was not ok"); + } + } finally { + response.close(); + } } @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { - // TODO Auto-generated method stub - return false; + if(ignoreCache) { + loadModel(); + } + return Objects.equals(status, "online"); } @Override public String getOnlineState(boolean failFast) throws IOException, ExecutionException { - // TODO Auto-generated method stub - return null; + if(failFast) { + return status; + } else { + if(status.equals("n/a")) { + loadModel(); + } + return status; + } } @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { - // TODO Auto-generated method stub - return null; + LOG.trace("Loading master playlist {}", streamUrl); + if(streamSources == null) { + Request req = new Request.Builder().url(streamUrl).build(); + Response response = site.getHttpClient().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(); + PlaylistData playlistData = master.getPlaylists().get(0); + StreamSource streamsource = new StreamSource(); + streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", playlistData.getUri()); + if(playlistData.hasStreamInfo()) { + StreamInfo info = playlistData.getStreamInfo(); + streamsource.bandwidth = info.getBandwidth(); + streamsource.width = info.hasResolution() ? info.getResolution().width : 0; + streamsource.height = info.hasResolution() ? info.getResolution().height : 0; + } else { + streamsource.bandwidth = 0; + streamsource.width = 0; + streamsource.height = 0; + } + streamSources = Collections.singletonList(streamsource); + } finally { + response.close(); + } + } + return streamSources; } @Override public void invalidateCacheEntries() { - // TODO Auto-generated method stub + streamSources = null; + resolution = null; + } + @Override + public int[] getStreamResolution(boolean failFast) throws ExecutionException { + if(resolution != null) { + return resolution; + } else { + if(failFast) { + return new int[] {0,0}; + } else { + try { + List streamSources = getStreamSources(); + StreamSource src = streamSources.get(0); + resolution = new int[] {src.width, src.height}; + return resolution; + } catch (IOException | ParseException | PlaylistException e) { + throw new ExecutionException(e); + } + } + } } @Override @@ -49,12 +151,6 @@ public class CamsodaModel extends AbstractModel { } - @Override - public int[] getStreamResolution(boolean failFast) throws ExecutionException { - // TODO Auto-generated method stub - return null; - } - @Override public boolean follow() throws IOException { // TODO Auto-generated method stub @@ -69,8 +165,11 @@ public class CamsodaModel extends AbstractModel { @Override public void setSite(Site site) { - // TODO Auto-generated method stub + this.site = site; + } + public void setStreamUrl(String streamUrl) { + this.streamUrl = streamUrl; } } diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java b/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java index 0a4d205b..30e9b771 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java @@ -1,17 +1,42 @@ package ctbrec.sites.camsoda; -import java.util.Collections; +import static ctbrec.sites.camsoda.Camsoda.*; + +import java.util.ArrayList; import java.util.List; +import ctbrec.recorder.Recorder; import ctbrec.ui.TabProvider; +import ctbrec.ui.ThumbOverviewTab; import javafx.scene.Scene; import javafx.scene.control.Tab; public class CamsodaTabProvider extends TabProvider { + private Camsoda camsoda; + private Recorder recorder; + + public CamsodaTabProvider(Camsoda camsoda, Recorder recorder) { + this.camsoda = camsoda; + this.recorder = recorder; + } + @Override public List getTabs(Scene scene) { - return Collections.emptyList(); + List tabs = new ArrayList<>(); + tabs.add(createTab("Featured", BASE_URI + "/api/v1/browse/online")); + // ChaturbateFollowedTab followedTab = new ChaturbateFollowedTab("Followed", BASE_URI + "/followed-cams/", chaturbate); + // followedTab.setRecorder(recorder); + // followedTab.setScene(scene); + // tabs.add(followedTab); + return tabs; + } + + private Tab createTab(String title, String url) { + CamsodaUpdateService updateService = new CamsodaUpdateService(url, false, camsoda); + ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, camsoda); + tab.setRecorder(recorder); + return tab; } } diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java b/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java new file mode 100644 index 00000000..df9e6f0d --- /dev/null +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java @@ -0,0 +1,97 @@ +package ctbrec.sites.camsoda; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jetty.util.StringUtil; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Model; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import okhttp3.Response; + +public class CamsodaUpdateService extends PaginatedScheduledService { + + private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaUpdateService.class); + + private String url; + private boolean loginRequired; + private Camsoda camsoda; + int modelsPerPage = 50; + + public CamsodaUpdateService(String url, boolean loginRequired, Camsoda camsoda) { + this.url = url; + this.loginRequired = loginRequired; + this.camsoda = camsoda; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + List models = new ArrayList<>(); + if(loginRequired && StringUtil.isBlank(ctbrec.Config.getInstance().getSettings().username)) { + return models; + } else { + String url = CamsodaUpdateService.this.url; + LOG.debug("Fetching page {}", url); + Request request = new Request.Builder().url(url).build(); + Response response = camsoda.getHttpClient().execute(request, loginRequired); + if (response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.has("status") && json.getBoolean("status")) { + JSONArray results = json.getJSONArray("results"); + for (int i = 0; i < results.length(); i++) { + JSONObject result = results.getJSONObject(i); + if(result.has("tpl")) { + JSONArray tpl = result.getJSONArray("tpl"); + String name = tpl.getString(0); + // int connections = tpl.getInt(2); + // float sortValue = tpl.getFloat(3); + String streamName = tpl.getString(5); + String tsize = tpl.getString(6); + String serverPrefix = tpl.getString(7); + JSONArray edgeServers = result.getJSONArray("edge_servers"); + CamsodaModel model = (CamsodaModel) camsoda.createModel(name); + model.setDescription(tpl.getString(4)); + model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); + long unixtime = System.currentTimeMillis() / 1000; + String preview = "https://thumbs-orig.camsoda.com/thumbs/" + + streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime; + model.setPreview(preview); + //LOG.debug(model.getPreview()); + models.add(model); + // https://vide16-ord.camsoda.com/cam/mp4:kipsyrose-enc6-ord_h264_aac_480p/playlist.m3u8 + // https://enc42-ord.camsoda.com/cam/mp4:elizasmile-enc42-ord_h264_aac_480p/playlist.m3u8 + // https://thumbs-orig.camsoda.com/thumbs/marriednaughtycol-enc35-ord/enc35-ord/340x255/51349794/marriednaughtycol.jpg?cb=51349794 + } else { + //LOG.debug("HÖ? {}", result.toString(2)); + } + } + return models.stream() + .skip( (page-1) * modelsPerPage) + .limit(modelsPerPage) + .collect(Collectors.toList()); + } else { + response.close(); + return models; + } + } else { + int code = response.code(); + response.close(); + throw new IOException("HTTP status " + code); + } + } + } + }; + } + +} diff --git a/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java index 28773232..493486e2 100644 --- a/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java +++ b/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java @@ -38,11 +38,6 @@ public class ChaturbateModel extends AbstractModel { this.site = site; } - @Override - public boolean isOnline() throws IOException, ExecutionException, InterruptedException { - return isOnline(false); - } - @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { StreamInfo info; From e940a81d41eb9978e9539a0ed8c0d385e2bb4e2a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 26 Oct 2018 01:22:49 +0200 Subject: [PATCH 04/42] Fix height of background etc --- src/main/java/ctbrec/ui/ThumbCell.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/ctbrec/ui/ThumbCell.java b/src/main/java/ctbrec/ui/ThumbCell.java index c50d91f2..aeb4d014 100644 --- a/src/main/java/ctbrec/ui/ThumbCell.java +++ b/src/main/java/ctbrec/ui/ThumbCell.java @@ -491,13 +491,13 @@ public class ThumbCell extends StackPane { nameBackground.setWidth(w); nameBackground.setHeight(20); topicBackground.setWidth(w); - topicBackground.setHeight(h-nameBackground.getHeight()); - topic.prefHeight(h-25); - topic.maxHeight(h-25); + topicBackground.setHeight(getHeight()-nameBackground.getHeight()); + topic.prefHeight(getHeight()-25); + topic.maxHeight(getHeight()-25); int margin = 4; topic.maxWidth(w-margin*2); topic.setWrappingWidth(w-margin*2); selectionOverlay.setWidth(w); - selectionOverlay.setHeight(h); + selectionOverlay.setHeight(getHeight()); } } From 60494a09fd18b4382288a20a8f1986472ad3f8bf Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 26 Oct 2018 01:23:07 +0200 Subject: [PATCH 05/42] Sort by camsoda sort value --- src/main/java/ctbrec/sites/camsoda/CamsodaModel.java | 8 ++++++++ .../ctbrec/sites/camsoda/CamsodaUpdateService.java | 10 ++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index ede30e93..976f90c3 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -35,6 +35,7 @@ public class CamsodaModel extends AbstractModel { private List streamSources = null; private int[] resolution; private String status = "n/a"; + private float sortOrder = 0; public String getStreamUrl() throws IOException { if(streamUrl == null) { @@ -172,4 +173,11 @@ public class CamsodaModel extends AbstractModel { this.streamUrl = streamUrl; } + public float getSortOrder() { + return sortOrder; + } + + public void setSortOrder(float sortOrder) { + this.sortOrder = sortOrder; + } } diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java b/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java index df9e6f0d..b28e16db 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java @@ -2,6 +2,7 @@ package ctbrec.sites.camsoda; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -37,9 +38,9 @@ public class CamsodaUpdateService extends PaginatedScheduledService { return new Task>() { @Override public List call() throws IOException { - List models = new ArrayList<>(); + List models = new ArrayList<>(); if(loginRequired && StringUtil.isBlank(ctbrec.Config.getInstance().getSettings().username)) { - return models; + return Collections.emptyList(); } else { String url = CamsodaUpdateService.this.url; LOG.debug("Fetching page {}", url); @@ -55,7 +56,6 @@ public class CamsodaUpdateService extends PaginatedScheduledService { JSONArray tpl = result.getJSONArray("tpl"); String name = tpl.getString(0); // int connections = tpl.getInt(2); - // float sortValue = tpl.getFloat(3); String streamName = tpl.getString(5); String tsize = tpl.getString(6); String serverPrefix = tpl.getString(7); @@ -63,6 +63,7 @@ public class CamsodaUpdateService extends PaginatedScheduledService { CamsodaModel model = (CamsodaModel) camsoda.createModel(name); model.setDescription(tpl.getString(4)); model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); + model.setSortOrder(tpl.getFloat(3)); long unixtime = System.currentTimeMillis() / 1000; String preview = "https://thumbs-orig.camsoda.com/thumbs/" + streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime; @@ -77,12 +78,13 @@ public class CamsodaUpdateService extends PaginatedScheduledService { } } return models.stream() + .sorted((m1,m2) -> (int)(m2.getSortOrder() - m1.getSortOrder())) .skip( (page-1) * modelsPerPage) .limit(modelsPerPage) .collect(Collectors.toList()); } else { response.close(); - return models; + return Collections.emptyList(); } } else { int code = response.code(); From ebbfacd8028c9aba14b2d7206a61f2f402b403b4 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 26 Oct 2018 01:55:08 +0200 Subject: [PATCH 06/42] Complete json parsing --- .../ctbrec/sites/camsoda/CamsodaModel.java | 4 +++ .../sites/camsoda/CamsodaUpdateService.java | 31 +++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index 976f90c3..c5b3483a 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -88,6 +88,10 @@ public class CamsodaModel extends AbstractModel { } } + public void setOnlineState(String state) { + this.status = state; + } + @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { LOG.trace("Loading master playlist {}", streamUrl); diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java b/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java index b28e16db..b5832798 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java @@ -55,7 +55,7 @@ public class CamsodaUpdateService extends PaginatedScheduledService { if(result.has("tpl")) { JSONArray tpl = result.getJSONArray("tpl"); String name = tpl.getString(0); - // int connections = tpl.getInt(2); + // int connections = tpl.getInt(2); String streamName = tpl.getString(5); String tsize = tpl.getString(6); String serverPrefix = tpl.getString(7); @@ -68,13 +68,32 @@ public class CamsodaUpdateService extends PaginatedScheduledService { String preview = "https://thumbs-orig.camsoda.com/thumbs/" + streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime; model.setPreview(preview); - //LOG.debug(model.getPreview()); models.add(model); - // https://vide16-ord.camsoda.com/cam/mp4:kipsyrose-enc6-ord_h264_aac_480p/playlist.m3u8 - // https://enc42-ord.camsoda.com/cam/mp4:elizasmile-enc42-ord_h264_aac_480p/playlist.m3u8 - // https://thumbs-orig.camsoda.com/thumbs/marriednaughtycol-enc35-ord/enc35-ord/340x255/51349794/marriednaughtycol.jpg?cb=51349794 } else { - //LOG.debug("HÖ? {}", result.toString(2)); + LOG.debug("{}", result.toString(2)); + String name = result.getString("username"); + CamsodaModel model = (CamsodaModel) camsoda.createModel(name); + JSONArray edgeServers = result.getJSONArray("edge_servers"); + String streamName = result.getString("stream_name"); + + if(result.has("server_prefix")) { + String serverPrefix = result.getString("server_prefix"); + model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); + + if(result.has("tsize")) { + long unixtime = System.currentTimeMillis() / 1000; + String tsize = result.getString("tsize"); + String preview = "https://thumbs-orig.camsoda.com/thumbs/" + + streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime; + model.setPreview(preview); + } + + model.setSortOrder(result.getFloat("sort_value")); + models.add(model); + if(result.has("status")) { + model.setOnlineState(result.getString("status")); + } + } } } return models.stream() From a31bad74664226002382d8cae71e3d4835c2c9bc Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 26 Oct 2018 16:13:44 +0200 Subject: [PATCH 07/42] Set backbround of ThumbCell to lightgray --- src/main/java/ctbrec/ui/ThumbCell.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/ctbrec/ui/ThumbCell.java b/src/main/java/ctbrec/ui/ThumbCell.java index aeb4d014..c4f35030 100644 --- a/src/main/java/ctbrec/ui/ThumbCell.java +++ b/src/main/java/ctbrec/ui/ThumbCell.java @@ -81,12 +81,12 @@ public class ThumbCell extends StackPane { this.model = model; this.recorder = recorder; recording = recorder.isRecording(model); + this.setStyle("-fx-background-color: lightgray"); iv = new ImageView(); - setImage(model.getPreview()); iv.setSmooth(true); iv.setPreserveRatio(true); - iv.setStyle("-fx-background-color: #000"); + setImage(model.getPreview()); getChildren().add(iv); nameBackground = new Rectangle(); @@ -258,9 +258,9 @@ public class ThumbCell extends StackPane { @Override public void changed(ObservableValue observable, Number oldValue, Number newValue) { if(newValue.doubleValue() == 1.0) { - imgAspectRatio = img.getHeight() / img.getWidth(); - setThumbWidth(Config.getInstance().getSettings().thumbWidth); + //imgAspectRatio = img.getHeight() / img.getWidth(); iv.setImage(img); + setThumbWidth(Config.getInstance().getSettings().thumbWidth); } } }); @@ -480,6 +480,7 @@ public class ThumbCell extends StackPane { public void setThumbWidth(int width) { int height = (int) (width * imgAspectRatio); + setPrefSize(width, height); setSize(width, height); } From d4bff345a158021deb9d77c7e5e20ce50827c0ad Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 26 Oct 2018 23:01:17 +0200 Subject: [PATCH 08/42] Add Camsoda to sites in HttpServer --- src/main/java/ctbrec/recorder/server/HttpServer.java | 6 +++++- src/main/java/ctbrec/sites/camsoda/CamsodaModel.java | 11 ++++++++++- .../java/ctbrec/sites/camsoda/CamsodaTabProvider.java | 2 +- .../ctbrec/sites/camsoda/CamsodaUpdateService.java | 6 +++--- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/java/ctbrec/recorder/server/HttpServer.java b/src/main/java/ctbrec/recorder/server/HttpServer.java index ccf47cde..a83c0025 100644 --- a/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -20,6 +20,7 @@ import ctbrec.Config; import ctbrec.recorder.LocalRecorder; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; +import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.mfc.MyFreeCams; @@ -52,7 +53,9 @@ public class HttpServer { } recorder = new LocalRecorder(config); for (Site site : sites) { - site.init(); + if(site.isEnabled()) { + site.init(); + } } startHttpServer(); } @@ -60,6 +63,7 @@ public class HttpServer { private void createSites() { sites.add(new Chaturbate()); sites.add(new MyFreeCams()); + sites.add(new Camsoda()); } private void addShutdownHook() { diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index c5b3483a..5f0d58bb 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -170,7 +170,16 @@ public class CamsodaModel extends AbstractModel { @Override public void setSite(Site site) { - this.site = site; + if(site instanceof Camsoda) { + this.site = site; + } else { + throw new IllegalArgumentException("Site has to be an instance of Camsoda"); + } + } + + @Override + public Site getSite() { + return site; } public void setStreamUrl(String streamUrl) { diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java b/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java index 30e9b771..190daa89 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java @@ -24,7 +24,7 @@ public class CamsodaTabProvider extends TabProvider { @Override public List getTabs(Scene scene) { List tabs = new ArrayList<>(); - tabs.add(createTab("Featured", BASE_URI + "/api/v1/browse/online")); + tabs.add(createTab("Online", BASE_URI + "/api/v1/browse/online")); // ChaturbateFollowedTab followedTab = new ChaturbateFollowedTab("Followed", BASE_URI + "/followed-cams/", chaturbate); // followedTab.setRecorder(recorder); // followedTab.setScene(scene); diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java b/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java index b5832798..6b745f4a 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java @@ -70,14 +70,14 @@ public class CamsodaUpdateService extends PaginatedScheduledService { model.setPreview(preview); models.add(model); } else { - LOG.debug("{}", result.toString(2)); + //LOG.debug("{}", result.toString(2)); String name = result.getString("username"); CamsodaModel model = (CamsodaModel) camsoda.createModel(name); - JSONArray edgeServers = result.getJSONArray("edge_servers"); - String streamName = result.getString("stream_name"); if(result.has("server_prefix")) { String serverPrefix = result.getString("server_prefix"); + String streamName = result.getString("stream_name"); + JSONArray edgeServers = result.getJSONArray("edge_servers"); model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); if(result.has("tsize")) { From 07864dc10a4bf127d274ce26780b9104baec35bb Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 27 Oct 2018 18:39:57 +0200 Subject: [PATCH 09/42] Add Camsoda tab with upcoming shows --- .../ctbrec/sites/camsoda/CamsodaShowsTab.java | 240 ++++++++++++++++++ .../sites/camsoda/CamsodaTabProvider.java | 1 + 2 files changed, 241 insertions(+) create mode 100644 src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java b/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java new file mode 100644 index 00000000..c85033bf --- /dev/null +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java @@ -0,0 +1,240 @@ +package ctbrec.sites.camsoda; + +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Model; +import ctbrec.recorder.Recorder; +import ctbrec.ui.AutosizeAlert; +import ctbrec.ui.TabSelectionListener; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.concurrent.Task; +import javafx.geometry.Insets; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.Tab; +import javafx.scene.control.TitledPane; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import okhttp3.Request; +import okhttp3.Response; + +public class CamsodaShowsTab extends Tab implements TabSelectionListener { + + private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaShowsTab.class); + + private Camsoda camsoda; + private Recorder recorder; + private VBox showList; + private ProgressIndicator progressIndicator; + + public CamsodaShowsTab(Camsoda camsoda, Recorder recorder) { + this.camsoda = camsoda; + this.recorder = recorder; + createGui(); + } + + private void createGui() { + showList = new VBox(10); + progressIndicator = new ProgressIndicator(); + progressIndicator.setPrefSize(100, 100); + setContent(progressIndicator); + setClosable(false); + setText("Shows"); + } + + @Override + public void selected() { + Task> task = new Task>() { + @Override + protected List call() throws Exception { + String url = camsoda.getBaseUrl() + "/api/v1/user/model_shows"; + Request req = new Request.Builder().url(url).build(); + Response response = camsoda.getHttpClient().execute(req); + if (response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if (json.optInt("success") == 1) { + List boxes = new ArrayList<>(); + JSONArray results = json.getJSONArray("results"); + for (int i = 0; i < results.length(); i++) { + JSONObject result = results.getJSONObject(i); + String modelUrl = camsoda.getBaseUrl() + result.getString("url"); + String name = modelUrl.substring(modelUrl.lastIndexOf('/') + 1); + Model model = camsoda.createModel(name); + ZonedDateTime startTime = parseUtcTime(result.getString("start")); + ZonedDateTime endTime = parseUtcTime(result.getString("end")); + boxes.add(new ShowBox(model, startTime, endTime)); + } + return boxes; + } else { + LOG.error("Couldn't load upcoming camsoda shows. Unexpected response: {}", json.toString()); + showErrorDialog("Oh no!", "Couldn't load upcoming CamSoda shows", "Got an unexpected response from server"); + } + } else { + response.close(); + showErrorDialog("Oh no!", "Couldn't load upcoming CamSoda shows", "Got an unexpected response from server"); + LOG.error("Couldn't load upcoming camsoda shows: {} {}", response.code(), response.message()); + } + return Collections.emptyList(); + } + + private ZonedDateTime parseUtcTime(String string) { + DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + TemporalAccessor ta = formatter.parse(string.replace(" UTC", "")); + Instant instant = Instant.from(ta); + return ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()); + } + + @Override + protected void done() { + super.done(); + Platform.runLater(() -> { + try { + List boxes = get(); + showList.getChildren().clear(); + for (ShowBox showBox : boxes) { + showList.getChildren().add(showBox); + VBox.setMargin(showBox, new Insets(20)); + } + } catch (Exception e) { + LOG.error("Couldn't load upcoming camsoda shows", e); + } + setContent(showList); + }); + } + }; + new Thread(task).start(); + } + + @Override + public void deselected() { + } + + private void showErrorDialog(String title, String head, String msg) { + Platform.runLater(() -> { + Alert alert = new AutosizeAlert(Alert.AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(head); + alert.setContentText(msg); + alert.showAndWait(); + }); + } + + private class ShowBox extends TitledPane { + BorderPane root = new BorderPane(); + int thumbSize = 200; + public ShowBox(Model model, ZonedDateTime startTime, ZonedDateTime endTime) { + setText(model.getName()); + setPrefHeight(268); + setContent(root); + + ImageView thumb = new ImageView(); + thumb.setPreserveRatio(true); + thumb.setFitHeight(thumbSize); + loadImage(model, thumb); + root.setLeft(new ProgressIndicator()); + BorderPane.setMargin(thumb, new Insets(10, 30, 10, 10)); + + DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM); + GridPane grid = new GridPane(); + + grid.add(createLabel("Start", true), 0, 0); + grid.add(createLabel(formatter.format(startTime), false), 1, 0); + grid.add(createLabel("End", true), 0, 1); + grid.add(createLabel(formatter.format(endTime), false), 1, 1); + Button record = new Button("Record Model"); + grid.add(record, 1, 2); + GridPane.setMargin(record, new Insets(10)); + root.setCenter(grid); + + record.setOnAction((evt) -> { + setCursor(Cursor.WAIT); + new Thread(() -> { + try { + recorder.startRecording(model); + } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) { + showErrorDialog("Oh no!", "Couldn't add model to the recorder", "Recorder error: " + e.getMessage()); + } finally { + Platform.runLater(() ->{ + setCursor(Cursor.DEFAULT); + }); + } + }).start(); + }); + + loadImage(model, thumb); + } + + private void loadImage(Model model, ImageView thumb) { + new Thread(() -> { + try { + String url = camsoda.getBaseUrl() + "/api/v1/user/" + model.getName(); + Request detailRequest = new Request.Builder().url(url).build(); + Response resp = camsoda.getHttpClient().execute(detailRequest); + if (resp.isSuccessful()) { + JSONObject json = new JSONObject(resp.body().string()); + if(json.optBoolean("status") && json.has("user")) { + JSONObject user = json.getJSONObject("user"); + if(user.has("settings")) { + JSONObject settings = user.getJSONObject("settings"); + if(settings.has("offline_picture")) { + Platform.runLater(() -> { + String imageUrl = settings.getString("offline_picture"); + Image img = new Image(imageUrl, 1000, thumbSize, true, true, true); + img.progressProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, Number oldValue, Number newValue) { + if(newValue.doubleValue() == 1.0) { + thumb.setImage(img); + root.setLeft(thumb); + } + } + }); + + }); + } + } + } + } + resp.close(); + } catch(Exception e) { + LOG.error("Couldn't load model details", e); + } + }).start(); + } + + private Node createLabel(String string, boolean bold) { + Label label = new Label(string); + label.setPadding(new Insets(10)); + Font def = Font.getDefault(); + label.setFont(Font.font(def.getFamily(), bold ? FontWeight.BOLD : FontWeight.NORMAL, 16)); + return label; + } + } +} diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java b/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java index 190daa89..bb431001 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java @@ -25,6 +25,7 @@ public class CamsodaTabProvider extends TabProvider { public List getTabs(Scene scene) { List tabs = new ArrayList<>(); tabs.add(createTab("Online", BASE_URI + "/api/v1/browse/online")); + tabs.add(new CamsodaShowsTab(camsoda, recorder)); // ChaturbateFollowedTab followedTab = new ChaturbateFollowedTab("Followed", BASE_URI + "/followed-cams/", chaturbate); // followedTab.setRecorder(recorder); // followedTab.setScene(scene); From 7442ddd3e4de4a6422c314438a515fed5507054e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 27 Oct 2018 18:50:29 +0200 Subject: [PATCH 10/42] Show thumb, if offline_picture is not available Tweak the insets --- .../ctbrec/sites/camsoda/CamsodaShowsTab.java | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java b/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java index c85033bf..53972c87 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java @@ -61,7 +61,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { } private void createGui() { - showList = new VBox(10); + showList = new VBox(); progressIndicator = new ProgressIndicator(); progressIndicator.setPrefSize(100, 100); setContent(progressIndicator); @@ -120,7 +120,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { showList.getChildren().clear(); for (ShowBox showBox : boxes) { showList.getChildren().add(showBox); - VBox.setMargin(showBox, new Insets(20)); + VBox.setMargin(showBox, new Insets(20, 20, 0, 20)); } } catch (Exception e) { LOG.error("Couldn't load upcoming camsoda shows", e); @@ -147,8 +147,10 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { } private class ShowBox extends TitledPane { + BorderPane root = new BorderPane(); int thumbSize = 200; + public ShowBox(Model model, ZonedDateTime startTime, ZonedDateTime endTime) { setText(model.getName()); setPrefHeight(268); @@ -203,22 +205,25 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { JSONObject user = json.getJSONObject("user"); if(user.has("settings")) { JSONObject settings = user.getJSONObject("settings"); + String imageUrl; if(settings.has("offline_picture")) { - Platform.runLater(() -> { - String imageUrl = settings.getString("offline_picture"); - Image img = new Image(imageUrl, 1000, thumbSize, true, true, true); - img.progressProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, Number oldValue, Number newValue) { - if(newValue.doubleValue() == 1.0) { - thumb.setImage(img); - root.setLeft(thumb); - } - } - }); - - }); + imageUrl = settings.getString("offline_picture"); + } else { + imageUrl = "https:" + user.getString("thumb"); } + Platform.runLater(() -> { + Image img = new Image(imageUrl, 1000, thumbSize, true, true, true); + img.progressProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue observable, Number oldValue, Number newValue) { + if(newValue.doubleValue() == 1.0) { + thumb.setImage(img); + root.setLeft(thumb); + } + } + }); + + }); } } } From 8fd09fd521bb347ae78841db27a63131f076a32d Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 28 Oct 2018 21:23:58 +0100 Subject: [PATCH 11/42] Implemented followed tab for CamSoda --- src/main/java/ctbrec/Settings.java | 2 + .../java/ctbrec/sites/camsoda/Camsoda.java | 87 +++++++++++++++---- .../sites/camsoda/CamsodaFollowedTab.java | 76 ++++++++++++++++ .../camsoda/CamsodaFollowedUpdateService.java | 73 ++++++++++++++++ .../sites/camsoda/CamsodaHttpClient.java | 38 ++++++++ .../ctbrec/sites/camsoda/CamsodaModel.java | 86 +++++++++++------- .../ctbrec/sites/camsoda/CamsodaShowsTab.java | 16 ++-- .../sites/camsoda/CamsodaTabProvider.java | 8 +- src/main/java/ctbrec/ui/JavaFxModel.java | 6 +- src/main/java/ctbrec/ui/TokenLabel.java | 10 ++- 10 files changed, 340 insertions(+), 62 deletions(-) create mode 100644 src/main/java/ctbrec/sites/camsoda/CamsodaFollowedTab.java create mode 100644 src/main/java/ctbrec/sites/camsoda/CamsodaFollowedUpdateService.java create mode 100644 src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java diff --git a/src/main/java/ctbrec/Settings.java b/src/main/java/ctbrec/Settings.java index 97b8bcd4..d1f41691 100644 --- a/src/main/java/ctbrec/Settings.java +++ b/src/main/java/ctbrec/Settings.java @@ -24,6 +24,8 @@ public class Settings { public String password = ""; // chaturbate password TODO maybe rename this onetime public String mfcUsername = ""; public String mfcPassword = ""; + public String camsodaUsername = ""; + public String camsodaPassword = ""; public String lastDownloadDir = ""; public List models = new ArrayList(); diff --git a/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/src/main/java/ctbrec/sites/camsoda/Camsoda.java index c0ce5337..c03139d5 100644 --- a/src/main/java/ctbrec/sites/camsoda/Camsoda.java +++ b/src/main/java/ctbrec/sites/camsoda/Camsoda.java @@ -2,12 +2,26 @@ package ctbrec.sites.camsoda; import java.io.IOException; +import org.json.JSONObject; + +import ctbrec.Config; import ctbrec.Model; import ctbrec.io.HttpClient; import ctbrec.recorder.Recorder; import ctbrec.sites.AbstractSite; +import ctbrec.ui.DesktopIntergation; +import ctbrec.ui.SettingsTab; import ctbrec.ui.TabProvider; +import javafx.geometry.Insets; import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import okhttp3.Request; +import okhttp3.Response; public class Camsoda extends AbstractSite { @@ -27,7 +41,7 @@ public class Camsoda extends AbstractSite { @Override public String getAffiliateLink() { - return ""; + return BASE_URI; } @Override @@ -51,7 +65,26 @@ public class Camsoda extends AbstractSite { @Override public Integer getTokenBalance() throws IOException { - return 0; + String username = Config.getInstance().getSettings().camsodaUsername; + if (username == null || username.trim().isEmpty()) { + throw new IOException("Not logged in"); + } + + String url = BASE_URI + "/api/v1/user/" + username; + Request request = new Request.Builder().url(url).build(); + Response response = getHttpClient().execute(request, true); + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.has("user")) { + JSONObject user = json.getJSONObject("user"); + if(user.has("tokens")) { + return user.getInt("tokens"); + } + } + } else { + throw new IOException(response.code() + " " + response.message()); + } + throw new RuntimeException("Tokens not found in response"); } @Override @@ -61,23 +94,19 @@ public class Camsoda extends AbstractSite { @Override public void login() throws IOException { - httpClient.login(); + getHttpClient().login(); } @Override public HttpClient getHttpClient() { + if(httpClient == null) { + httpClient = new CamsodaHttpClient(); + } return httpClient; } @Override public void init() throws IOException { - httpClient = new HttpClient() { - @Override - public boolean login() throws IOException { - return false; - } - - }; } @Override @@ -87,12 +116,12 @@ public class Camsoda extends AbstractSite { @Override public boolean supportsTips() { - return false; + return true; } @Override public boolean supportsFollow() { - return false; + return true; } @Override @@ -101,12 +130,38 @@ public class Camsoda extends AbstractSite { } @Override - public Node getConfigurationGui() { - return null; + public boolean credentialsAvailable() { + String username = Config.getInstance().getSettings().camsodaUsername; + return username != null && !username.trim().isEmpty(); } @Override - public boolean credentialsAvailable() { - return false; + public Node getConfigurationGui() { + GridPane layout = SettingsTab.createGridLayout(); + layout.add(new Label("CamSoda User"), 0, 0); + TextField username = new TextField(Config.getInstance().getSettings().camsodaUsername); + username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().camsodaUsername = username.getText()); + GridPane.setFillWidth(username, true); + GridPane.setHgrow(username, Priority.ALWAYS); + GridPane.setColumnSpan(username, 2); + layout.add(username, 1, 0); + + layout.add(new Label("CamSoda Password"), 0, 1); + PasswordField password = new PasswordField(); + password.setText(Config.getInstance().getSettings().camsodaPassword); + password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().camsodaPassword = password.getText()); + GridPane.setFillWidth(password, true); + GridPane.setHgrow(password, Priority.ALWAYS); + GridPane.setColumnSpan(password, 2); + layout.add(password, 1, 1); + + Button createAccount = new Button("Create new Account"); + createAccount.setOnAction((e) -> DesktopIntergation.open(getAffiliateLink())); + layout.add(createAccount, 1, 2); + GridPane.setColumnSpan(createAccount, 2); + GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + return layout; } } diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedTab.java b/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedTab.java new file mode 100644 index 00000000..8ef31c6f --- /dev/null +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedTab.java @@ -0,0 +1,76 @@ +package ctbrec.sites.camsoda; + +import ctbrec.ui.FollowedTab; +import ctbrec.ui.ThumbOverviewTab; +import javafx.concurrent.WorkerStateEvent; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.Label; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; + +public class CamsodaFollowedTab extends ThumbOverviewTab implements FollowedTab { + private Label status; + boolean showOnline = true; + + public CamsodaFollowedTab(String title, Camsoda camsoda) { + super(title, new CamsodaFollowedUpdateService(camsoda), camsoda); + status = new Label("Logging in..."); + grid.getChildren().add(status); + } + + @Override + protected void createGui() { + super.createGui(); + addOnlineOfflineSelector(); + } + + private void addOnlineOfflineSelector() { + ToggleGroup group = new ToggleGroup(); + RadioButton online = new RadioButton("online"); + online.setToggleGroup(group); + RadioButton offline = new RadioButton("offline"); + offline.setToggleGroup(group); + pagination.getChildren().add(online); + pagination.getChildren().add(offline); + HBox.setMargin(online, new Insets(5, 5, 5, 40)); + HBox.setMargin(offline, new Insets(5, 5, 5, 5)); + online.setSelected(true); + group.selectedToggleProperty().addListener((e) -> { + queue.clear(); + ((CamsodaFollowedUpdateService)updateService).showOnline(online.isSelected()); + updateService.restart(); + }); + } + + @Override + protected void onSuccess() { + grid.getChildren().remove(status); + super.onSuccess(); + } + + @Override + protected void onFail(WorkerStateEvent event) { + status.setText("Login failed"); + super.onFail(event); + } + + @Override + public void selected() { + status.setText("Logging in..."); + super.selected(); + } + + public void setScene(Scene scene) { + scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (this.isSelected()) { + if (event.getCode() == KeyCode.DELETE) { + follow(selectedThumbCells, false); + } + } + }); + } +} diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedUpdateService.java b/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedUpdateService.java new file mode 100644 index 00000000..e9577906 --- /dev/null +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedUpdateService.java @@ -0,0 +1,73 @@ +package ctbrec.sites.camsoda; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import org.json.JSONArray; +import org.json.JSONObject; + +import ctbrec.Model; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import okhttp3.Response; + +public class CamsodaFollowedUpdateService extends PaginatedScheduledService { + private Camsoda camsoda; + private boolean showOnline = true; + + public CamsodaFollowedUpdateService(Camsoda camsoda) { + this.camsoda = camsoda; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + List models = new ArrayList<>(); + String url = camsoda.getBaseUrl() + "/api/v1/user/current"; + Request request = new Request.Builder().url(url).build(); + Response response = camsoda.getHttpClient().execute(request, true); + if (response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.has("status") && json.getBoolean("status")) { + JSONObject user = json.getJSONObject("user"); + JSONArray following = user.getJSONArray("following"); + for (int i = 0; i < following.length(); i++) { + JSONObject m = following.getJSONObject(i); + CamsodaModel model = (CamsodaModel) camsoda.createModel(m.getString("followname")); + boolean online = m.getInt("online") == 1; + model.setOnlineState(online ? "offline" : "online"); + model.setPreview("https://md.camsoda.com/thumbs/" + model.getName() + ".jpg"); + models.add(model); + } + return models.stream() + .filter((m) -> { + try { + return m.isOnline() == showOnline; + } catch (IOException | ExecutionException | InterruptedException e) { + return false; + } + }).collect(Collectors.toList()); + } else { + response.close(); + return Collections.emptyList(); + } + } else { + int code = response.code(); + response.close(); + throw new IOException("HTTP status " + code); + } + } + }; + } + + void showOnline(boolean online) { + this.showOnline = online; + } +} diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java b/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java new file mode 100644 index 00000000..76e3b98b --- /dev/null +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java @@ -0,0 +1,38 @@ +package ctbrec.sites.camsoda; + +import java.io.IOException; + +import org.json.JSONObject; + +import ctbrec.Config; +import ctbrec.io.HttpClient; +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.Response; + +public class CamsodaHttpClient extends HttpClient { + + @Override + public boolean login() throws IOException { + String url = Camsoda.BASE_URI + "/api/v1/auth/login"; + FormBody body = new FormBody.Builder() + .add("username", Config.getInstance().getSettings().camsodaUsername) + .add("password", Config.getInstance().getSettings().camsodaPassword) + .build(); + Request request = new Request.Builder() + .url(url) + .post(body) + .build(); + Response response = execute(request); + if(response.isSuccessful()) { + JSONObject resp = new JSONObject(response.body().string()); + if(resp.has("error")) { + throw new IOException(resp.getString("error")); + } else { + return true; + } + } else { + throw new IOException(response.code() + " " + response.message()); + } + } +} diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index 5f0d58bb..fcce75ae 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -25,6 +25,7 @@ import ctbrec.AbstractModel; import ctbrec.recorder.download.StreamSource; import ctbrec.sites.Site; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; public class CamsodaModel extends AbstractModel { @@ -94,32 +95,30 @@ public class CamsodaModel extends AbstractModel { @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { - LOG.trace("Loading master playlist {}", streamUrl); - if(streamSources == null) { - Request req = new Request.Builder().url(streamUrl).build(); - Response response = site.getHttpClient().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(); - PlaylistData playlistData = master.getPlaylists().get(0); - StreamSource streamsource = new StreamSource(); - streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", playlistData.getUri()); - if(playlistData.hasStreamInfo()) { - StreamInfo info = playlistData.getStreamInfo(); - streamsource.bandwidth = info.getBandwidth(); - streamsource.width = info.hasResolution() ? info.getResolution().width : 0; - streamsource.height = info.hasResolution() ? info.getResolution().height : 0; - } else { - streamsource.bandwidth = 0; - streamsource.width = 0; - streamsource.height = 0; - } - streamSources = Collections.singletonList(streamsource); - } finally { - response.close(); + LOG.debug("Loading master playlist {}", streamUrl); + Request req = new Request.Builder().url(streamUrl).build(); + Response response = site.getHttpClient().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(); + PlaylistData playlistData = master.getPlaylists().get(0); + StreamSource streamsource = new StreamSource(); + streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", playlistData.getUri()); + if(playlistData.hasStreamInfo()) { + StreamInfo info = playlistData.getStreamInfo(); + streamsource.bandwidth = info.getBandwidth(); + streamsource.width = info.hasResolution() ? info.getResolution().width : 0; + streamsource.height = info.hasResolution() ? info.getResolution().height : 0; + } else { + streamsource.bandwidth = 0; + streamsource.width = 0; + streamsource.height = 0; } + streamSources = Collections.singletonList(streamsource); + } finally { + response.close(); } return streamSources; } @@ -153,18 +152,47 @@ public class CamsodaModel extends AbstractModel { @Override public void receiveTip(int tokens) throws IOException { // TODO Auto-generated method stub - + /* + sendTip: function(i, a, r, o, c, d) { + if (!s.isAuthenticated()) return s.showRegister(), t.when(!1); + var u = t.defer(); + return e.post("/api/v1/tip/" + i, { + amount: a, + comment: o, + type: r, + app_data: c, + source_id: d + }).then(function(e) { + 1 == e.data.status ? (s.currentUser.tokens = e.data.total, void 0 != e.data.tipped_performer_last_24hrs && e.data.tipped_performer_last_24hrs >= 25 && (n.$emit("local.allowed_to_rate"), 0 == n.allowedToRate && (n.allowedToRate = !0, l.pop("info", "Voting Unlocked", "You tipped " + i + " 25 tokens in the past 24 hours, you may now vote!"))), u.resolve(e.data)) : (l.pop("error", e.data.error, e.data.message), u.reject(e.data)) + }), u.promise + }, + */ } @Override public boolean follow() throws IOException { - // TODO Auto-generated method stub - return false; + String url = Camsoda.BASE_URI + "/api/v1/follow/" + getName(); + //RequestBody body = new FormBody.Builder().build(); + LOG.debug("Sending follow request {}", url); + Request request = new Request.Builder() + .url(url) + .post(RequestBody.create(null, "")) + .addHeader("Content-Lentgh", "0") + .addHeader("Referer", Camsoda.BASE_URI + '/' + getName()) + .build(); + Response resp = site.getHttpClient().execute(request, true); + if (resp.isSuccessful()) { + System.out.println(resp.body().string()); + return true; + } else { + resp.close(); + throw new IOException("HTTP status " + resp.code() + " " + resp.message()); + } } @Override public boolean unfollow() throws IOException { - // TODO Auto-generated method stub + // TODO /api/v1/unfollow/" + n.slug return false; } diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java b/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java index 53972c87..1eceeae3 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java @@ -33,13 +33,13 @@ import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.control.TitledPane; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.GridPane; -import javafx.scene.layout.VBox; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import okhttp3.Request; @@ -51,7 +51,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { private Camsoda camsoda; private Recorder recorder; - private VBox showList; + private GridPane showList; private ProgressIndicator progressIndicator; public CamsodaShowsTab(Camsoda camsoda, Recorder recorder) { @@ -61,7 +61,10 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { } private void createGui() { - showList = new VBox(); + showList = new GridPane(); + showList.setPadding(new Insets(5)); + showList.setHgap(5); + showList.setVgap(5); progressIndicator = new ProgressIndicator(); progressIndicator.setPrefSize(100, 100); setContent(progressIndicator); @@ -118,14 +121,15 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { try { List boxes = get(); showList.getChildren().clear(); + int index = 0; for (ShowBox showBox : boxes) { - showList.getChildren().add(showBox); - VBox.setMargin(showBox, new Insets(20, 20, 0, 20)); + showList.add(showBox, index%2, index++/2); + GridPane.setMargin(showBox, new Insets(20, 20, 0, 20)); } } catch (Exception e) { LOG.error("Couldn't load upcoming camsoda shows", e); } - setContent(showList); + setContent(new ScrollPane(showList)); }); } }; diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java b/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java index bb431001..9aa552a7 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java @@ -25,11 +25,11 @@ public class CamsodaTabProvider extends TabProvider { public List getTabs(Scene scene) { List tabs = new ArrayList<>(); tabs.add(createTab("Online", BASE_URI + "/api/v1/browse/online")); + CamsodaFollowedTab followedTab = new CamsodaFollowedTab("Followed", camsoda); + followedTab.setRecorder(recorder); + followedTab.setScene(scene); + tabs.add(followedTab); tabs.add(new CamsodaShowsTab(camsoda, recorder)); - // ChaturbateFollowedTab followedTab = new ChaturbateFollowedTab("Followed", BASE_URI + "/followed-cams/", chaturbate); - // followedTab.setRecorder(recorder); - // followedTab.setScene(scene); - // tabs.add(followedTab); return tabs; } diff --git a/src/main/java/ctbrec/ui/JavaFxModel.java b/src/main/java/ctbrec/ui/JavaFxModel.java index 1ede0c64..c713020c 100644 --- a/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/src/main/java/ctbrec/ui/JavaFxModel.java @@ -2,7 +2,6 @@ package ctbrec.ui; import java.io.IOException; import java.util.List; -import java.util.Objects; import java.util.concurrent.ExecutionException; import com.iheartradio.m3u8.ParseException; @@ -20,14 +19,13 @@ import javafx.beans.property.SimpleBooleanProperty; */ public class JavaFxModel extends AbstractModel { private transient BooleanProperty onlineProperty = new SimpleBooleanProperty(); - private Model delegate; public JavaFxModel(Model delegate) { this.delegate = delegate; try { - onlineProperty.set(Objects.equals("public", delegate.getOnlineState(true))); - } catch (IOException | ExecutionException e) {} + onlineProperty.set(delegate.isOnline()); + } catch (IOException | ExecutionException | InterruptedException e) {} } @Override diff --git a/src/main/java/ctbrec/ui/TokenLabel.java b/src/main/java/ctbrec/ui/TokenLabel.java index a3123a34..30471ab8 100644 --- a/src/main/java/ctbrec/ui/TokenLabel.java +++ b/src/main/java/ctbrec/ui/TokenLabel.java @@ -13,6 +13,7 @@ import ctbrec.sites.Site; import javafx.application.Platform; import javafx.concurrent.Task; import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; public class TokenLabel extends Label { @@ -26,10 +27,10 @@ public class TokenLabel extends Label { CamrecApplication.bus.register(new Object() { @Subscribe public void tokensUpdates(Map e) { - if(Objects.equals("tokens", e.get("event"))) { + if (Objects.equals("tokens", e.get("event"))) { tokens = (int) e.get("amount"); updateText(); - } else if(Objects.equals("tokens.sent", e.get("event"))) { + } else if (Objects.equals("tokens.sent", e.get("event"))) { int _tokens = (int) e.get("amount"); tokens -= _tokens; updateText(); @@ -70,7 +71,10 @@ public class TokenLabel extends Label { update(tokens); } catch (InterruptedException | ExecutionException e) { LOG.error("Couldn't retrieve account balance", e); - Platform.runLater(() -> setText("Tokens: error")); + Platform.runLater(() -> { + setText("Tokens: error"); + setTooltip(new Tooltip(e.getMessage())); + }); } } }; From dc6b514d8f03a8e0641614e93b00f41f897e61d8 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 29 Oct 2018 13:49:04 +0100 Subject: [PATCH 12/42] Disable follow/unfollow for the time being --- .../sites/camsoda/CamsodaFollowedTab.java | 6 ++- .../camsoda/CamsodaFollowedUpdateService.java | 2 +- .../ctbrec/sites/camsoda/CamsodaModel.java | 44 +++++++++++-------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedTab.java b/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedTab.java index 8ef31c6f..bc95672b 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedTab.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedTab.java @@ -54,7 +54,11 @@ public class CamsodaFollowedTab extends ThumbOverviewTab implements FollowedTab @Override protected void onFail(WorkerStateEvent event) { - status.setText("Login failed"); + String msg = ""; + if (event.getSource().getException() != null) { + msg = ": " + event.getSource().getException().getMessage(); + } + status.setText("Login failed" + msg); super.onFail(event); } diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedUpdateService.java b/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedUpdateService.java index e9577906..20c4bbe0 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedUpdateService.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaFollowedUpdateService.java @@ -42,7 +42,7 @@ public class CamsodaFollowedUpdateService extends PaginatedScheduledService { JSONObject m = following.getJSONObject(i); CamsodaModel model = (CamsodaModel) camsoda.createModel(m.getString("followname")); boolean online = m.getInt("online") == 1; - model.setOnlineState(online ? "offline" : "online"); + model.setOnlineState(online ? "online" : "offline"); model.setPreview("https://md.camsoda.com/thumbs/" + model.getName() + ".jpg"); models.add(model); } diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index fcce75ae..1a6a6269 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -25,7 +25,6 @@ import ctbrec.AbstractModel; import ctbrec.recorder.download.StreamSource; import ctbrec.sites.Site; import okhttp3.Request; -import okhttp3.RequestBody; import okhttp3.Response; public class CamsodaModel extends AbstractModel { @@ -171,29 +170,36 @@ public class CamsodaModel extends AbstractModel { @Override public boolean follow() throws IOException { - String url = Camsoda.BASE_URI + "/api/v1/follow/" + getName(); - //RequestBody body = new FormBody.Builder().build(); - LOG.debug("Sending follow request {}", url); - Request request = new Request.Builder() - .url(url) - .post(RequestBody.create(null, "")) - .addHeader("Content-Lentgh", "0") - .addHeader("Referer", Camsoda.BASE_URI + '/' + getName()) - .build(); - Response resp = site.getHttpClient().execute(request, true); - if (resp.isSuccessful()) { - System.out.println(resp.body().string()); - return true; - } else { - resp.close(); - throw new IOException("HTTP status " + resp.code() + " " + resp.message()); - } + // FIXME follow and unfollow don't work yet, because the HTTP requests need to have + // the cross-site scripting prevention header (e.g. X-CSRF-Token: YDixu6rFg3ovqos9C1YuYpsVd7bxuXlpNnZnelKG), + // but i didn't find out yet, how to get the token (cookie, hmac calc, ?!?) + throw new RuntimeException("Not implemented, yet"); + + // String url = Camsoda.BASE_URI + "/api/v1/follow/" + getName(); + // //RequestBody body = new FormBody.Builder().build(); + // LOG.debug("Sending follow request {}", url); + // Request request = new Request.Builder() + // .url(url) + // .post(RequestBody.create(null, "")) + // .addHeader("Referer", Camsoda.BASE_URI + '/' + getName()) + // .addHeader("User-Agent", " Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0") + // .addHeader("Accept", " application/json, text/plain, */*") + // .addHeader("Accept-Language", " de,en-US;q=0.7,en;q=0.3") + // .build(); + // Response resp = site.getHttpClient().execute(request, false); + // if (resp.isSuccessful()) { + // System.out.println(resp.body().string()); + // return true; + // } else { + // resp.close(); + // throw new IOException("HTTP status " + resp.code() + " " + resp.message()); + // } } @Override public boolean unfollow() throws IOException { // TODO /api/v1/unfollow/" + n.slug - return false; + throw new RuntimeException("Not implemented, yet"); } @Override From 1b0b5f18a4b173d44dbe659b88b70ec3bbf41377 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 30 Oct 2018 18:21:13 +0100 Subject: [PATCH 13/42] Add assembly descriptor for macOS --- ctbrec-macos.sh | 9 +++++++++ pom.xml | 3 ++- server-macos.sh | 9 +++++++++ src/assembly/macos-jre.xml | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100755 ctbrec-macos.sh create mode 100755 server-macos.sh create mode 100644 src/assembly/macos-jre.xml diff --git a/ctbrec-macos.sh b/ctbrec-macos.sh new file mode 100755 index 00000000..f28903cc --- /dev/null +++ b/ctbrec-macos.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +DIR=$(dirname $0) +pushd $DIR +JAVA_HOME="$DIR/jre/Contents/Home" +JAVA="$JAVA_HOME/bin/java" +$JAVA -version +$JAVA -cp ${name.final}.jar ctbrec.ui.Launcher +popd \ No newline at end of file diff --git a/pom.xml b/pom.xml index f38886a0..77b1df4a 100644 --- a/pom.xml +++ b/pom.xml @@ -66,6 +66,7 @@ src/assembly/win64-jre.xml src/assembly/win32-jre.xml src/assembly/linux.xml + src/assembly/macos-jre.xml @@ -77,7 +78,7 @@ 1.7.22 - l4j-clui + l4j-win package launch4j diff --git a/server-macos.sh b/server-macos.sh new file mode 100755 index 00000000..7ab059e4 --- /dev/null +++ b/server-macos.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +DIR=$(dirname $0) +pushd $DIR +JAVA_HOME="$DIR/jre/Contents/Home" +JAVA="$JAVA_HOME/bin/java" +$JAVA -version +$JAVA -cp ${name.final}.jar -Dctbrec.config=server.json ctbrec.recorder.server.HttpServer +popd diff --git a/src/assembly/macos-jre.xml b/src/assembly/macos-jre.xml new file mode 100644 index 00000000..c8a1c261 --- /dev/null +++ b/src/assembly/macos-jre.xml @@ -0,0 +1,34 @@ + + + macos-jre + + zip + + false + + + ${project.basedir}/ctbrec-macos.sh + ctbrec + true + + + ${project.basedir}/server-macos.sh + ctbrec + true + + + ${project.build.directory}/${name.final}.jar + ctbrec + + + + + jre/jre1.8.0_192_macos + + **/* + + ctbrec/jre + false + + + From b19ea01ce7521bd2095753f25fe8ee601c48bd38 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 30 Oct 2018 18:24:40 +0100 Subject: [PATCH 14/42] Only do initial login if credentials are available --- src/main/java/ctbrec/sites/camsoda/Camsoda.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/src/main/java/ctbrec/sites/camsoda/Camsoda.java index c03139d5..17614643 100644 --- a/src/main/java/ctbrec/sites/camsoda/Camsoda.java +++ b/src/main/java/ctbrec/sites/camsoda/Camsoda.java @@ -94,7 +94,9 @@ public class Camsoda extends AbstractSite { @Override public void login() throws IOException { - getHttpClient().login(); + if(credentialsAvailable()) { + getHttpClient().login(); + } } @Override From 3d245d65158dfe615f42cd4f1d543fab63d51977 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 30 Oct 2018 19:07:10 +0100 Subject: [PATCH 15/42] Add buttons to follow and open in browser to show tab --- .../ctbrec/sites/camsoda/CamsodaShowsTab.java | 72 +++++++++++++------ 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java b/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java index 1eceeae3..c8ba8a2d 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java @@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory; import ctbrec.Model; import ctbrec.recorder.Recorder; import ctbrec.ui.AutosizeAlert; +import ctbrec.ui.DesktopIntergation; import ctbrec.ui.TabSelectionListener; import javafx.application.Platform; import javafx.beans.value.ChangeListener; @@ -123,7 +124,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { showList.getChildren().clear(); int index = 0; for (ShowBox showBox : boxes) { - showList.add(showBox, index%2, index++/2); + showList.add(showBox, index % 2, index++ / 2); GridPane.setMargin(showBox, new Insets(20, 20, 0, 20)); } } catch (Exception e) { @@ -175,26 +176,53 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { grid.add(createLabel("End", true), 0, 1); grid.add(createLabel(formatter.format(endTime), false), 1, 1); Button record = new Button("Record Model"); + record.setOnAction((evt) -> record(model)); grid.add(record, 1, 2); GridPane.setMargin(record, new Insets(10)); + Button follow = new Button("Follow"); + follow.setOnAction((evt) -> follow(model)); + grid.add(follow, 1, 3); + GridPane.setMargin(follow, new Insets(10)); + Button openInBrowser = new Button("Open in Browser"); + openInBrowser.setOnAction((evt) -> DesktopIntergation.open(model.getUrl())); + grid.add(openInBrowser, 1, 4); + GridPane.setMargin(openInBrowser, new Insets(10)); root.setCenter(grid); - - record.setOnAction((evt) -> { - setCursor(Cursor.WAIT); - new Thread(() -> { - try { - recorder.startRecording(model); - } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) { - showErrorDialog("Oh no!", "Couldn't add model to the recorder", "Recorder error: " + e.getMessage()); - } finally { - Platform.runLater(() ->{ - setCursor(Cursor.DEFAULT); - }); - } - }).start(); - }); - loadImage(model, thumb); + + record.prefWidthProperty().bind(openInBrowser.widthProperty()); + follow.prefWidthProperty().bind(openInBrowser.widthProperty()); + } + + private void follow(Model model) { + setCursor(Cursor.WAIT); + new Thread(() -> { + try { + model.follow(); + } catch (Exception e) { + LOG.error("Couldn't follow model {}", model, e); + showErrorDialog("Oh no!", "Couldn't follow model", e.getMessage()); + } finally { + Platform.runLater(() -> { + setCursor(Cursor.DEFAULT); + }); + } + }).start(); + } + + private void record(Model model) { + setCursor(Cursor.WAIT); + new Thread(() -> { + try { + recorder.startRecording(model); + } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) { + showErrorDialog("Oh no!", "Couldn't add model to the recorder", "Recorder error: " + e.getMessage()); + } finally { + Platform.runLater(() -> { + setCursor(Cursor.DEFAULT); + }); + } + }).start(); } private void loadImage(Model model, ImageView thumb) { @@ -205,12 +233,12 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { Response resp = camsoda.getHttpClient().execute(detailRequest); if (resp.isSuccessful()) { JSONObject json = new JSONObject(resp.body().string()); - if(json.optBoolean("status") && json.has("user")) { + if (json.optBoolean("status") && json.has("user")) { JSONObject user = json.getJSONObject("user"); - if(user.has("settings")) { + if (user.has("settings")) { JSONObject settings = user.getJSONObject("settings"); String imageUrl; - if(settings.has("offline_picture")) { + if (settings.has("offline_picture")) { imageUrl = settings.getString("offline_picture"); } else { imageUrl = "https:" + user.getString("thumb"); @@ -220,7 +248,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { img.progressProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observable, Number oldValue, Number newValue) { - if(newValue.doubleValue() == 1.0) { + if (newValue.doubleValue() == 1.0) { thumb.setImage(img); root.setLeft(thumb); } @@ -232,7 +260,7 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { } } resp.close(); - } catch(Exception e) { + } catch (Exception e) { LOG.error("Couldn't load model details", e); } }).start(); From 30add6aa3f6a9fc1327fbc220c40329aeda61de5 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 30 Oct 2018 21:08:37 +0100 Subject: [PATCH 16/42] Fix bug in cookie update code of CookieJar --- src/main/java/ctbrec/ui/CookieJarImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/ctbrec/ui/CookieJarImpl.java b/src/main/java/ctbrec/ui/CookieJarImpl.java index ce18ea4d..42b1e034 100644 --- a/src/main/java/ctbrec/ui/CookieJarImpl.java +++ b/src/main/java/ctbrec/ui/CookieJarImpl.java @@ -34,6 +34,7 @@ public class CookieJarImpl implements CookieJar { if(newCookie.name().equalsIgnoreCase(name)) { LOG.debug("Replacing cookie {} {} -> {} [{}]", oldCookie.name(), oldCookie.value(), newCookie.value(), oldCookie.domain()); iterator.remove(); + break; } } } From f2832940f155a1590c49a105702c984140e4374b Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 30 Oct 2018 21:09:29 +0100 Subject: [PATCH 17/42] Move CokkieJarImpl to io package --- src/main/java/ctbrec/{ui => io}/CookieJarImpl.java | 2 +- src/main/java/ctbrec/io/HttpClient.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) rename src/main/java/ctbrec/{ui => io}/CookieJarImpl.java (99%) diff --git a/src/main/java/ctbrec/ui/CookieJarImpl.java b/src/main/java/ctbrec/io/CookieJarImpl.java similarity index 99% rename from src/main/java/ctbrec/ui/CookieJarImpl.java rename to src/main/java/ctbrec/io/CookieJarImpl.java index 42b1e034..712ff30c 100644 --- a/src/main/java/ctbrec/ui/CookieJarImpl.java +++ b/src/main/java/ctbrec/io/CookieJarImpl.java @@ -1,4 +1,4 @@ -package ctbrec.ui; +package ctbrec.io; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/main/java/ctbrec/io/HttpClient.java b/src/main/java/ctbrec/io/HttpClient.java index 3b8e18e8..02c8f818 100644 --- a/src/main/java/ctbrec/io/HttpClient.java +++ b/src/main/java/ctbrec/io/HttpClient.java @@ -7,7 +7,6 @@ import java.util.concurrent.TimeUnit; import ctbrec.Config; import ctbrec.Settings.ProxyType; -import ctbrec.ui.CookieJarImpl; import okhttp3.ConnectionPool; import okhttp3.Credentials; import okhttp3.OkHttpClient; From efbae5711345e2daffaa1a7eabb0c2081d69cf51 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 30 Oct 2018 21:10:42 +0100 Subject: [PATCH 18/42] Set webengine user dir to child of config dir --- src/main/java/ctbrec/sites/cam4/Cam4LoginDialog.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/ctbrec/sites/cam4/Cam4LoginDialog.java b/src/main/java/ctbrec/sites/cam4/Cam4LoginDialog.java index 9aae0544..45fbf6e5 100644 --- a/src/main/java/ctbrec/sites/cam4/Cam4LoginDialog.java +++ b/src/main/java/ctbrec/sites/cam4/Cam4LoginDialog.java @@ -1,5 +1,6 @@ package ctbrec.sites.cam4; +import java.io.File; import java.io.InputStream; import java.net.CookieHandler; import java.net.CookieManager; @@ -13,6 +14,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Config; +import ctbrec.OS; import javafx.concurrent.Worker.State; import javafx.scene.Scene; import javafx.scene.control.ProgressIndicator; @@ -89,6 +91,7 @@ public class Cam4LoginDialog { p.setVisible(false); } }); + webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine")); webEngine.load(URL); return browser; } From 5333e427ad32e94dfbb20ccc4899efbcbe1a66df Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 30 Oct 2018 21:13:48 +0100 Subject: [PATCH 19/42] Add CamSoda login dialog This dialog is shown, if the normal login didn't work. For example, if you log in several times in a short amount of time, they respond with "Please confirm that you are not a robot" and you have to log in with a captcha --- .../sites/camsoda/CamsodaHttpClient.java | 84 ++++++++++++- .../sites/camsoda/CamsodaLoginDialog.java | 110 ++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ctbrec/sites/camsoda/CamsodaLoginDialog.java diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java b/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java index 76e3b98b..5381a968 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java @@ -1,19 +1,37 @@ package ctbrec.sites.camsoda; import java.io.IOException; +import java.net.HttpCookie; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.io.HttpClient; +import ctbrec.sites.cam4.Cam4LoginDialog; +import javafx.application.Platform; +import okhttp3.Cookie; import okhttp3.FormBody; +import okhttp3.HttpUrl; import okhttp3.Request; import okhttp3.Response; public class CamsodaHttpClient extends HttpClient { + private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaHttpClient.class); + @Override public boolean login() throws IOException { + if(loggedIn) { + return true; + } + String url = Camsoda.BASE_URI + "/api/v1/auth/login"; FormBody body = new FormBody.Builder() .add("username", Config.getInstance().getSettings().camsodaUsername) @@ -27,7 +45,12 @@ public class CamsodaHttpClient extends HttpClient { if(response.isSuccessful()) { JSONObject resp = new JSONObject(response.body().string()); if(resp.has("error")) { - throw new IOException(resp.getString("error")); + String error = resp.getString("error"); + if(Objects.equals(error, "Please confirm that you are not a robot.")) { + return loginWithDialog(); + } else { + throw new IOException(resp.getString("error")); + } } else { return true; } @@ -35,4 +58,63 @@ public class CamsodaHttpClient extends HttpClient { throw new IOException(response.code() + " " + response.message()); } } + + private boolean loginWithDialog() throws IOException { + BlockingQueue queue = new LinkedBlockingQueue<>(); + + Runnable showDialog = () -> { + // login with javafx WebView + CamsodaLoginDialog loginDialog = new CamsodaLoginDialog(); + + // transfer cookies from WebView to OkHttp cookie jar + transferCookies(loginDialog); + + try { + queue.put(true); + } catch (InterruptedException e) { + LOG.error("Error while signaling termination", e); + } + }; + + if(Platform.isFxApplicationThread()) { + showDialog.run(); + } else { + Platform.runLater(showDialog); + try { + queue.take(); + } catch (InterruptedException e) { + LOG.error("Error while waiting for login dialog to close", e); + throw new IOException(e); + } + } + + loggedIn = checkLoginSuccess(); + return loggedIn; + } + + /** + * check, if the login worked + * @throws IOException + */ + private boolean checkLoginSuccess() throws IOException { + return true; + } + + private void transferCookies(CamsodaLoginDialog loginDialog) { + HttpUrl redirectedUrl = HttpUrl.parse(loginDialog.getUrl()); + List cookies = new ArrayList<>(); + for (HttpCookie webViewCookie : loginDialog.getCookies()) { + Cookie cookie = Cookie.parse(redirectedUrl, webViewCookie.toString()); + cookies.add(cookie); + } + cookieJar.saveFromResponse(redirectedUrl, cookies); + + HttpUrl origUrl = HttpUrl.parse(Cam4LoginDialog.URL); + cookies = new ArrayList<>(); + for (HttpCookie webViewCookie : loginDialog.getCookies()) { + Cookie cookie = Cookie.parse(origUrl, webViewCookie.toString()); + cookies.add(cookie); + } + cookieJar.saveFromResponse(origUrl, cookies); + } } diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaLoginDialog.java b/src/main/java/ctbrec/sites/camsoda/CamsodaLoginDialog.java new file mode 100644 index 00000000..c0e08a2a --- /dev/null +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaLoginDialog.java @@ -0,0 +1,110 @@ +package ctbrec.sites.camsoda; + +import java.io.File; +import java.io.InputStream; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.HttpCookie; +import java.util.Base64; +import java.util.List; + +import ctbrec.OS; +import javafx.concurrent.Worker.State; +import javafx.scene.Scene; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.image.Image; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebView; +import javafx.stage.Stage; + +public class CamsodaLoginDialog { + + public static final String URL = Camsoda.BASE_URI; + private List cookies = null; + private String url; + private Region veil; + private ProgressIndicator p; + + public CamsodaLoginDialog() { + Stage stage = new Stage(); + stage.setTitle("CamSoda Login"); + InputStream icon = getClass().getResourceAsStream("/icon.png"); + stage.getIcons().add(new Image(icon)); + CookieManager cookieManager = new CookieManager(); + CookieHandler.setDefault(cookieManager); + WebView webView = createWebView(stage); + + veil = new Region(); + veil.setStyle("-fx-background-color: rgba(1, 1, 1)"); + p = new ProgressIndicator(); + p.setMaxSize(140, 140); + + p.setVisible(true); + veil.visibleProperty().bind(p.visibleProperty()); + + StackPane stackPane = new StackPane(); + stackPane.getChildren().addAll(webView, veil, p); + + stage.setScene(new Scene(stackPane, 400, 358)); + stage.showAndWait(); + cookies = cookieManager.getCookieStore().getCookies(); + } + + private WebView createWebView(Stage stage) { + WebView browser = new WebView(); + WebEngine webEngine = browser.getEngine(); + webEngine.setJavaScriptEnabled(true); + webEngine.locationProperty().addListener((obs, oldV, newV) -> { + // try { + // URL _url = new URL(newV); + // if (Objects.equals(_url.getPath(), "/")) { + // stage.close(); + // } + // } catch (MalformedURLException e) { + // LOG.error("Couldn't parse new url {}", newV, e); + // } + url = newV.toString(); + System.out.println(newV.toString()); + }); + webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> { + if (newState == State.SUCCEEDED) { + webEngine.executeScript("document.querySelector('a[ng-click=\"signin();\"]').click()"); + p.setVisible(false); + + // TODO make this work + // String username = Config.getInstance().getSettings().camsodaUsername; + // if (username != null && !username.trim().isEmpty()) { + // webEngine.executeScript("document.querySelector('input[name=\"loginUsername\"]').value = '" + username + "'"); + // } + // String password = Config.getInstance().getSettings().camsodaPassword; + // if (password != null && !password.trim().isEmpty()) { + // webEngine.executeScript("document.querySelector('input[name=\"loginPassword\"]').value = '" + password + "'"); + // } + } else if (newState == State.CANCELLED || newState == State.FAILED) { + p.setVisible(false); + } + }); + + webEngine.setUserStyleSheetLocation("data:text/css;base64," + Base64.getEncoder().encodeToString(CUSTOM_STYLE.getBytes())); + webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine")); + webEngine.load(URL); + return browser; + } + + public List getCookies() { + for (HttpCookie httpCookie : cookies) { + System.out.println(httpCookie); + } + return cookies; + } + + public String getUrl() { + return url; + } + + private static final String CUSTOM_STYLE = "" + + ".ngdialog.ngdialog-theme-custom { padding: 0 !important }" + + ".ngdialog-overlay { background: black !important; }"; +} From 6de58c380521a5ce3932d4889a91c8dd358238da Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 30 Oct 2018 23:17:40 +0100 Subject: [PATCH 20/42] Change package for CookieJarImpl in logback.xml --- src/main/resources/logback.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 6c731cec..4642d547 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -41,7 +41,7 @@ - + From d6b443b05a1986eb50d05a2a6606374f69f93175 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 30 Oct 2018 23:18:50 +0100 Subject: [PATCH 21/42] Implement follow and unfollow for CamsodaModel --- .../sites/camsoda/CamsodaHttpClient.java | 21 ++++++ .../ctbrec/sites/camsoda/CamsodaModel.java | 67 ++++++++++++------- 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java b/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java index 5381a968..bae35ef7 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java @@ -9,12 +9,14 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import org.json.JSONObject; +import org.jsoup.nodes.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.io.HttpClient; import ctbrec.sites.cam4.Cam4LoginDialog; +import ctbrec.ui.HtmlParser; import javafx.application.Platform; import okhttp3.Cookie; import okhttp3.FormBody; @@ -25,6 +27,7 @@ import okhttp3.Response; public class CamsodaHttpClient extends HttpClient { private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaHttpClient.class); + private String csrfToken = null; @Override public boolean login() throws IOException { @@ -97,6 +100,7 @@ public class CamsodaHttpClient extends HttpClient { * @throws IOException */ private boolean checkLoginSuccess() throws IOException { + // TODO load /api/v1/user/current and check status or so return true; } @@ -117,4 +121,21 @@ public class CamsodaHttpClient extends HttpClient { } cookieJar.saveFromResponse(origUrl, cookies); } + + protected String getCsrfToken() throws IOException { + if(csrfToken == null) { + String url = Camsoda.BASE_URI; + Request request = new Request.Builder().url(url).build(); + Response resp = execute(request, true); + if(resp.isSuccessful()) { + Element meta = HtmlParser.getTag(resp.body().string(), "meta[name=\"_token\"]"); + csrfToken = meta.attr("content"); + } else { + IOException e = new IOException(resp.code() + " " + resp.message()); + resp.close(); + throw e; + } + } + return csrfToken; + } } diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index 1a6a6269..24274259 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -25,6 +25,7 @@ import ctbrec.AbstractModel; import ctbrec.recorder.download.StreamSource; import ctbrec.sites.Site; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; public class CamsodaModel extends AbstractModel { @@ -170,36 +171,50 @@ public class CamsodaModel extends AbstractModel { @Override public boolean follow() throws IOException { - // FIXME follow and unfollow don't work yet, because the HTTP requests need to have - // the cross-site scripting prevention header (e.g. X-CSRF-Token: YDixu6rFg3ovqos9C1YuYpsVd7bxuXlpNnZnelKG), - // but i didn't find out yet, how to get the token (cookie, hmac calc, ?!?) - throw new RuntimeException("Not implemented, yet"); - - // String url = Camsoda.BASE_URI + "/api/v1/follow/" + getName(); - // //RequestBody body = new FormBody.Builder().build(); - // LOG.debug("Sending follow request {}", url); - // Request request = new Request.Builder() - // .url(url) - // .post(RequestBody.create(null, "")) - // .addHeader("Referer", Camsoda.BASE_URI + '/' + getName()) - // .addHeader("User-Agent", " Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0") - // .addHeader("Accept", " application/json, text/plain, */*") - // .addHeader("Accept-Language", " de,en-US;q=0.7,en;q=0.3") - // .build(); - // Response resp = site.getHttpClient().execute(request, false); - // if (resp.isSuccessful()) { - // System.out.println(resp.body().string()); - // return true; - // } else { - // resp.close(); - // throw new IOException("HTTP status " + resp.code() + " " + resp.message()); - // } + String url = Camsoda.BASE_URI + "/api/v1/follow/" + getName(); + LOG.debug("Sending follow request {}", url); + String csrfToken = ((CamsodaHttpClient)site.getHttpClient()).getCsrfToken(); + Request request = new Request.Builder() + .url(url) + .post(RequestBody.create(null, "")) + .addHeader("Referer", Camsoda.BASE_URI + '/' + getName()) + .addHeader("User-Agent", " Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0") + .addHeader("Accept", " application/json, text/plain, */*") + .addHeader("Accept-Language", " de,en-US;q=0.7,en;q=0.3") + .addHeader("X-CSRF-Token", csrfToken) + .build(); + Response resp = site.getHttpClient().execute(request, true); + if (resp.isSuccessful()) { + resp.close(); + return true; + } else { + resp.close(); + throw new IOException("HTTP status " + resp.code() + " " + resp.message()); + } } @Override public boolean unfollow() throws IOException { - // TODO /api/v1/unfollow/" + n.slug - throw new RuntimeException("Not implemented, yet"); + String url = Camsoda.BASE_URI + "/api/v1/unfollow/" + getName(); + LOG.debug("Sending follow request {}", url); + String csrfToken = ((CamsodaHttpClient)site.getHttpClient()).getCsrfToken(); + Request request = new Request.Builder() + .url(url) + .post(RequestBody.create(null, "")) + .addHeader("Referer", Camsoda.BASE_URI + '/' + getName()) + .addHeader("User-Agent", " Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0") + .addHeader("Accept", " application/json, text/plain, */*") + .addHeader("Accept-Language", " de,en-US;q=0.7,en;q=0.3") + .addHeader("X-CSRF-Token", csrfToken) + .build(); + Response resp = site.getHttpClient().execute(request, true); + if (resp.isSuccessful()) { + resp.close(); + return true; + } else { + resp.close(); + throw new IOException("HTTP status " + resp.code() + " " + resp.message()); + } } @Override From 7d4245e44e672698dc221983c0d6b96abb308db8 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 30 Oct 2018 23:21:39 +0100 Subject: [PATCH 22/42] Disable tips for the time being --- src/main/java/ctbrec/sites/camsoda/Camsoda.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/src/main/java/ctbrec/sites/camsoda/Camsoda.java index 17614643..74a5ebb0 100644 --- a/src/main/java/ctbrec/sites/camsoda/Camsoda.java +++ b/src/main/java/ctbrec/sites/camsoda/Camsoda.java @@ -118,7 +118,7 @@ public class Camsoda extends AbstractSite { @Override public boolean supportsTips() { - return true; + return false; } @Override From 91210f8addd37999735f8ab9edf2a54b81d86452 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 30 Oct 2018 23:21:55 +0100 Subject: [PATCH 23/42] Add comment --- src/main/java/ctbrec/sites/camsoda/CamsodaLoginDialog.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaLoginDialog.java b/src/main/java/ctbrec/sites/camsoda/CamsodaLoginDialog.java index c0e08a2a..6668babf 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaLoginDialog.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaLoginDialog.java @@ -19,6 +19,8 @@ import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import javafx.stage.Stage; +// FIXME this dialog does not help, because google's recaptcha does not work +// with WebView even though it does work in Cam4LoginDialog public class CamsodaLoginDialog { public static final String URL = Camsoda.BASE_URI; @@ -94,9 +96,6 @@ public class CamsodaLoginDialog { } public List getCookies() { - for (HttpCookie httpCookie : cookies) { - System.out.println(httpCookie); - } return cookies; } From 4696c0f5340bb472234c0ac69b9d0f12fa4ebb80 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 31 Oct 2018 00:04:53 +0100 Subject: [PATCH 24/42] Put all site config panes in an accordion --- src/main/java/ctbrec/ui/SettingsTab.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/ctbrec/ui/SettingsTab.java b/src/main/java/ctbrec/ui/SettingsTab.java index ae5ce0c2..693427d6 100644 --- a/src/main/java/ctbrec/ui/SettingsTab.java +++ b/src/main/java/ctbrec/ui/SettingsTab.java @@ -20,6 +20,7 @@ import javafx.beans.value.ObservableValue; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.scene.Node; +import javafx.scene.control.Accordion; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; @@ -67,6 +68,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private ComboBox splitAfter; private List sites; private Label restartLabel; + private Accordion credentialsAccordion = new Accordion(); public SettingsTab(List sites) { this.sites = sites; @@ -113,14 +115,16 @@ public class SettingsTab extends Tab implements TabSelectionListener { //right side rightSide.getChildren().add(createSiteSelectionPanel()); - for (Site site : sites) { + rightSide.getChildren().add(credentialsAccordion); + for (int i = 0; i < sites.size(); i++) { + Site site = sites.get(i); Node siteConfig = site.getConfigurationGui(); if(siteConfig != null) { TitledPane pane = new TitledPane(site.getName(), siteConfig); - pane.setCollapsible(false); - rightSide.getChildren().add(pane); + credentialsAccordion.getPanes().add(pane); } } + credentialsAccordion.setExpandedPane(credentialsAccordion.getPanes().get(0)); } private Node createSiteSelectionPanel() { From b90313dd86eceaacf1dfaa14a92d5207674894d0 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 31 Oct 2018 01:34:45 +0100 Subject: [PATCH 25/42] Restart websocket, if no message arrived for 2 mins --- src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index 188f2378..86d02faf 100644 --- a/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -57,6 +57,7 @@ public class MyFreeCamsClient { private String ctxenc; private String chatToken; private int sessionId; + private long heartBeat; private EvictingQueue receivedTextHistory = EvictingQueue.create(10000); @@ -135,6 +136,7 @@ public class MyFreeCamsClient { // TODO find out, what the values in the json message mean, at the moment we hust send 0s, which seems to work, too // webSocket.send("1 0 0 81 0 %7B%22err%22%3A0%2C%22start%22%3A1540159843072%2C%22stop%22%3A1540159844121%2C%22a%22%3A6392%2C%22time%22%3A1540159844%2C%22key%22%3A%228da80f985c9db390809713dac71df297%22%2C%22cid%22%3A%22c504d684%22%2C%22pid%22%3A1%2C%22site%22%3A%22www%22%7D\n"); webSocket.send("1 0 0 81 0 %7B%22err%22%3A0%2C%22start%22%3A0%2C%22stop%22%3A0%2C%22a%22%3A0%2C%22time%22%3A0%2C%22key%22%3A%22%22%2C%22cid%22%3A%22%22%2C%22pid%22%3A1%2C%22site%22%3A%22www%22%7D\n"); + heartBeat = System.currentTimeMillis(); startKeepAlive(webSocket); } catch (IOException e) { e.printStackTrace(); @@ -165,6 +167,7 @@ public class MyFreeCamsClient { @Override public void onMessage(WebSocket webSocket, String text) { super.onMessage(webSocket, text); + heartBeat = System.currentTimeMillis(); receivedTextHistory.add(text); msgBuffer.append(text); Message message; @@ -469,6 +472,14 @@ public class MyFreeCamsClient { LOG.trace("--> NULL to keep the connection alive"); try { ws.send("0 0 0 0 0 -\n"); + + long millisSinceLastMessage = System.currentTimeMillis() - heartBeat; + if(millisSinceLastMessage > TimeUnit.MINUTES.toMillis(2)) { + LOG.info("No message since 2 mins. Restarting websocket"); + ws.close(1000, ""); + MyFreeCamsClient.this.ws = null; + } + Thread.sleep(TimeUnit.SECONDS.toMillis(15)); } catch (Exception e) { e.printStackTrace(); From 6faee8290c7d17e8cf3253d89ba3cb18a263462c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 31 Oct 2018 01:35:18 +0100 Subject: [PATCH 26/42] Remove initial login Login should we done on demand --- src/main/java/ctbrec/ui/CamrecApplication.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/ctbrec/ui/CamrecApplication.java b/src/main/java/ctbrec/ui/CamrecApplication.java index 352dadb9..90be7e79 100644 --- a/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/src/main/java/ctbrec/ui/CamrecApplication.java @@ -27,8 +27,8 @@ import ctbrec.recorder.LocalRecorder; import ctbrec.recorder.Recorder; import ctbrec.recorder.RemoteRecorder; import ctbrec.sites.Site; -import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.cam4.Cam4; +import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.mfc.MyFreeCams; import javafx.application.Application; @@ -74,9 +74,6 @@ public class CamrecApplication extends Application { try { site.setRecorder(recorder); site.init(); - if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { - site.login(); - } } catch(Exception e) { LOG.error("Error while initializing site {}", site.getName(), e); } From bc40c16000253cf213886aacb74d73bf6a047282 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 31 Oct 2018 13:55:03 +0100 Subject: [PATCH 27/42] Add possibility to safe site specific data Extend the json serialization to allow to write site specific data for each model. --- src/main/java/ctbrec/AbstractModel.java | 12 ++++ src/main/java/ctbrec/Model.java | 4 ++ src/main/java/ctbrec/io/ModelJsonAdapter.java | 70 +++++++++++-------- src/main/java/ctbrec/ui/JavaFxModel.java | 12 ++++ 4 files changed, 68 insertions(+), 30 deletions(-) diff --git a/src/main/java/ctbrec/AbstractModel.java b/src/main/java/ctbrec/AbstractModel.java index 267f9661..28220312 100644 --- a/src/main/java/ctbrec/AbstractModel.java +++ b/src/main/java/ctbrec/AbstractModel.java @@ -8,6 +8,8 @@ import java.util.concurrent.ExecutionException; import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; import ctbrec.recorder.download.StreamSource; @@ -93,6 +95,16 @@ public abstract class AbstractModel implements Model { return url; } + @Override + public void readSiteSpecificData(JsonReader reader) throws IOException { + // noop default implementation, can be overriden by concrete models + } + + @Override + public void writeSiteSpecificData(JsonWriter writer) throws IOException { + // noop default implementation, can be overriden by concrete models + } + @Override public int hashCode() { final int prime = 31; diff --git a/src/main/java/ctbrec/Model.java b/src/main/java/ctbrec/Model.java index 68d9646b..2db582a3 100644 --- a/src/main/java/ctbrec/Model.java +++ b/src/main/java/ctbrec/Model.java @@ -6,6 +6,8 @@ import java.util.concurrent.ExecutionException; import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; import ctbrec.recorder.download.StreamSource; import ctbrec.sites.Site; @@ -35,4 +37,6 @@ public interface Model { public boolean unfollow() throws IOException; public void setSite(Site site); public Site getSite(); + public void writeSiteSpecificData(JsonWriter writer) throws IOException; + public void readSiteSpecificData(JsonReader reader) throws IOException; } \ No newline at end of file diff --git a/src/main/java/ctbrec/io/ModelJsonAdapter.java b/src/main/java/ctbrec/io/ModelJsonAdapter.java index fdaefc39..804c77fa 100644 --- a/src/main/java/ctbrec/io/ModelJsonAdapter.java +++ b/src/main/java/ctbrec/io/ModelJsonAdapter.java @@ -32,45 +32,51 @@ public class ModelJsonAdapter extends JsonAdapter { String url = null; String type = null; int streamUrlIndex = -1; + + Model model = null; while(reader.hasNext()) { - Token token = reader.peek(); - if(token == Token.NAME) { - String key = reader.nextName(); - if(key.equals("name")) { - name = reader.nextString(); - } else if(key.equals("description")) { - description = reader.nextString(); - } else if(key.equals("url")) { - url = reader.nextString(); - } else if(key.equals("type")) { - type = reader.nextString(); - } else if(key.equals("streamUrlIndex")) { - streamUrlIndex = reader.nextInt(); + try { + Token token = reader.peek(); + if(token == Token.NAME) { + String key = reader.nextName(); + if(key.equals("name")) { + name = reader.nextString(); + model.setName(name); + } else if(key.equals("description")) { + description = reader.nextString(); + model.setDescription(description); + } else if(key.equals("url")) { + url = reader.nextString(); + model.setUrl(url); + } else if(key.equals("type")) { + type = reader.nextString(); + Class modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName())); + model = (Model) modelClass.newInstance(); + } else if(key.equals("streamUrlIndex")) { + streamUrlIndex = reader.nextInt(); + model.setStreamUrlIndex(streamUrlIndex); + } else if(key.equals("siteSpecific")) { + reader.beginObject(); + model.readSiteSpecificData(reader); + reader.endObject(); + } + } else { + reader.skipValue(); } - } else { - reader.skipValue(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + throw new IOException("Couldn't instantiate model class [" + type + "]", e); } } reader.endObject(); - try { - Class modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName())); - Model model = (Model) modelClass.newInstance(); - model.setName(name); - model.setDescription(description); - model.setUrl(url); - model.setStreamUrlIndex(streamUrlIndex); - if(sites != null) { - for (Site site : sites) { - if(site.isSiteForModel(model)) { - model.setSite(site); - } + if(sites != null) { + for (Site site : sites) { + if(site.isSiteForModel(model)) { + model.setSite(site); } } - return model; - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { - throw new IOException("Couldn't instantiate model class [" + type + "]", e); } + return model; } @Override @@ -81,6 +87,10 @@ public class ModelJsonAdapter extends JsonAdapter { writeValueIfSet(writer, "description", model.getDescription()); writeValueIfSet(writer, "url", model.getUrl()); writer.name("streamUrlIndex").value(model.getStreamUrlIndex()); + writer.name("siteSpecific"); + writer.beginObject(); + model.writeSiteSpecificData(writer); + writer.endObject(); writer.endObject(); } diff --git a/src/main/java/ctbrec/ui/JavaFxModel.java b/src/main/java/ctbrec/ui/JavaFxModel.java index 1ede0c64..105da296 100644 --- a/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/src/main/java/ctbrec/ui/JavaFxModel.java @@ -7,6 +7,8 @@ import java.util.concurrent.ExecutionException; import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; import ctbrec.AbstractModel; import ctbrec.Model; @@ -147,4 +149,14 @@ public class JavaFxModel extends AbstractModel { public Site getSite() { return delegate.getSite(); } + + @Override + public void readSiteSpecificData(JsonReader reader) throws IOException { + delegate.readSiteSpecificData(reader); + } + + @Override + public void writeSiteSpecificData(JsonWriter writer) throws IOException { + delegate.writeSiteSpecificData(writer); + } } From 07dee1a631939b13c89d7d236daf8ffc8820d87d Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 31 Oct 2018 13:57:04 +0100 Subject: [PATCH 28/42] Add detection of model name changes for MyFreeCams MyFreeCamsModel now also takes the uid into account instead of only using the the name. To support this feature between sessions, the uid is saved as site specific data. --- .../ctbrec/sites/mfc/MyFreeCamsClient.java | 2 +- .../ctbrec/sites/mfc/MyFreeCamsModel.java | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java b/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java index 188f2378..7013dfd8 100644 --- a/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCamsClient.java @@ -484,7 +484,7 @@ public class MyFreeCamsClient { lock.lock(); try { for (SessionState state : sessionStates.values()) { - if(Objects.equals(state.getNm(), model.getName())) { + if(Objects.equals(state.getNm(), model.getName()) || Objects.equals(model.getUid(), state.getUid())) { model.update(state, getStreamUrl(state)); return; } diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java b/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java index 36a24da6..db794ae2 100644 --- a/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCamsModel.java @@ -23,6 +23,8 @@ 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.JsonReader; +import com.squareup.moshi.JsonWriter; import ctbrec.AbstractModel; import ctbrec.recorder.download.StreamSource; @@ -37,7 +39,7 @@ public class MyFreeCamsModel extends AbstractModel { private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsModel.class); - private int uid; + private int uid = -1; // undefined private String hlsUrl; private double camScore; private int viewerCount; @@ -207,7 +209,17 @@ public class MyFreeCamsModel extends AbstractModel { this.state = state; } + @Override + public void setName(String name) { + if(getName() != null && name != null && !getName().equals(name)) { + LOG.debug("Model name changed {} -> {}", getName(), name); + } + super.setName(name); + } + public void update(SessionState state, String streamUrl) { + uid = Integer.parseInt(state.getUid().toString()); + setName(state.getNm()); setCamScore(state.getM().getCamscore()); setState(State.of(state.getVs())); setStreamUrl(streamUrl); @@ -308,4 +320,15 @@ public class MyFreeCamsModel extends AbstractModel { public Site getSite() { return site; } + + @Override + public void readSiteSpecificData(JsonReader reader) throws IOException { + reader.nextName(); + uid = reader.nextInt(); + } + + @Override + public void writeSiteSpecificData(JsonWriter writer) throws IOException { + writer.name("uid").value(uid); + } } From 625b7727d01112d649b13055f54972c6d7f5c7e0 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 31 Oct 2018 14:17:47 +0100 Subject: [PATCH 29/42] Add affiliate link to MyFreeCams --- src/main/java/ctbrec/sites/mfc/MyFreeCams.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCams.java b/src/main/java/ctbrec/sites/mfc/MyFreeCams.java index c8544740..c06c1218 100644 --- a/src/main/java/ctbrec/sites/mfc/MyFreeCams.java +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCams.java @@ -55,7 +55,7 @@ public class MyFreeCams extends AbstractSite { @Override public String getAffiliateLink() { - return ""; + return BASE_URI + "/?baf=8127165"; } @Override @@ -93,7 +93,7 @@ public class MyFreeCams extends AbstractSite { @Override public String getBuyTokensLink() { - return "https://www.myfreecams.com/php/purchase.php?request=tokens"; + return BASE_URI + "/php/purchase.php?request=tokens"; } @Override @@ -149,7 +149,7 @@ public class MyFreeCams extends AbstractSite { layout.add(password, 1, 1); Button createAccount = new Button("Create new Account"); - createAccount.setOnAction((e) -> DesktopIntergation.open(BASE_URI + "/php/signup.php?request=register")); + createAccount.setOnAction((e) -> DesktopIntergation.open(getAffiliateLink())); layout.add(createAccount, 1, 2); GridPane.setColumnSpan(createAccount, 2); GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); From 3f84f94873f061e4687a28a3855c58bb9836646b Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 31 Oct 2018 14:18:12 +0100 Subject: [PATCH 30/42] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3b95ef4..2638c346 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +1.6.2 +======================== +* Added detection of model name changes for MyFreeCams + + 1.6.1 ======================== * Fixed UI freeze, which occured for a high number of recorded models From cbc4a5e3395690131e4e13735c0d6aa92dd62926 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 2 Nov 2018 18:13:40 +0100 Subject: [PATCH 31/42] Fix model parsing JSON structure seems to have changed slightly --- .../sites/camsoda/CamsodaUpdateService.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java b/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java index 6b745f4a..a43f9332 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java @@ -59,26 +59,37 @@ public class CamsodaUpdateService extends PaginatedScheduledService { String streamName = tpl.getString(5); String tsize = tpl.getString(6); String serverPrefix = tpl.getString(7); - JSONArray edgeServers = result.getJSONArray("edge_servers"); CamsodaModel model = (CamsodaModel) camsoda.createModel(name); model.setDescription(tpl.getString(4)); - model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); model.setSortOrder(tpl.getFloat(3)); long unixtime = System.currentTimeMillis() / 1000; String preview = "https://thumbs-orig.camsoda.com/thumbs/" + streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime; model.setPreview(preview); + if(result.has("edge_servers")) { + JSONArray edgeServers = result.getJSONArray("edge_servers"); + model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); + } models.add(model); } else { //LOG.debug("{}", result.toString(2)); String name = result.getString("username"); CamsodaModel model = (CamsodaModel) camsoda.createModel(name); + if(result.has("server_prefix")) { String serverPrefix = result.getString("server_prefix"); String streamName = result.getString("stream_name"); - JSONArray edgeServers = result.getJSONArray("edge_servers"); - model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); + model.setSortOrder(result.getFloat("sort_value")); + models.add(model); + if(result.has("status")) { + model.setOnlineState(result.getString("status")); + } + + if(result.has("edge_servers")) { + JSONArray edgeServers = result.getJSONArray("edge_servers"); + model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); + } if(result.has("tsize")) { long unixtime = System.currentTimeMillis() / 1000; @@ -88,11 +99,6 @@ public class CamsodaUpdateService extends PaginatedScheduledService { model.setPreview(preview); } - model.setSortOrder(result.getFloat("sort_value")); - models.add(model); - if(result.has("status")) { - model.setOnlineState(result.getString("status")); - } } } } From 233dd3fa9cec5ee7f265bd7fa0a3561fb602ddb3 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 2 Nov 2018 18:14:36 +0100 Subject: [PATCH 32/42] Implement tipping for CamSoda Sending tips for CamSoda works now --- .../java/ctbrec/sites/camsoda/Camsoda.java | 12 +-- .../ctbrec/sites/camsoda/CamsodaModel.java | 81 ++++++++++++------- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/src/main/java/ctbrec/sites/camsoda/Camsoda.java index 74a5ebb0..3cf7f937 100644 --- a/src/main/java/ctbrec/sites/camsoda/Camsoda.java +++ b/src/main/java/ctbrec/sites/camsoda/Camsoda.java @@ -65,11 +65,11 @@ public class Camsoda extends AbstractSite { @Override public Integer getTokenBalance() throws IOException { - String username = Config.getInstance().getSettings().camsodaUsername; - if (username == null || username.trim().isEmpty()) { - throw new IOException("Not logged in"); + if (!credentialsAvailable()) { + throw new IOException("Account settings not available"); } + String username = Config.getInstance().getSettings().camsodaUsername; String url = BASE_URI + "/api/v1/user/" + username; Request request = new Request.Builder().url(url).build(); Response response = getHttpClient().execute(request, true); @@ -113,12 +113,14 @@ public class Camsoda extends AbstractSite { @Override public void shutdown() { - httpClient.shutdown(); + if(httpClient != null) { + httpClient.shutdown(); + } } @Override public boolean supportsTips() { - return false; + return true; } @Override diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index 24274259..1fe5beeb 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -6,11 +6,14 @@ import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.iheartradio.m3u8.Encoding; import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; @@ -24,6 +27,7 @@ import com.iheartradio.m3u8.data.StreamInfo; import ctbrec.AbstractModel; import ctbrec.recorder.download.StreamSource; import ctbrec.sites.Site; +import okhttp3.FormBody; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; @@ -34,10 +38,15 @@ public class CamsodaModel extends AbstractModel { private String streamUrl; private Site site; private List streamSources = null; - private int[] resolution; private String status = "n/a"; private float sortOrder = 0; + private static Cache streamResolutionCache = CacheBuilder.newBuilder() + .initialCapacity(10_000) + .maximumSize(10_000) + .expireAfterWrite(30, TimeUnit.MINUTES) + .build(); + public String getStreamUrl() throws IOException { if(streamUrl == null) { // load model @@ -95,7 +104,10 @@ public class CamsodaModel extends AbstractModel { @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { - LOG.debug("Loading master playlist {}", streamUrl); + String streamUrl = getStreamUrl(); + if(streamUrl == null) { + return Collections.emptyList(); + } Request req = new Request.Builder().url(streamUrl).build(); Response response = site.getHttpClient().execute(req); try { @@ -126,11 +138,12 @@ public class CamsodaModel extends AbstractModel { @Override public void invalidateCacheEntries() { streamSources = null; - resolution = null; + streamResolutionCache.invalidate(getName()); } @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { + int[] resolution = streamResolutionCache.getIfPresent(getName()); if(resolution != null) { return resolution; } else { @@ -139,9 +152,14 @@ public class CamsodaModel extends AbstractModel { } else { try { List streamSources = getStreamSources(); - StreamSource src = streamSources.get(0); - resolution = new int[] {src.width, src.height}; - return resolution; + if(streamSources.isEmpty()) { + return new int[] {0,0}; + } else { + StreamSource src = streamSources.get(0); + resolution = new int[] {src.width, src.height}; + streamResolutionCache.put(getName(), resolution); + return resolution; + } } catch (IOException | ParseException | PlaylistException e) { throw new ExecutionException(e); } @@ -151,22 +169,29 @@ public class CamsodaModel extends AbstractModel { @Override public void receiveTip(int tokens) throws IOException { - // TODO Auto-generated method stub - /* - sendTip: function(i, a, r, o, c, d) { - if (!s.isAuthenticated()) return s.showRegister(), t.when(!1); - var u = t.defer(); - return e.post("/api/v1/tip/" + i, { - amount: a, - comment: o, - type: r, - app_data: c, - source_id: d - }).then(function(e) { - 1 == e.data.status ? (s.currentUser.tokens = e.data.total, void 0 != e.data.tipped_performer_last_24hrs && e.data.tipped_performer_last_24hrs >= 25 && (n.$emit("local.allowed_to_rate"), 0 == n.allowedToRate && (n.allowedToRate = !0, l.pop("info", "Voting Unlocked", "You tipped " + i + " 25 tokens in the past 24 hours, you may now vote!"))), u.resolve(e.data)) : (l.pop("error", e.data.error, e.data.message), u.reject(e.data)) - }), u.promise - }, - */ + String csrfToken = ((CamsodaHttpClient)site.getHttpClient()).getCsrfToken(); + String url = site.getBaseUrl() + "/api/v1/tip/" + getName(); + if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) { + LOG.debug("Sending tip {}", url); + RequestBody body = new FormBody.Builder() + .add("amount", Integer.toString(tokens)) + .add("comment", "") + .build(); + Request request = new Request.Builder() + .url(url) + .post(body) + .addHeader("Referer", Camsoda.BASE_URI + '/' + getName()) + .addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0") + .addHeader("Accept", "application/json, text/plain, */*") + .addHeader("Accept-Language", "en") + .addHeader("X-CSRF-Token", csrfToken) + .build(); + try(Response response = site.getHttpClient().execute(request, true)) { + if(!response.isSuccessful()) { + throw new IOException("HTTP status " + response.code() + " " + response.message()); + } + } + } } @Override @@ -178,9 +203,9 @@ public class CamsodaModel extends AbstractModel { .url(url) .post(RequestBody.create(null, "")) .addHeader("Referer", Camsoda.BASE_URI + '/' + getName()) - .addHeader("User-Agent", " Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0") - .addHeader("Accept", " application/json, text/plain, */*") - .addHeader("Accept-Language", " de,en-US;q=0.7,en;q=0.3") + .addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0") + .addHeader("Accept", "application/json, text/plain, */*") + .addHeader("Accept-Language", "en") .addHeader("X-CSRF-Token", csrfToken) .build(); Response resp = site.getHttpClient().execute(request, true); @@ -202,9 +227,9 @@ public class CamsodaModel extends AbstractModel { .url(url) .post(RequestBody.create(null, "")) .addHeader("Referer", Camsoda.BASE_URI + '/' + getName()) - .addHeader("User-Agent", " Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0") - .addHeader("Accept", " application/json, text/plain, */*") - .addHeader("Accept-Language", " de,en-US;q=0.7,en;q=0.3") + .addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0") + .addHeader("Accept", "application/json, text/plain, */*") + .addHeader("Accept-Language", "en") .addHeader("X-CSRF-Token", csrfToken) .build(); Response resp = site.getHttpClient().execute(request, true); From 9b39440190302ec0ba783d3bffba5fc03041b94f Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 2 Nov 2018 18:26:52 +0100 Subject: [PATCH 33/42] Make login dialog more robust --- .../ctbrec/sites/cam4/Cam4LoginDialog.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/ctbrec/sites/cam4/Cam4LoginDialog.java b/src/main/java/ctbrec/sites/cam4/Cam4LoginDialog.java index 45fbf6e5..c170af83 100644 --- a/src/main/java/ctbrec/sites/cam4/Cam4LoginDialog.java +++ b/src/main/java/ctbrec/sites/cam4/Cam4LoginDialog.java @@ -73,19 +73,23 @@ public class Cam4LoginDialog { }); webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> { if (newState == State.SUCCEEDED) { - String username = Config.getInstance().getSettings().cam4Username; - if (username != null && !username.trim().isEmpty()) { - webEngine.executeScript("$('input[name=username]').attr('value','" + username + "')"); - } - String password = Config.getInstance().getSettings().cam4Password; - if (password != null && !password.trim().isEmpty()) { - webEngine.executeScript("$('input[name=password]').attr('value','" + password + "')"); - } - webEngine.executeScript("$('div[class~=navbar]').css('display','none')"); - webEngine.executeScript("$('div#footer').css('display','none')"); - webEngine.executeScript("$('div#content').css('padding','0')"); veil.setVisible(false); p.setVisible(false); + try { + String username = Config.getInstance().getSettings().cam4Username; + if (username != null && !username.trim().isEmpty()) { + webEngine.executeScript("$('input[name=username]').attr('value','" + username + "')"); + } + String password = Config.getInstance().getSettings().cam4Password; + if (password != null && !password.trim().isEmpty()) { + webEngine.executeScript("$('input[name=password]').attr('value','" + password + "')"); + } + webEngine.executeScript("$('div[class~=navbar]').css('display','none')"); + webEngine.executeScript("$('div#footer').css('display','none')"); + webEngine.executeScript("$('div#content').css('padding','0')"); + } catch(Exception e) { + LOG.warn("Couldn't auto fill username and password for Cam4", e); + } } else if (newState == State.CANCELLED || newState == State.FAILED) { veil.setVisible(false); p.setVisible(false); From 95a3e2dc79ac3f1af19328bbc0e1788ecd167e44 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 2 Nov 2018 18:32:57 +0100 Subject: [PATCH 34/42] Don't open login dialog, if login has captcha The login dialog doesn't work anyways. So it makes more sense to show a reasonable error dialog at the moment. --- src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java b/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java index bae35ef7..466f7f10 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java @@ -49,8 +49,9 @@ public class CamsodaHttpClient extends HttpClient { JSONObject resp = new JSONObject(response.body().string()); if(resp.has("error")) { String error = resp.getString("error"); - if(Objects.equals(error, "Please confirm that you are not a robot.")) { - return loginWithDialog(); + if (Objects.equals(error, "Please confirm that you are not a robot.")) { + //return loginWithDialog(); + throw new IOException("CamSoda requested to solve a captcha. Please try again in a while (maybe 15 min)."); } else { throw new IOException(resp.getString("error")); } @@ -62,6 +63,7 @@ public class CamsodaHttpClient extends HttpClient { } } + @SuppressWarnings("unused") private boolean loginWithDialog() throws IOException { BlockingQueue queue = new LinkedBlockingQueue<>(); From e0d65c85722b2cfb6b292b7dbd892e9b52bd4477 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Fri, 2 Nov 2018 18:39:33 +0100 Subject: [PATCH 35/42] Call /api/v1/user/current to check the login status --- .../java/ctbrec/sites/camsoda/CamsodaHttpClient.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java b/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java index 466f7f10..9ab57b9e 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java @@ -102,8 +102,16 @@ public class CamsodaHttpClient extends HttpClient { * @throws IOException */ private boolean checkLoginSuccess() throws IOException { - // TODO load /api/v1/user/current and check status or so - return true; + String url = Camsoda.BASE_URI + "/api/v1/user/current"; + Request request = new Request.Builder().url(url).build(); + try(Response response = execute(request)) { + if(response.isSuccessful()) { + JSONObject resp = new JSONObject(response.body().string()); + return resp.optBoolean("status"); + } else { + return false; + } + } } private void transferCookies(CamsodaLoginDialog loginDialog) { From dbd5a42dbcd5a127badbc9eed0fb625f001d150a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sat, 3 Nov 2018 13:36:47 +0100 Subject: [PATCH 36/42] Show image placeholder in camsoda shows tab in dev mode --- .../ctbrec/sites/camsoda/CamsodaShowsTab.java | 15 ++++++++++++--- src/main/resources/image_not_found.png | Bin 0 -> 6827 bytes 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/image_not_found.png diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java b/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java index c8ba8a2d..44070fcc 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java @@ -12,6 +12,7 @@ import java.time.temporal.TemporalAccessor; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import org.json.JSONArray; import org.json.JSONObject; @@ -37,6 +38,7 @@ import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.control.TitledPane; +import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; @@ -176,14 +178,17 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { grid.add(createLabel("End", true), 0, 1); grid.add(createLabel(formatter.format(endTime), false), 1, 1); Button record = new Button("Record Model"); + record.setTooltip(new Tooltip(record.getText())); record.setOnAction((evt) -> record(model)); grid.add(record, 1, 2); GridPane.setMargin(record, new Insets(10)); Button follow = new Button("Follow"); + follow.setTooltip(new Tooltip(follow.getText())); follow.setOnAction((evt) -> follow(model)); grid.add(follow, 1, 3); GridPane.setMargin(follow, new Insets(10)); Button openInBrowser = new Button("Open in Browser"); + openInBrowser.setTooltip(new Tooltip(openInBrowser.getText())); openInBrowser.setOnAction((evt) -> DesktopIntergation.open(model.getUrl())); grid.add(openInBrowser, 1, 4); GridPane.setMargin(openInBrowser, new Insets(10)); @@ -238,10 +243,14 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { if (user.has("settings")) { JSONObject settings = user.getJSONObject("settings"); String imageUrl; - if (settings.has("offline_picture")) { - imageUrl = settings.getString("offline_picture"); + if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) { + imageUrl = getClass().getResource("/image_not_found.png").toString(); } else { - imageUrl = "https:" + user.getString("thumb"); + if (settings.has("offline_picture")) { + imageUrl = settings.getString("offline_picture"); + } else { + imageUrl = "https:" + user.getString("thumb"); + } } Platform.runLater(() -> { Image img = new Image(imageUrl, 1000, thumbSize, true, true, true); diff --git a/src/main/resources/image_not_found.png b/src/main/resources/image_not_found.png new file mode 100644 index 0000000000000000000000000000000000000000..7ab180c637f3a56607647f8c0a37f658cd3d0ec1 GIT binary patch literal 6827 zcmZ{J2|QH&_y6b4j4`$hk)>iR6(w7w6mv0UDK)7S60Inb63I4$kP4M3ib)FfMD|3e zWZ#L9C|d~GmoUH2<$3z{Jg@)Xc)jM{^ZA@}-e%{8D$cQacGv<>AQwC+f4Z^tqag#}T1>ZQ_?0}!tII12P}73he59sP>hbF`yxa;7GO>Lt$wbB3lgiFSM;=?=mFdNC{pex)x1)rm$9trCNGb|Jy9 z<6G8r_443I&LKmIUR=ggQmRxZG8$?-wR`GC;Q@93cLyT4<)t8nKE?72Ba*zU49kS&dGu z3%IN=bCN{m!IY6bS z5LLP1t-{0!b=EVnI3^iod7*eFkp4Knj~Z%^FQpL3*BYz8FWWbdn(&in7Hlu#bc9#=?pG%Kd0P2BFAHjxpev1=k)*> zsvtOU`_3g-R}C+vLCv#KJuVvOK8u#upZyRd{NDT1E0ya5S8Ihg)21&B1q}%}eow86 zuHIihHt!yyP%iCOklks0q*WdBVYj=hcAZvIJ~`PEGCLE^1&pQGI3%KpB^!6K>KswzdV2t`Z+NebSehpmLgQrgi;s@caq(S-yA4< zP%5?oQeJq0@lycw6XDdK-3A*0ISv&i~pxbvP*h$r&#+#>J(CVn)-4hq@d z5Hi9~Kd885D+Gs|z{@s5#kYIj*-$Epssuqa?kE-5)uFh+xPjtnPJ^4*p|-^RRP-Ak zEp{&$i8`H{K3Nx&^nFG42K&IDi307KOWt-k+WXblx>KdgT730#mQeOMCz>al^f*2~ ze#BjD)hb>op$msHw+*;le2<<#HA+QIb(hOeJv%qIM%M zsGwdwbont{;QA(hP+tqtl&LNt_bRR_BQ*7`GaU3?crhFo<(VlJH0a{FDC-YWi>P4J z*alSs`QT$O(9-~olJ4meK|Ohy1afT?v>P3KKw6F-V9p{5InX9rQK=H62$K?fat=?I z)T1CY50619bRXYIk}M-I(F@205d~4&`)N*=(;h!A%C`ZaWTvAOUnvJzV~fmkZ7p%6KlCT)EBQznL?`idxQQ; z^R;u{&1m2Uvhn{7*Vs-(F56UM^(yay(>MZ<)dwFS(gKN<2mw5)pWUxZqByPvFWYwl zw-X4%Qyqtu#t)AyCDjQm?f35XHg5@JzyPtQp+{7(?1I%ZFw$)RSnmx6Mqy)CVDbwT zAr4+r^?@56irBIwqBy=CdG@yVD+k7YT>Oh1_>?8X5bT~4Va#K77zGEz%43jc#ZPJ$ z?^OIXIKku?iACGW=CE!LUUZ)DDMfB;#}KGUOrD{&{cpBpHe$e3TEc<}dh&ioiQ&A3 zXD#dk7SS3pA+AKH6}y)@1V&H!==Lmbr{-5qC+zvNHxy0mP>Y2|~UgLAsDAb}bov*=z^sni&= za5KL?=S!E&{0=w9CnqcU)wH)edLB~~kTPqN*IgnD|2#4aYle|xq6 zqQ=|-_sDJZTD~6K{P~N%m~MYl|8^o}$(R4}2M)+RQmDT8901u51QIaV)7aM5HvNWo z*)o6fl`B`w=f0Qcww>Od`SsVyr|X=d8xHe^#LSn({<@{&`z}O3O~UAZj}-F%=qP_j zeZDu_e&mr#%xuL$RZ-FlOv;v;N`!?a_%5 zOBnpSjygjB+gEWA>#kUud%wfvP7(bwGkt5vl#K70?=J~zdfwjb3XLsuKi^kx8_wNS zwWagkiC`|NA1xUJY1P`Txo%_b!*8Z)^(6;CrF2GEd2Jmnk%>?+t7~p{TbSu}b#aMy zX|dxGQJkA-vdW+7i3*`ET&{m=Je<#7SQq0lI+)cy)S5M%@Hp7~wR4|CR+|gtit(Im zzAmD4^riLNz0~=1&tH@L+SAsMD@w!h%j-)6Lv1;|l~Gx?wXvtl!#7&xdCZJ<+eEwd zaIJfA{^wgx_RshHdU|>mnf7iFTBd#TXm^Cw!gOayz^V%+*VaW}ZZxxjTywc&Rd7$U zQ}ODEQ_ZgJo>NdWKedCOI`?xRIsZ$VO|^jjDyPxXbukyKw(bg7xIdHZ{ryou($P1w zpH75$4!p>UzWnW4la;sU!raUu%S#`PHQJota){Ws=gl|U#CQ&6_qzP}4yD#J!kvkF+(HO&zin4&p7|}okqLI#rfF@$J~)39}XVVq$Am< zDG(^=l{xx;by?^+3{_ z$(B2NH2WU9ei0KDy*!xFG&$JXR9|1;+Uj1rLuBxC_84@Ich7wre}Dfft*OsLLqjbw z>(4eNJsfSf@}8dvSncu&GS9r@QTod~qyLt^L^D(b<2l*?Jo(Gz&YLiT9WZW=+1=p~ z^VlTx0DIwA{%rsAFJHbGM{RDqSgken#eFz@qs0|>_pEbmE$!{zO_r_>pI?oHTbygo zzSs*R50@})7LIQ&RYz-Kk?Q~W-0Di-lWjw|4h6#9R(yS(Xz#Z&>F-`T}w885GbU zyd0|DkU~}JB6mz%1i-^`uNf;bRUxW=a4>>#dO)u0$BCT0QWmcDVZG&jVI<6Ku=r|h zu*_mLB-0g(s3;GKWs+;D+=3VcC^1%`pcA$~e;4sIYtVOo0iJ_5I_E?d3j+PgRvuy~ zt~VdPdvB@oMsRWyuqF8PUWReoTH2`WhWE=XeawHX!h5%Hi_OGwMWe_E>~~%giznc1 zkBinOMw&Og4VcMn0qg_}6l+%m*50sD@ez}U+IW7sEIP<&;igZbJo@9vf&v2+!D0;C z@$p7jv(FRgf)%?GH+M?@s1`sk;}PrOo8Vm(5x8F#3O~fRb3mL$ zRM_3P_@lr{WnoFDf4d3V08!{yEfBom)+NdR*}5yg%Bxewbz2*Iq<}Bz z)kqlstD%+EUmeZIZ`xP}MV|W{ck2G2Nx(}(pR8ZG?INO+%@$1>Mygcpe z8gOP&hxJ7yTwu6*O&M7?04_%HhIluGEY@tb<~;jT zq+?zSsZW5dN+*05hazkrvCB>U$;&nqaJEvAMl-XeFiacyaeu z-N7^&bsd75R;WU^hCOF4D`i{DgUG^+YFyx`<^epS_QH#dhE=gO1&B2#p>rX%f=pu3 zu}hO*Yks|fA_Xx;Zu1Z3wc-3ks2JnDwyrL*r!9?B0yA@!O*ME+qj%kCg4P?OO+7X`rtYgoWPKM@QktCIv4PG ztm!Wz%gdi_J$EM(YiGz2+jBJerTn(@OTo`CZ@TRzK%T-(3lU}aWuro8C{Cw8h?93I- zi5He<`Aw_9nOv_Mw_@v$am9}1{H$YY^_Q?mN7!3tt;R;!mph7@z3nSQG}n4N*i9CX zXheVW8Z~s?_I_utbLV}DW0e}qe(_UNw!d>?U#g3YU<*`vbdFNr-H+)q_VhcD=@qET zfUTE!e#G0iZ^^n+uBUR+&Sf387|R#6qcFPq;|~Y>3OF>WqkZO)gAxvu=SZ9xd}}Nf zM6YSoxUN79!e<50xX-9JuM`B4A%sdFne_$V7%yg=;9{H?3Iuj4`FsgiN)<=BVLW`n z@mEY2I|@2aVHmt*=4Fu3?nFrfdHKWWK`5e}U1tJ>JKxz5$Z4(Nl8yq5ppX0{%2W02 z1a7#x_oX;`w;#T;A%zuuIphEdNaS#Jlv-zg)m?PleI=!dIncCRw;-HZ@*q9Q54H7lrROmV{ z)rymovvndBF?t?4&fMz6ePi>%1o(utfDClmb{qSjOV(V*>RO=9iZbbE+@*N z6V-21)+AH-F8Lj0h$#NcebbMtI^~XG zAE=WyP^|w3YOCWFZ?h_=$jN$oG+Z~jzHDR9h+`0=Lhb6_|jCSp7U;R;r* zp3lHzLs`!T_=JgSaQl=s!7(RU_wUK@H+KeLbe)86XgAw;P|6$kMl0uA0risDsd34ft;@oll&Sc2^^uvxjRD=UH7{L9FE^!BXr|xU7!Tjii{RC zczcnT`9v_~%R8ISax%k0=Hf1Nzy#22tsZ zDWjD35QVew)iOQ(W@LnV8Ei?=9h#+ya;%#@X1M>)tMuO;D(hhC_Uacn5Gu5s0)K76 ze#PmWs!M9`JCT!l_Md+X&c#L82L9RCQUD`ij@qSqDfI{U^AiC68SdCaOVYLW`9BZH B{=onM literal 0 HcmV?d00001 From 97e2338cec0f0d4ff5d87e82d9f22d58caa0c7c7 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 4 Nov 2018 13:24:34 +0100 Subject: [PATCH 37/42] Move getSegmentPlaylistUrl from AbstractModel to AbstractDownload --- src/main/java/ctbrec/AbstractModel.java | 18 ------------------ src/main/java/ctbrec/Model.java | 1 - .../recorder/download/AbstractHlsDownload.java | 16 ++++++++++++++++ .../ctbrec/recorder/download/HlsDownload.java | 2 +- .../recorder/download/MergedHlsDownload.java | 2 +- 5 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src/main/java/ctbrec/AbstractModel.java b/src/main/java/ctbrec/AbstractModel.java index 078c8cc8..bf0395d0 100644 --- a/src/main/java/ctbrec/AbstractModel.java +++ b/src/main/java/ctbrec/AbstractModel.java @@ -2,17 +2,12 @@ package ctbrec; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; -import com.iheartradio.m3u8.ParseException; -import com.iheartradio.m3u8.PlaylistException; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; -import ctbrec.recorder.download.StreamSource; - public abstract class AbstractModel implements Model { private String url; @@ -87,19 +82,6 @@ public abstract class AbstractModel implements Model { this.streamUrlIndex = streamUrlIndex; } - @Override - public String getSegmentPlaylistUrl() throws IOException, ExecutionException, ParseException, PlaylistException { - List streamSources = getStreamSources(); - String url = null; - if(getStreamUrlIndex() >= 0 && getStreamUrlIndex() < streamSources.size()) { - url = streamSources.get(getStreamUrlIndex()).getMediaPlaylistUrl(); - } else { - Collections.sort(streamSources); - url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl(); - } - return url; - } - @Override public void readSiteSpecificData(JsonReader reader) throws IOException { // noop default implementation, can be overriden by concrete models diff --git a/src/main/java/ctbrec/Model.java b/src/main/java/ctbrec/Model.java index 2db582a3..351dda3e 100644 --- a/src/main/java/ctbrec/Model.java +++ b/src/main/java/ctbrec/Model.java @@ -29,7 +29,6 @@ public interface Model { public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException; public String getOnlineState(boolean failFast) throws IOException, ExecutionException; public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException; - public String getSegmentPlaylistUrl() throws IOException, ExecutionException, ParseException, PlaylistException; public void invalidateCacheEntries(); public void receiveTip(int tokens) throws IOException; public int[] getStreamResolution(boolean failFast) throws ExecutionException; diff --git a/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java b/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java index 787ca938..0bf3d61e 100644 --- a/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java +++ b/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java @@ -6,7 +6,9 @@ import java.io.InputStream; import java.net.URL; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -20,6 +22,7 @@ import com.iheartradio.m3u8.data.MediaPlaylist; import com.iheartradio.m3u8.data.Playlist; import com.iheartradio.m3u8.data.TrackData; +import ctbrec.Model; import ctbrec.io.HttpClient; import okhttp3.Request; import okhttp3.Response; @@ -69,6 +72,19 @@ public abstract class AbstractHlsDownload implements Download { } } + + String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException { + List streamSources = model.getStreamSources(); + String url = null; + if(model.getStreamUrlIndex() >= 0 && model.getStreamUrlIndex() < streamSources.size()) { + url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl(); + } else { + Collections.sort(streamSources); + url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl(); + } + return url; + } + @Override public boolean isAlive() { return alive; diff --git a/src/main/java/ctbrec/recorder/download/HlsDownload.java b/src/main/java/ctbrec/recorder/download/HlsDownload.java index 313b0c0e..6974056a 100644 --- a/src/main/java/ctbrec/recorder/download/HlsDownload.java +++ b/src/main/java/ctbrec/recorder/download/HlsDownload.java @@ -48,7 +48,7 @@ public class HlsDownload extends AbstractHlsDownload { throw new IOException(model.getName() +"'s room is not public"); } - String segments = model.getSegmentPlaylistUrl(); + String segments = getSegmentPlaylistUrl(model); if(segments != null) { if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) { Files.createDirectories(downloadDir); diff --git a/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java b/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java index f122b895..51e7b422 100644 --- a/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java +++ b/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java @@ -101,7 +101,7 @@ public class MergedHlsDownload extends AbstractHlsDownload { target = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-00000.ts")); } - String segments = model.getSegmentPlaylistUrl(); + String segments = getSegmentPlaylistUrl(model); mergeThread = createMergeThread(target, null, true); mergeThread.start(); if(segments != null) { From 9da580c6e808f941fbc0cf39a34b96cd8605b0ff Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 4 Nov 2018 13:37:27 +0100 Subject: [PATCH 38/42] Add setting to define a maximum resolution for recordings --- src/main/java/ctbrec/Settings.java | 1 + .../download/AbstractHlsDownload.java | 24 ++++++- src/main/java/ctbrec/ui/SettingsTab.java | 68 +++++++++++++------ 3 files changed, 72 insertions(+), 21 deletions(-) diff --git a/src/main/java/ctbrec/Settings.java b/src/main/java/ctbrec/Settings.java index db2cbf0c..a8809ddf 100644 --- a/src/main/java/ctbrec/Settings.java +++ b/src/main/java/ctbrec/Settings.java @@ -34,6 +34,7 @@ public class Settings { public boolean determineResolution = false; public boolean requireAuthentication = false; public boolean chooseStreamQuality = false; + public int maximumResolution = 0; public byte[] key = null; public ProxyType proxyType = ProxyType.DIRECT; public String proxyHost; diff --git a/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java b/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java index 0bf3d61e..97bc1bdd 100644 --- a/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java +++ b/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java @@ -7,11 +7,15 @@ import java.net.URL; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.iheartradio.m3u8.Encoding; import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; @@ -22,6 +26,7 @@ import com.iheartradio.m3u8.data.MediaPlaylist; import com.iheartradio.m3u8.data.Playlist; import com.iheartradio.m3u8.data.TrackData; +import ctbrec.Config; import ctbrec.Model; import ctbrec.io.HttpClient; import okhttp3.Request; @@ -29,6 +34,8 @@ import okhttp3.Response; public abstract class AbstractHlsDownload implements Download { + private static final transient Logger LOG = LoggerFactory.getLogger(AbstractHlsDownload.class); + ExecutorService downloadThreadPool = Executors.newFixedThreadPool(5); HttpClient client; volatile boolean running = false; @@ -80,7 +87,22 @@ public abstract class AbstractHlsDownload implements Download { url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl(); } else { Collections.sort(streamSources); - url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl(); + // filter out stream resolutions, which are too high + int maxRes = Config.getInstance().getSettings().maximumResolution; + if(maxRes > 0) { + for (Iterator iterator = streamSources.iterator(); iterator.hasNext();) { + StreamSource streamSource = iterator.next(); + if(streamSource.height > 0 && maxRes < streamSource.height) { + LOG.debug("Res too high {} > {}", streamSource.height, maxRes); + iterator.remove(); + } + } + } + if(streamSources.isEmpty()) { + throw new ExecutionException(new RuntimeException("No stream left in playlist")); + } else { + url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl(); + } } return url; } diff --git a/src/main/java/ctbrec/ui/SettingsTab.java b/src/main/java/ctbrec/ui/SettingsTab.java index 693427d6..f0066613 100644 --- a/src/main/java/ctbrec/ui/SettingsTab.java +++ b/src/main/java/ctbrec/ui/SettingsTab.java @@ -65,6 +65,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private RadioButton recordRemote; private ToggleGroup recordLocation; private ProxySettingsPane proxySettingsPane; + private ComboBox maxResolution; private ComboBox splitAfter; private List sites; private Label restartLabel; @@ -231,8 +232,8 @@ public class SettingsTab extends Tab implements TabSelectionListener { keyDialog.show(); } }); - GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, CHECKBOX_MARGIN, 0, 0)); - GridPane.setMargin(secureCommunication, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); + GridPane.setMargin(l, new Insets(4, CHECKBOX_MARGIN, 0, 0)); + GridPane.setMargin(secureCommunication, new Insets(4, 0, 0, 0)); layout.add(secureCommunication, 1, 3); TitledPane recordLocation = new TitledPane("Record Location", layout); @@ -249,6 +250,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setFillWidth(recordingsDirectory, true); GridPane.setHgrow(recordingsDirectory, Priority.ALWAYS); GridPane.setColumnSpan(recordingsDirectory, 2); + GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(recordingsDirectory, 1, 0); recordingsDirectoryButton = createRecordingsBrowseButton(); layout.add(recordingsDirectoryButton, 3, 0); @@ -259,19 +261,10 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setFillWidth(mediaPlayer, true); GridPane.setHgrow(mediaPlayer, Priority.ALWAYS); GridPane.setColumnSpan(mediaPlayer, 2); + GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN)); layout.add(mediaPlayer, 1, 1); layout.add(createMpvBrowseButton(), 3, 1); - Label l = new Label("Allow multiple players"); - layout.add(l, 0, 2); - multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer); - multiplePlayers.setOnAction((e) -> Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected()); - GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - GridPane.setMargin(l, new Insets(3, 0, 0, 0)); - GridPane.setMargin(multiplePlayers, new Insets(3, 0, 0, CHECKBOX_MARGIN)); - layout.add(multiplePlayers, 1, 2); - TitledPane locations = new TitledPane("Locations", layout); locations.setCollapsible(false); return locations; @@ -279,8 +272,9 @@ public class SettingsTab extends Tab implements TabSelectionListener { private Node createGeneralPanel() { GridPane layout = createGridLayout(); + int row = 0; Label l = new Label("Display stream resolution in overview"); - layout.add(l, 0, 0); + layout.add(l, 0, row); loadResolution = new CheckBox(); loadResolution.setSelected(Config.getInstance().getSettings().determineResolution); loadResolution.setOnAction((e) -> { @@ -291,18 +285,41 @@ public class SettingsTab extends Tab implements TabSelectionListener { }); //GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); GridPane.setMargin(loadResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN)); - layout.add(loadResolution, 1, 0); + layout.add(loadResolution, 1, row++); + + l = new Label("Allow multiple players"); + layout.add(l, 0, row); + multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer); + multiplePlayers.setOnAction((e) -> Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected()); + GridPane.setMargin(l, new Insets(3, 0, 0, 0)); + GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + layout.add(multiplePlayers, 1, row++); l = new Label("Manually select stream quality"); - layout.add(l, 0, 1); + layout.add(l, 0, row); chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality); chooseStreamQuality.setOnAction((e) -> Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected()); GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - layout.add(chooseStreamQuality, 1, 1); + layout.add(chooseStreamQuality, 1, row++); + + l = new Label("Maximum resolution (0 = unlimited)"); + layout.add(l, 0, row); + List resolutionOptions = new ArrayList<>(); + resolutionOptions.add(1080); + resolutionOptions.add(720); + resolutionOptions.add(600); + resolutionOptions.add(480); + resolutionOptions.add(0); + maxResolution = new ComboBox<>(new ObservableListWrapper<>(resolutionOptions)); + setMaxResolutionValue(); + maxResolution.setOnAction((e) -> Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem()); + layout.add(maxResolution, 1, row++); + GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); + GridPane.setMargin(maxResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); l = new Label("Split recordings after (minutes)"); - layout.add(l, 0, 2); + layout.add(l, 0, row); List options = new ArrayList<>(); options.add(new SplitAfterOption("disabled", 0)); options.add(new SplitAfterOption("10 min", 10 * 60)); @@ -311,11 +328,12 @@ public class SettingsTab extends Tab implements TabSelectionListener { options.add(new SplitAfterOption("30 min", 30 * 60)); options.add(new SplitAfterOption("60 min", 60 * 60)); splitAfter = new ComboBox<>(new ObservableListWrapper<>(options)); - layout.add(splitAfter, 1, 2); + layout.add(splitAfter, 1, row++); setSplitAfterValue(); splitAfter.setOnAction((e) -> Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue()); - GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0)); - GridPane.setMargin(splitAfter, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + GridPane.setMargin(l, new Insets(0, 0, 0, 0)); + GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + maxResolution.prefWidthProperty().bind(splitAfter.widthProperty()); TitledPane general = new TitledPane("General", layout); general.setCollapsible(false); @@ -331,6 +349,15 @@ public class SettingsTab extends Tab implements TabSelectionListener { } } + private void setMaxResolutionValue() { + int value = Config.getInstance().getSettings().maximumResolution; + for (Integer option : maxResolution.getItems()) { + if(option == value) { + maxResolution.getSelectionModel().select(option); + } + } + } + void showRestartRequired() { restartLabel.setVisible(true); } @@ -350,6 +377,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { recordingsDirectory.setDisable(!local); recordingsDirectoryButton.setDisable(!local); splitAfter.setDisable(!local); + maxResolution.setDisable(!local); } private ChangeListener createRecordingsDirectoryFocusListener() { From e9b699fddacd6f10f79e3446845326dc9ea65fa4 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 4 Nov 2018 13:40:59 +0100 Subject: [PATCH 39/42] Change log level --- src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java b/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java index 97bc1bdd..84a3744d 100644 --- a/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java +++ b/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java @@ -93,7 +93,7 @@ public abstract class AbstractHlsDownload implements Download { for (Iterator iterator = streamSources.iterator(); iterator.hasNext();) { StreamSource streamSource = iterator.next(); if(streamSource.height > 0 && maxRes < streamSource.height) { - LOG.debug("Res too high {} > {}", streamSource.height, maxRes); + LOG.trace("Res too high {} > {}", streamSource.height, maxRes); iterator.remove(); } } From 5698354f3b1f61fc9ab039994398ed8a28d59e0e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 4 Nov 2018 13:46:11 +0100 Subject: [PATCH 40/42] Update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2638c346..2caaf928 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ 1.6.2 ======================== +* Added CamSoda * Added detection of model name changes for MyFreeCams - +* Added setting to define a maximum resolution 1.6.1 ======================== From 7daffd259472efb85aa85a915937a02dcd369502 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 4 Nov 2018 20:10:34 +0100 Subject: [PATCH 41/42] Fix sorting by date in recordings table --- CHANGELOG.md | 1 + src/main/java/ctbrec/ui/RecordingsTab.java | 28 ++++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2caaf928..bd634e53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * Added CamSoda * Added detection of model name changes for MyFreeCams * Added setting to define a maximum resolution +* Fixed sorting by date in recordings table 1.6.1 ======================== diff --git a/src/main/java/ctbrec/ui/RecordingsTab.java b/src/main/java/ctbrec/ui/RecordingsTab.java index fc3acd64..e64184f0 100644 --- a/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/src/main/java/ctbrec/ui/RecordingsTab.java @@ -34,7 +34,7 @@ import ctbrec.recorder.Recorder; import ctbrec.recorder.download.MergedHlsDownload; import ctbrec.sites.Site; import javafx.application.Platform; -import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.concurrent.ScheduledService; @@ -47,6 +47,7 @@ import javafx.scene.control.ContextMenu; import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; +import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.cell.PropertyValueFactory; @@ -57,6 +58,7 @@ import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.FlowPane; import javafx.stage.FileChooser; +import javafx.util.Callback; import javafx.util.Duration; public class RecordingsTab extends Tab implements TabSelectionListener { @@ -99,12 +101,28 @@ public class RecordingsTab extends Tab implements TabSelectionListener { TableColumn name = new TableColumn<>("Model"); name.setPrefWidth(200); name.setCellValueFactory(new PropertyValueFactory("modelName")); - TableColumn date = new TableColumn<>("Date"); + TableColumn date = new TableColumn<>("Date"); date.setCellValueFactory((cdf) -> { Instant instant = cdf.getValue().getStartDate(); - ZonedDateTime time = instant.atZone(ZoneId.systemDefault()); - DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM); - return new SimpleStringProperty(dtf.format(time)); + return new SimpleObjectProperty(instant); + }); + date.setCellFactory(new Callback, TableCell>() { + @Override + public TableCell call(TableColumn param) { + TableCell cell = new TableCell() { + @Override + protected void updateItem(Instant instant, boolean empty) { + if(empty || instant == null) { + setText(null); + } else { + ZonedDateTime time = instant.atZone(ZoneId.systemDefault()); + DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM); + setText(dtf.format(time)); + } + } + }; + return cell; + } }); date.setPrefWidth(200); TableColumn status = new TableColumn<>("Status"); From a3e0c18cf9ba7f7a5c7286239c816e08fcd2728a Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 4 Nov 2018 21:38:38 +0100 Subject: [PATCH 42/42] Bumb version to 1.7.0 --- CHANGELOG.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd634e53..5a025d00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -1.6.2 +1.7.0 ======================== * Added CamSoda * Added detection of model name changes for MyFreeCams diff --git a/pom.xml b/pom.xml index 77b1df4a..5994a6db 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 ctbrec ctbrec - 1.6.1 + 1.7.0 UTF-8