From 713328303293ae8ac726d337ea102502ab18e2db Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 19 Nov 2018 23:20:39 +0100 Subject: [PATCH 01/26] Start implementation for FC2Live --- .../java/ctbrec/ui/CamrecApplication.java | 2 + .../main/java/ctbrec/ui/SiteUiFactory.java | 8 + .../ctbrec/ui/sites/fc2live/Fc2LiveUi.java | 35 +++ .../ui/sites/fc2live/Fc2TabProvider.java | 38 +++ .../ui/sites/fc2live/Fc2UpdateService.java | 86 +++++++ .../src/main/java/ctbrec/io/HttpClient.java | 7 + .../ctbrec/sites/fc2live/Fc2HttpClient.java | 106 +++++++++ .../java/ctbrec/sites/fc2live/Fc2Live.java | 91 +++++++ .../java/ctbrec/sites/fc2live/Fc2Model.java | 225 ++++++++++++++++++ .../sites/fc2live/Fc2WebSocketClient.java | 76 ++++++ 10 files changed, 674 insertions(+) create mode 100644 client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveUi.java create mode 100644 client/src/main/java/ctbrec/ui/sites/fc2live/Fc2TabProvider.java create mode 100644 client/src/main/java/ctbrec/ui/sites/fc2live/Fc2UpdateService.java create mode 100644 common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java create mode 100644 common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java create mode 100644 common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java create mode 100644 common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 9a4e3eb1..27f35fb7 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -31,6 +31,7 @@ import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; +import ctbrec.sites.fc2live.Fc2Live; import ctbrec.sites.mfc.MyFreeCams; import javafx.application.Application; import javafx.application.HostServices; @@ -66,6 +67,7 @@ public class CamrecApplication extends Application { sites.add(new Cam4()); sites.add(new Camsoda()); sites.add(new Chaturbate()); + sites.add(new Fc2Live()); sites.add(new MyFreeCams()); loadConfig(); createHttpClient(); diff --git a/client/src/main/java/ctbrec/ui/SiteUiFactory.java b/client/src/main/java/ctbrec/ui/SiteUiFactory.java index 94352c1e..ef1ad64c 100644 --- a/client/src/main/java/ctbrec/ui/SiteUiFactory.java +++ b/client/src/main/java/ctbrec/ui/SiteUiFactory.java @@ -5,11 +5,13 @@ import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; +import ctbrec.sites.fc2live.Fc2Live; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.ui.sites.bonga.BongaCamsSiteUi; import ctbrec.ui.sites.cam4.Cam4SiteUi; import ctbrec.ui.sites.camsoda.CamsodaSiteUi; import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi; +import ctbrec.ui.sites.fc2live.Fc2LiveUi; import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi; public class SiteUiFactory { @@ -18,6 +20,7 @@ public class SiteUiFactory { private static Cam4SiteUi cam4SiteUi; private static CamsodaSiteUi camsodaSiteUi; private static ChaturbateSiteUi ctbSiteUi; + private static Fc2LiveUi fc2SiteUi; private static MyFreeCamsSiteUi mfcSiteUi; public static SiteUI getUi(Site site) { @@ -41,6 +44,11 @@ public class SiteUiFactory { ctbSiteUi = new ChaturbateSiteUi((Chaturbate) site); } return ctbSiteUi; + } else if (site instanceof Fc2Live) { + if (fc2SiteUi == null) { + fc2SiteUi = new Fc2LiveUi((Fc2Live) site); + } + return fc2SiteUi; } else if (site instanceof MyFreeCams) { if (mfcSiteUi == null) { mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site); diff --git a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveUi.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveUi.java new file mode 100644 index 00000000..bc7a61f2 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveUi.java @@ -0,0 +1,35 @@ +package ctbrec.ui.sites.fc2live; + +import java.io.IOException; + +import ctbrec.sites.ConfigUI; +import ctbrec.sites.fc2live.Fc2Live; +import ctbrec.ui.SiteUI; +import ctbrec.ui.TabProvider; + +public class Fc2LiveUi implements SiteUI { + + private Fc2Live fc2live; + private Fc2TabProvider tabProvider; + + public Fc2LiveUi(Fc2Live fc2live) { + this.fc2live = fc2live; + this.tabProvider = new Fc2TabProvider(fc2live); + } + + @Override + public TabProvider getTabProvider() { + return tabProvider; + } + + @Override + public ConfigUI getConfigUI() { + return null; + } + + @Override + public boolean login() throws IOException { + return false; + } + +} diff --git a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2TabProvider.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2TabProvider.java new file mode 100644 index 00000000..d6e448c9 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2TabProvider.java @@ -0,0 +1,38 @@ +package ctbrec.ui.sites.fc2live; + +import java.util.ArrayList; +import java.util.List; + +import ctbrec.sites.fc2live.Fc2Live; +import ctbrec.ui.TabProvider; +import ctbrec.ui.ThumbOverviewTab; +import javafx.scene.Scene; +import javafx.scene.control.Tab; + +public class Fc2TabProvider extends TabProvider { + + private Fc2Live fc2live; + + public Fc2TabProvider(Fc2Live fc2live) { + this.fc2live = fc2live; + } + + @Override + public List getTabs(Scene scene) { + List tabs = new ArrayList<>(); + tabs.add(createTab("Online", Fc2Live.BASE_URL + "/adult/contents/allchannellist.php")); + return tabs; + } + + private Tab createTab(String title, String url) { + Fc2UpdateService updateService = new Fc2UpdateService(url, fc2live); + ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, fc2live); + tab.setRecorder(fc2live.getRecorder()); + return tab; + } + + @Override + public Tab getFollowedTab() { + return null; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2UpdateService.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2UpdateService.java new file mode 100644 index 00000000..46c8cba7 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2UpdateService.java @@ -0,0 +1,86 @@ +package ctbrec.ui.sites.fc2live; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.sites.fc2live.Fc2Live; +import ctbrec.sites.fc2live.Fc2Model; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class Fc2UpdateService extends PaginatedScheduledService { + private static final transient Logger LOG = LoggerFactory.getLogger(Fc2UpdateService.class); + + private String url; + private Fc2Live fc2live; + private int modelsPerPage = 30; + + public Fc2UpdateService(String url, Fc2Live fc2live) { + this.url = url; + this.fc2live = fc2live; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + RequestBody body = RequestBody.create(null, new byte[0]); + Request req = new Request.Builder() + .url(url) + .method("POST", body) + .header("Accept", "*/*") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Referer", Fc2Live.BASE_URL) + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .header("X-Requested-With", "XMLHttpRequest") + .build(); + LOG.debug("Fetching page {}", url); + try(Response resp = fc2live.getHttpClient().execute(req)) { + if(resp.isSuccessful()) { + List models = new ArrayList<>(); + String msg = resp.body().string(); + JSONObject json = new JSONObject(msg); + JSONArray channels = json.getJSONArray("channel"); + for (int i = 0; i < channels.length(); i++) { + JSONObject channel = channels.getJSONObject(i); + Fc2Model model = (Fc2Model) fc2live.createModel(channel.getString("name")); + model.setId(channel.getString("id")); + model.setUrl(Fc2Live.BASE_URL + '/' + model.getId()); + String previewUrl = channel.getString("image"); + if(previewUrl == null || previewUrl.trim().isEmpty()) { + previewUrl = getClass().getResource("/image_not_found.png").toString(); + } + model.setPreview(previewUrl); + model.setDescription(channel.optString("title")); + model.setViewerCount(channel.optInt("count")); + if(channel.getInt("login") == 0) { + models.add(model); + } + } + return models.stream() + .sorted((m1, m2) -> m2.getViewerCount() - m1.getViewerCount()) + .skip( (page - 1) * modelsPerPage) + .limit(modelsPerPage) + .collect(Collectors.toList()); + } else { + resp.close(); + throw new IOException("HTTP status " + resp.code() + " " + resp.message()); + } + } + } + }; + } +} diff --git a/common/src/main/java/ctbrec/io/HttpClient.java b/common/src/main/java/ctbrec/io/HttpClient.java index df03cf52..2cc40372 100644 --- a/common/src/main/java/ctbrec/io/HttpClient.java +++ b/common/src/main/java/ctbrec/io/HttpClient.java @@ -30,6 +30,8 @@ import okhttp3.OkHttpClient.Builder; import okhttp3.Request; import okhttp3.Response; import okhttp3.Route; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; public abstract class HttpClient { private static final transient Logger LOG = LoggerFactory.getLogger(HttpClient.class); @@ -215,4 +217,9 @@ public abstract class HttpClient { public CookieJar getCookieJar() { return cookieJar; } + + public WebSocket newWebSocket(String url, WebSocketListener l) { + Request request = new Request.Builder().url(url).build(); + return client.newWebSocket(request, l); + } } diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java new file mode 100644 index 00000000..db6139d1 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java @@ -0,0 +1,106 @@ +package ctbrec.sites.fc2live; + +import java.io.IOException; + +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.io.HtmlParser; +import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; +import ctbrec.sites.mfc.MyFreeCams; +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; + +public class Fc2HttpClient extends HttpClient { + + private static final transient Logger LOG = LoggerFactory.getLogger(Fc2HttpClient.class); + + public Fc2HttpClient() { + super("fc2live"); + } + + @Override + public boolean login() throws IOException { + if(loggedIn) { + return true; + } + + if(checkLogin()) { + loggedIn = true; + LOG.debug("Logged in with cookies"); + return true; + } + + String username = Config.getInstance().getSettings().mfcUsername; + String password = Config.getInstance().getSettings().mfcPassword; + RequestBody body = new FormBody.Builder() + .add("username", username) + .add("password", password) + .add("tz", "2") + .add("ss", "1920x1080") + .add("submit_login", "97") + .build(); + Request req = new Request.Builder() + .url(MyFreeCams.BASE_URI + "/php/login.php") + .header("Referer", MyFreeCams.BASE_URI) + .header("Content-Type", "application/x-www-form-urlencoded") + .post(body) + .build(); + Response resp = execute(req); + if(resp.isSuccessful()) { + String page = resp.body().string(); + if(page.contains("Your username or password are incorrect")) { + return false; + } else { + loggedIn = true; + return true; + } + } else { + resp.close(); + LOG.error("Login failed {} {}", resp.code(), resp.message()); + return false; + } + } + + private boolean checkLogin() throws IOException { + Request req = new Request.Builder().url(MyFreeCams.BASE_URI + "/php/account.php?request=status").build(); + try(Response response = execute(req)) { + if(response.isSuccessful()) { + String content = response.body().string(); + try { + Elements tags = HtmlParser.getTags(content, "div.content > p > b"); + tags.get(2).text(); + return true; + } catch(Exception e) { + LOG.debug("Token tag not found. Login failed"); + return false; + } + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + public WebSocket newWebSocket(Request req, WebSocketListener webSocketListener) { + return client.newWebSocket(req, webSocketListener); + } + + // public Cookie getCookie(String name) { + // CookieJar jar = client.cookieJar(); + // HttpUrl url = HttpUrl.parse(MyFreeCams.BASE_URI); + // List cookies = jar.loadForRequest(url); + // for (Cookie cookie : cookies) { + // if(Objects.equals(cookie.name(), name)) { + // return cookie; + // } + // } + // throw new NoSuchElementException("No cookie with name " + name); + // } +} diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java new file mode 100644 index 00000000..9bf8aab2 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java @@ -0,0 +1,91 @@ +package ctbrec.sites.fc2live; + +import java.io.IOException; + +import ctbrec.Model; +import ctbrec.io.HttpClient; +import ctbrec.sites.AbstractSite; + +public class Fc2Live extends AbstractSite { + + public static final String BASE_URL = "https://live.fc2.com"; + private Fc2HttpClient httpClient; + + @Override + public String getName() { + return "FC2Live"; + } + + @Override + public String getBaseUrl() { + return BASE_URL; + } + + @Override + public String getAffiliateLink() { + return BASE_URL; + } + + @Override + public Model createModel(String name) { + Fc2Model model = new Fc2Model(); + model.setSite(this); + model.setName(name); + return model; + } + + @Override + public Integer getTokenBalance() throws IOException { + return 0; + } + + @Override + public String getBuyTokensLink() { + return BASE_URL; + } + + @Override + public boolean login() throws IOException { + return false; + } + + @Override + public HttpClient getHttpClient() { + if(httpClient == null) { + httpClient = new Fc2HttpClient(); + } + return httpClient; + } + + @Override + public void init() throws IOException { + } + + @Override + public void shutdown() { + if(httpClient != null) { + httpClient.shutdown(); + } + } + + @Override + public boolean supportsTips() { + return false; + } + + @Override + public boolean supportsFollow() { + return false; + } + + @Override + public boolean isSiteForModel(Model m) { + return m instanceof Fc2Model; + } + + @Override + public boolean credentialsAvailable() { + return false; + } + +} diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java new file mode 100644 index 00000000..fb263585 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java @@ -0,0 +1,225 @@ +package ctbrec.sites.fc2live; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.function.BiConsumer; + +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.Config; +import ctbrec.io.HttpException; +import ctbrec.recorder.download.StreamSource; +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class Fc2Model extends AbstractModel { + private static final transient Logger LOG = LoggerFactory.getLogger(Fc2Model.class); + private String id; + private int viewerCount; + private boolean online; + private String onlineState = "n/a"; + private String version; + + @Override + public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { + if(ignoreCache) { + loadModelInfo(); + } + return online; + } + + private void loadModelInfo() throws IOException { + String url = Fc2Live.BASE_URL + "/api/memberApi.php"; + RequestBody body = new FormBody.Builder() + .add("channel", "1") + .add("profile", "1") + .add("streamid", id) + .build(); + Request req = new Request.Builder() + .url(url) + .method("POST", body) + .header("Accept", "*/*") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Referer", Fc2Live.BASE_URL) + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .header("X-Requested-With", "XMLHttpRequest") + .build(); + LOG.debug("Fetching page {}", url); + try(Response resp = getSite().getHttpClient().execute(req)) { + if(resp.isSuccessful()) { + String msg = resp.body().string(); + JSONObject json = new JSONObject(msg); + JSONObject data = json.getJSONObject("data"); + JSONObject channelData = data.getJSONObject("channel_data"); + online = channelData.optInt("is_publish") == 1; + onlineState = online ? "online" : "offline"; + version = channelData.optString("version"); + } else { + resp.close(); + throw new IOException("HTTP status " + resp.code() + " " + resp.message()); + } + } + } + + @Override + public String getOnlineState(boolean failFast) throws IOException, ExecutionException { + if(failFast) { + return onlineState; + } else if(Objects.equals(onlineState, "n/a")){ + loadModelInfo(); + } + return onlineState; + } + + @Override + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + loadModelInfo(); + List sources = new ArrayList<>(); + getControlToken((token, url) -> { + url = url + "?control_token=" + token; + LOG.debug("Session token: {}", token); + LOG.debug("Getting playlist token over websocket {}", url); + Fc2WebSocketClient wsClient = new Fc2WebSocketClient(url, getSite().getHttpClient()); + try { + String playlistUrl = wsClient.getPlaylistUrl(); + LOG.debug("Paylist url {}", playlistUrl); + sources.addAll(parseMasterPlaylist(playlistUrl)); + } catch (InterruptedException | IOException | ParseException | PlaylistException e) { + LOG.error("Couldn't fetch stream information", e); + } + }); + return sources; + } + + private List parseMasterPlaylist(String playlistUrl) throws IOException, ParseException, PlaylistException { + List sources = new ArrayList<>(); + Request req = new Request.Builder() + .url(playlistUrl) + .header("Accept", "*/*") + .header("Accept-Language", "en-US,en;q=0.5") + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .header("Origin", Fc2Live.BASE_URL) + .header("Referer", getUrl()) + .build(); + try(Response response = site.getHttpClient().execute(req)) { + if(response.isSuccessful()) { + InputStream inputStream = response.body().byteStream(); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); + Playlist playlist = parser.parse(); + MasterPlaylist master = playlist.getMasterPlaylist(); + sources.clear(); + for (PlaylistData playlistData : master.getPlaylists()) { + StreamSource streamsource = new StreamSource(); + streamsource.mediaPlaylistUrl = 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; + } + sources.add(streamsource); + } + LOG.debug(sources.toString()); + return sources; + } else { + throw new HttpException(response.code(), response.message()); + } + } + } + + private void getControlToken(BiConsumer callback) throws IOException { + String url = Fc2Live.BASE_URL + "/api/getControlServer.php"; + RequestBody body = new FormBody.Builder() + .add("channel_id", id) + .add("channel_version", version) + .add("client_app", "browser_hls") + .add("client_type", "pc") + .add("client_version", "1.6.0 [1]") + .add("mode", "play") + .build(); + Request req = new Request.Builder() + .url(url) + .method("POST", body) + .header("Accept", "*/*") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Referer", Fc2Live.BASE_URL) + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .header("X-Requested-With", "XMLHttpRequest") + .build(); + LOG.debug("Fetching page {}", url); + try(Response resp = getSite().getHttpClient().execute(req)) { + if(resp.isSuccessful()) { + String msg = resp.body().string(); + JSONObject json = new JSONObject(msg); + String wssurl = json.getString("url"); + String token = json.getString("control_token"); + callback.accept(token, wssurl); + } else { + resp.close(); + throw new IOException("HTTP status " + resp.code() + " " + resp.message()); + } + } + } + + @Override + public void invalidateCacheEntries() { + } + + @Override + public void receiveTip(int tokens) throws IOException { + } + + @Override + public int[] getStreamResolution(boolean failFast) throws ExecutionException { + return new int[2]; + } + + @Override + public boolean follow() throws IOException { + return false; + } + + @Override + public boolean unfollow() throws IOException { + return false; + } + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public int getViewerCount() { + return viewerCount; + } + + public void setViewerCount(int viewerCount) { + this.viewerCount = viewerCount; + } +} diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java new file mode 100644 index 00000000..32965998 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java @@ -0,0 +1,76 @@ +package ctbrec.sites.fc2live; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.io.HttpClient; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +public class Fc2WebSocketClient { + + private static final transient Logger LOG = LoggerFactory.getLogger(Fc2WebSocketClient.class); + private String url; + private HttpClient client; + + public Fc2WebSocketClient(String url, HttpClient client) { + this.url = url; + this.client = client; + } + + String playlistUrl = ""; + public String getPlaylistUrl() throws InterruptedException { + LOG.debug("Connecting to {}", url); + Object monitor = new Object(); + client.newWebSocket(url, new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, Response response) { + response.close(); + webSocket.send("{\"name\":\"get_hls_information\",\"arguments\":{},\"id\":1}"); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + JSONObject json = new JSONObject(text); + if(json.optString("name").equals("_response_") && json.optInt("id") == 1) { + LOG.debug(json.toString(2)); + JSONObject args = json.getJSONObject("arguments"); + JSONArray playlists = args.getJSONArray("playlists_high_latency"); + JSONObject playlist = playlists.getJSONObject(0); + playlistUrl = playlist.getString("url"); + webSocket.close(1000, ""); + } + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + LOG.debug("ws btxt {}", bytes.toString()); + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + synchronized (monitor) { + monitor.notify(); + } + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + LOG.debug("ws failure", t); + response.close(); + synchronized (monitor) { + monitor.notify(); + } + } + }); + synchronized (monitor) { + monitor.wait(); + } + return playlistUrl; + } +} + From 2f09b1d5177c5f6751322cb8b639125e92e7e679 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 18 Dec 2018 13:18:49 +0100 Subject: [PATCH 02/26] Add own download class to manage the websocket connection --- .../download/AbstractHlsDownload.java | 23 ++++++++++----- .../java/ctbrec/sites/fc2live/Fc2Live.java | 4 +-- .../sites/fc2live/Fc2MergedHlsDownload.java | 28 +++++++++++++++++++ .../java/ctbrec/sites/fc2live/Fc2Model.java | 2 +- .../sites/fc2live/Fc2WebSocketClient.java | 8 +++--- 5 files changed, 51 insertions(+), 14 deletions(-) create mode 100644 common/src/main/java/ctbrec/sites/fc2live/Fc2MergedHlsDownload.java diff --git a/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java index b4ab0507..20ab1f95 100644 --- a/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java @@ -29,6 +29,7 @@ import ctbrec.Config; import ctbrec.Model; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; +import ctbrec.sites.fc2live.Fc2Live; import okhttp3.Request; import okhttp3.Response; @@ -47,9 +48,17 @@ public abstract class AbstractHlsDownload implements Download { this.client = client; } - SegmentPlaylist getNextSegments(String segments) throws IOException, ParseException, PlaylistException { + protected SegmentPlaylist getNextSegments(String segments) throws IOException, ParseException, PlaylistException { URL segmentsUrl = new URL(segments); - Request request = new Request.Builder().url(segmentsUrl).addHeader("connection", "keep-alive").build(); + Request request = new Request.Builder() + .url(segmentsUrl) + .header("Accept", "*/*") + .header("Accept-Language", "en-US,en;q=0.5") + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .header("Origin", Fc2Live.BASE_URL) + .header("Referer", Fc2Live.BASE_URL) + .header("Connection", "keep-alive") + .build(); try(Response response = client.execute(request)) { if(response.isSuccessful()) { // String body = response.body().string(); @@ -69,11 +78,11 @@ public abstract class AbstractHlsDownload implements Download { if(!uri.startsWith("http")) { String _url = segmentsUrl.toString(); _url = _url.substring(0, _url.lastIndexOf('/') + 1); - String segmentUri = _url + uri; - lsp.totalDuration += trackData.getTrackInfo().duration; - lsp.lastSegDuration = trackData.getTrackInfo().duration; - lsp.segments.add(segmentUri); + uri = _url + uri; } + lsp.totalDuration += trackData.getTrackInfo().duration; + lsp.lastSegDuration = trackData.getTrackInfo().duration; + lsp.segments.add(uri); } return lsp; } @@ -85,7 +94,7 @@ public abstract class AbstractHlsDownload implements Download { } - String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException { + protected String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException { LOG.debug("{} stream idx: {}", model.getName(), model.getStreamUrlIndex()); List streamSources = model.getStreamSources(); Collections.sort(streamSources); diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java index 9bf8aab2..787d0801 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java @@ -23,7 +23,7 @@ public class Fc2Live extends AbstractSite { @Override public String getAffiliateLink() { - return BASE_URL; + return BASE_URL + "/?afid=98987181"; } @Override @@ -41,7 +41,7 @@ public class Fc2Live extends AbstractSite { @Override public String getBuyTokensLink() { - return BASE_URL; + return getAffiliateLink(); } @Override diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2MergedHlsDownload.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2MergedHlsDownload.java new file mode 100644 index 00000000..d49439f0 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2MergedHlsDownload.java @@ -0,0 +1,28 @@ +package ctbrec.sites.fc2live; + +import java.io.IOException; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HttpClient; +import ctbrec.recorder.download.MergedHlsDownload; + +public class Fc2MergedHlsDownload extends MergedHlsDownload { + + private Fc2WebSocketClient ws; + + public Fc2MergedHlsDownload(HttpClient client) { + super(client); + + } + + @Override + public void start(Model model, Config config) throws IOException { + super.start(model, config); + } + + @Override + public void stop() { + super.stop(); + } +} diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java index 05afaeeb..f6a6975b 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java @@ -83,7 +83,7 @@ public class Fc2Model extends AbstractModel { public State getOnlineState(boolean failFast) throws IOException, ExecutionException { if(failFast) { return onlineState; - } else if(Objects.equals(onlineState, "n/a")){ + } else if(Objects.equals(onlineState, State.UNKNOWN)){ loadModelInfo(); } return onlineState; diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java index 32965998..90fc073e 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java @@ -42,7 +42,9 @@ public class Fc2WebSocketClient { JSONArray playlists = args.getJSONArray("playlists_high_latency"); JSONObject playlist = playlists.getJSONObject(0); playlistUrl = playlist.getString("url"); - webSocket.close(1000, ""); + synchronized (monitor) { + monitor.notify(); + } } } @@ -53,9 +55,7 @@ public class Fc2WebSocketClient { @Override public void onClosed(WebSocket webSocket, int code, String reason) { - synchronized (monitor) { - monitor.notify(); - } + LOG.debug("ws closed {} - {}", code, reason); } @Override From 3a839431305ce11c0ba23b1bd061f6942fb0a88e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 21 Jan 2019 15:31:13 +0100 Subject: [PATCH 03/26] Let SiteUi launch the player for a model stream This is done, because there might be some special conditions for the player to work. For example on fc2live a websocket has to be open while the player is running. The SiteUI can handle these cases transparently. --- client/src/main/java/ctbrec/ui/Player.java | 11 +++++++++++ client/src/main/java/ctbrec/ui/SiteUI.java | 2 ++ .../main/java/ctbrec/ui/SiteUiFactory.java | 6 +++--- .../java/ctbrec/ui/action/PlayAction.java | 6 ++++-- .../java/ctbrec/ui/sites/AbstractSiteUi.java | 12 ++++++++++++ .../ui/sites/bonga/BongaCamsSiteUi.java | 4 ++-- .../java/ctbrec/ui/sites/cam4/Cam4SiteUi.java | 4 ++-- .../ui/sites/camsoda/CamsodaSiteUi.java | 4 ++-- .../ui/sites/chaturbate/ChaturbateSiteUi.java | 4 ++-- .../{Fc2LiveUi.java => Fc2LiveSiteUi.java} | 19 ++++++++++++++++--- .../ui/sites/jasmin/LiveJasminSiteUi.java | 4 ++-- .../ui/sites/myfreecams/MyFreeCamsSiteUi.java | 4 ++-- .../ui/sites/streamate/StreamateSiteUi.java | 4 ++-- 13 files changed, 62 insertions(+), 22 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/AbstractSiteUi.java rename client/src/main/java/ctbrec/ui/sites/fc2live/{Fc2LiveUi.java => Fc2LiveSiteUi.java} (58%) diff --git a/client/src/main/java/ctbrec/ui/Player.java b/client/src/main/java/ctbrec/ui/Player.java index bace7e78..87a9e6b2 100644 --- a/client/src/main/java/ctbrec/ui/Player.java +++ b/client/src/main/java/ctbrec/ui/Player.java @@ -24,6 +24,10 @@ public class Player { private static PlayerThread playerThread; public static boolean play(String url) { + return play(url, true); + } + + public static boolean play(String url, boolean async) { boolean singlePlayer = Config.getInstance().getSettings().singlePlayer; try { if (singlePlayer && playerThread != null && playerThread.isRunning()) { @@ -31,6 +35,9 @@ public class Player { } playerThread = new PlayerThread(url); + if(!async) { + playerThread.join(); + } return true; } catch (Exception e1) { LOG.error("Couldn't start player", e1); @@ -54,6 +61,10 @@ public class Player { } public static boolean play(Model model) { + return play(model, true); + } + + public static boolean play(Model model, boolean async) { try { if(model.isOnline(true)) { boolean singlePlayer = Config.getInstance().getSettings().singlePlayer; diff --git a/client/src/main/java/ctbrec/ui/SiteUI.java b/client/src/main/java/ctbrec/ui/SiteUI.java index 865b1b53..076ba0ae 100644 --- a/client/src/main/java/ctbrec/ui/SiteUI.java +++ b/client/src/main/java/ctbrec/ui/SiteUI.java @@ -2,6 +2,7 @@ package ctbrec.ui; import java.io.IOException; +import ctbrec.Model; import ctbrec.sites.ConfigUI; public interface SiteUI { @@ -9,4 +10,5 @@ public interface SiteUI { public TabProvider getTabProvider(); public ConfigUI getConfigUI(); public boolean login() throws IOException; + public boolean play(Model model); } diff --git a/client/src/main/java/ctbrec/ui/SiteUiFactory.java b/client/src/main/java/ctbrec/ui/SiteUiFactory.java index aa2b5edd..1a7ce137 100644 --- a/client/src/main/java/ctbrec/ui/SiteUiFactory.java +++ b/client/src/main/java/ctbrec/ui/SiteUiFactory.java @@ -13,7 +13,7 @@ import ctbrec.ui.sites.bonga.BongaCamsSiteUi; import ctbrec.ui.sites.cam4.Cam4SiteUi; import ctbrec.ui.sites.camsoda.CamsodaSiteUi; import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi; -import ctbrec.ui.sites.fc2live.Fc2LiveUi; +import ctbrec.ui.sites.fc2live.Fc2LiveSiteUi; import ctbrec.ui.sites.jasmin.LiveJasminSiteUi; import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi; import ctbrec.ui.sites.streamate.StreamateSiteUi; @@ -24,7 +24,7 @@ public class SiteUiFactory { private static Cam4SiteUi cam4SiteUi; private static CamsodaSiteUi camsodaSiteUi; private static ChaturbateSiteUi ctbSiteUi; - private static Fc2LiveUi fc2SiteUi; + private static Fc2LiveSiteUi fc2SiteUi; private static LiveJasminSiteUi jasminSiteUi; private static MyFreeCamsSiteUi mfcSiteUi; private static StreamateSiteUi streamateSiteUi; @@ -52,7 +52,7 @@ public class SiteUiFactory { return ctbSiteUi; } else if (site instanceof Fc2Live) { if (fc2SiteUi == null) { - fc2SiteUi = new Fc2LiveUi((Fc2Live) site); + fc2SiteUi = new Fc2LiveSiteUi((Fc2Live) site); } return fc2SiteUi; } else if (site instanceof MyFreeCams) { diff --git a/client/src/main/java/ctbrec/ui/action/PlayAction.java b/client/src/main/java/ctbrec/ui/action/PlayAction.java index 06f9cc6b..a667a04e 100644 --- a/client/src/main/java/ctbrec/ui/action/PlayAction.java +++ b/client/src/main/java/ctbrec/ui/action/PlayAction.java @@ -2,7 +2,8 @@ package ctbrec.ui.action; import ctbrec.Config; import ctbrec.Model; -import ctbrec.ui.Player; +import ctbrec.ui.SiteUI; +import ctbrec.ui.SiteUiFactory; import ctbrec.ui.controls.Toast; import javafx.application.Platform; import javafx.scene.Cursor; @@ -21,7 +22,8 @@ public class PlayAction { public void execute() { source.setCursor(Cursor.WAIT); new Thread(() -> { - boolean started = Player.play(selectedModel); + SiteUI siteUI = SiteUiFactory.getUi(selectedModel.getSite()); + boolean started = siteUI.play(selectedModel); Platform.runLater(() -> { if (started && Config.getInstance().getSettings().showPlayerStarting) { Toast.makeText(source.getScene(), "Starting Player", 2000, 500, 500); diff --git a/client/src/main/java/ctbrec/ui/sites/AbstractSiteUi.java b/client/src/main/java/ctbrec/ui/sites/AbstractSiteUi.java new file mode 100644 index 00000000..fb5cbfea --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/AbstractSiteUi.java @@ -0,0 +1,12 @@ +package ctbrec.ui.sites; + +import ctbrec.Model; +import ctbrec.ui.Player; +import ctbrec.ui.SiteUI; + +public abstract class AbstractSiteUi implements SiteUI { + @Override + public boolean play(Model model) { + return Player.play(model); + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java index d5453670..928c2df7 100644 --- a/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/bonga/BongaCamsSiteUi.java @@ -10,11 +10,11 @@ import org.slf4j.LoggerFactory; import ctbrec.sites.ConfigUI; import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.bonga.BongaCamsHttpClient; -import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; import ctbrec.ui.controls.Dialogs; +import ctbrec.ui.sites.AbstractSiteUi; -public class BongaCamsSiteUi implements SiteUI { +public class BongaCamsSiteUi extends AbstractSiteUi { private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsSiteUi.class); private BongaCamsTabProvider tabProvider; diff --git a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java index 3d56eec3..202937f3 100644 --- a/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/cam4/Cam4SiteUi.java @@ -10,12 +10,12 @@ import org.slf4j.LoggerFactory; import ctbrec.sites.ConfigUI; import ctbrec.sites.cam4.Cam4; import ctbrec.sites.cam4.Cam4HttpClient; -import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; import ctbrec.ui.controls.Dialogs; +import ctbrec.ui.sites.AbstractSiteUi; import javafx.application.Platform; -public class Cam4SiteUi implements SiteUI { +public class Cam4SiteUi extends AbstractSiteUi { private static final transient Logger LOG = LoggerFactory.getLogger(Cam4SiteUi.class); private Cam4TabProvider tabProvider; diff --git a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaSiteUi.java b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaSiteUi.java index 2b2a2eb7..5bb5a62c 100644 --- a/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/camsoda/CamsodaSiteUi.java @@ -7,10 +7,10 @@ import org.slf4j.LoggerFactory; import ctbrec.sites.ConfigUI; import ctbrec.sites.camsoda.Camsoda; -import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; +import ctbrec.ui.sites.AbstractSiteUi; -public class CamsodaSiteUi implements SiteUI { +public class CamsodaSiteUi extends AbstractSiteUi { private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaSiteUi.class); diff --git a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateSiteUi.java b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateSiteUi.java index 575ca10f..3d01af09 100644 --- a/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/chaturbate/ChaturbateSiteUi.java @@ -4,10 +4,10 @@ import java.io.IOException; import ctbrec.sites.ConfigUI; import ctbrec.sites.chaturbate.Chaturbate; -import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; +import ctbrec.ui.sites.AbstractSiteUi; -public class ChaturbateSiteUi implements SiteUI { +public class ChaturbateSiteUi extends AbstractSiteUi { private ChaturbateTabProvider tabProvider; private ChaturbateConfigUi configUi; diff --git a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveUi.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java similarity index 58% rename from client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveUi.java rename to client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java index bc7a61f2..a2976969 100644 --- a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveUi.java +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java @@ -2,17 +2,19 @@ package ctbrec.ui.sites.fc2live; import java.io.IOException; +import ctbrec.Model; import ctbrec.sites.ConfigUI; import ctbrec.sites.fc2live.Fc2Live; -import ctbrec.ui.SiteUI; +import ctbrec.ui.Player; import ctbrec.ui.TabProvider; +import ctbrec.ui.sites.AbstractSiteUi; -public class Fc2LiveUi implements SiteUI { +public class Fc2LiveSiteUi extends AbstractSiteUi { private Fc2Live fc2live; private Fc2TabProvider tabProvider; - public Fc2LiveUi(Fc2Live fc2live) { + public Fc2LiveSiteUi(Fc2Live fc2live) { this.fc2live = fc2live; this.tabProvider = new Fc2TabProvider(fc2live); } @@ -32,4 +34,15 @@ public class Fc2LiveUi implements SiteUI { return false; } + @Override + public boolean play(Model model) { + new Thread(() -> { + // create websocket + + Player.play(model, false); + + // close websocket + }).start(); + return true; + } } diff --git a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java index e017e4aa..1c6a96b1 100644 --- a/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/jasmin/LiveJasminSiteUi.java @@ -11,11 +11,11 @@ import org.slf4j.LoggerFactory; import ctbrec.sites.ConfigUI; import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.jasmin.LiveJasminHttpClient; -import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; import ctbrec.ui.controls.Dialogs; +import ctbrec.ui.sites.AbstractSiteUi; -public class LiveJasminSiteUi implements SiteUI { +public class LiveJasminSiteUi extends AbstractSiteUi { private static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminSiteUi.class); private LiveJasmin liveJasmin; diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsSiteUi.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsSiteUi.java index 59bb5829..fcf95111 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsSiteUi.java @@ -4,10 +4,10 @@ import java.io.IOException; import ctbrec.sites.ConfigUI; import ctbrec.sites.mfc.MyFreeCams; -import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; +import ctbrec.ui.sites.AbstractSiteUi; -public class MyFreeCamsSiteUi implements SiteUI { +public class MyFreeCamsSiteUi extends AbstractSiteUi { private MyFreeCamsTabProvider tabProvider; private MyFreeCamsConfigUI configUi; diff --git a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java index c7348a1f..465246c4 100644 --- a/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/streamate/StreamateSiteUi.java @@ -4,10 +4,10 @@ import java.io.IOException; import ctbrec.sites.ConfigUI; import ctbrec.sites.streamate.Streamate; -import ctbrec.ui.SiteUI; import ctbrec.ui.TabProvider; +import ctbrec.ui.sites.AbstractSiteUi; -public class StreamateSiteUi implements SiteUI { +public class StreamateSiteUi extends AbstractSiteUi { private StreamateTabProvider tabProvider; private StreamateConfigUI configUi; From 7b2f30474a993fed398356fd38d2c6103b1d00d3 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 21 Jan 2019 15:34:18 +0100 Subject: [PATCH 04/26] Make sure the reader thread is ready before sending any commands --- client/src/main/java/ctbrec/ui/ExternalBrowser.java | 9 ++++++++- client/src/main/resources/logback.xml | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/ExternalBrowser.java b/client/src/main/java/ctbrec/ui/ExternalBrowser.java index 79d84997..cd8f449a 100644 --- a/client/src/main/java/ctbrec/ui/ExternalBrowser.java +++ b/client/src/main/java/ctbrec/ui/ExternalBrowser.java @@ -31,6 +31,7 @@ public class ExternalBrowser implements AutoCloseable { private Socket socket; private Thread reader; private volatile boolean stopped = true; + private Object ready = new Object(); public static ExternalBrowser getInstance() { return INSTANCE; @@ -51,6 +52,9 @@ public class ExternalBrowser implements AutoCloseable { LOG.debug("Browser started"); connectToRemoteControlSocket(); + synchronized (ready) { + ready.wait(); + } if(LOG.isTraceEnabled()) { LOG.debug("Connected to remote control server. Sending config {}", jsonConfig); } else { @@ -131,9 +135,12 @@ public class ExternalBrowser implements AutoCloseable { try { BufferedReader br = new BufferedReader(new InputStreamReader(in)); String line; + synchronized (ready) { + ready.notify(); + } while( !Thread.interrupted() && (line = br.readLine()) != null ) { + LOG.debug(line); if(!line.startsWith("{")) { - System.err.println(line); } else { if(messageListener != null) { messageListener.accept(line); diff --git a/client/src/main/resources/logback.xml b/client/src/main/resources/logback.xml index a2555eb9..b6629bea 100644 --- a/client/src/main/resources/logback.xml +++ b/client/src/main/resources/logback.xml @@ -27,6 +27,7 @@ + From fc6aeff94a3fd87a9091ced90d2e132f7e94ee78 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 21 Jan 2019 17:58:59 +0100 Subject: [PATCH 05/26] Implemt special player handling for fc2live --- client/src/main/java/ctbrec/ui/Player.java | 2 +- .../ui/sites/fc2live/Fc2LiveSiteUi.java | 30 ++++- .../java/ctbrec/sites/fc2live/Fc2Model.java | 109 +++++++++++++++--- 3 files changed, 119 insertions(+), 22 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/Player.java b/client/src/main/java/ctbrec/ui/Player.java index 87a9e6b2..516a5ac4 100644 --- a/client/src/main/java/ctbrec/ui/Player.java +++ b/client/src/main/java/ctbrec/ui/Player.java @@ -75,7 +75,7 @@ public class Player { Collections.sort(sources); StreamSource best = sources.get(sources.size()-1); LOG.debug("Playing {}", best.getMediaPlaylistUrl()); - return Player.play(best.getMediaPlaylistUrl()); + return Player.play(best.getMediaPlaylistUrl(), async); } else { Platform.runLater(() -> { Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION); diff --git a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java index a2976969..52ae25a8 100644 --- a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java @@ -2,15 +2,19 @@ package ctbrec.ui.sites.fc2live; import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import ctbrec.Model; import ctbrec.sites.ConfigUI; import ctbrec.sites.fc2live.Fc2Live; +import ctbrec.sites.fc2live.Fc2Model; import ctbrec.ui.Player; import ctbrec.ui.TabProvider; import ctbrec.ui.sites.AbstractSiteUi; public class Fc2LiveSiteUi extends AbstractSiteUi { - + private static final transient Logger LOG = LoggerFactory.getLogger(Fc2LiveSiteUi.class); private Fc2Live fc2live; private Fc2TabProvider tabProvider; @@ -37,11 +41,25 @@ public class Fc2LiveSiteUi extends AbstractSiteUi { @Override public boolean play(Model model) { new Thread(() -> { - // create websocket - - Player.play(model, false); - - // close websocket + Fc2Model m = (Fc2Model) model; + try { + boolean opened = m.openWebsocket(); + if(opened) { + LOG.debug("Opened new websocket for player"); + } else { + LOG.debug("Using existing websocket for player"); + } + LOG.debug("Starting player"); + Player.play(model, false); + if(opened) { + LOG.debug("Closing websocket for player"); + m.closeWebsocket(); + } else { + LOG.debug("Leaving websocket for player open"); + } + } catch (InterruptedException | IOException e) { + LOG.error("Error opening websocket connection", e); + } }).start(); return true; } diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java index b5b106da..28d605c1 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java @@ -8,6 +8,7 @@ import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.function.BiConsumer; +import org.json.JSONArray; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,6 +31,9 @@ import okhttp3.FormBody; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; public class Fc2Model extends AbstractModel { private static final transient Logger LOG = LoggerFactory.getLogger(Fc2Model.class); @@ -37,6 +41,8 @@ public class Fc2Model extends AbstractModel { private int viewerCount; private boolean online; private String version; + private WebSocket ws; + private String playlistUrl; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { @@ -91,22 +97,18 @@ public class Fc2Model extends AbstractModel { @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { - loadModelInfo(); - List sources = new ArrayList<>(); - getControlToken((token, url) -> { - url = url + "?control_token=" + token; - LOG.debug("Session token: {}", token); - LOG.debug("Getting playlist token over websocket {}", url); - Fc2WebSocketClient wsClient = new Fc2WebSocketClient(url, getSite().getHttpClient()); - try { - String playlistUrl = wsClient.getPlaylistUrl(); - LOG.debug("Paylist url {}", playlistUrl); - sources.addAll(parseMasterPlaylist(playlistUrl)); - } catch (InterruptedException | IOException | ParseException | PlaylistException e) { - LOG.error("Couldn't fetch stream information", e); + try { + boolean opened = openWebsocket(); + List sources = new ArrayList<>(); + LOG.debug("Paylist url {}", playlistUrl); + sources.addAll(parseMasterPlaylist(playlistUrl)); + if(opened) { + closeWebsocket(); } - }); - return sources; + return sources; + } catch (InterruptedException e1) { + throw new ExecutionException(e1); + } } private List parseMasterPlaylist(String playlistUrl) throws IOException, ParseException, PlaylistException { @@ -221,4 +223,81 @@ public class Fc2Model extends AbstractModel { public void setViewerCount(int viewerCount) { this.viewerCount = viewerCount; } + + /** + * Opens a chat websocket connection. This connection is used to retrieve the HLS playlist url. It also has to be kept open as long as the HLS stream is + * "played" + * + * @return true, if a new websocket connection is opened. If the connection was already open, this method returns false + * @throws IOException + */ + public boolean openWebsocket() throws InterruptedException, IOException { + if(ws != null) { + return false; + } else { + Object monitor = new Object(); + loadModelInfo(); + getControlToken((token, url) -> { + url = url + "?control_token=" + token; + LOG.debug("Session token: {}", token); + LOG.debug("Getting playlist token over websocket {}", url); + + Request request = new Request.Builder() + .url(url) + .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) + .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + .header("Accept-Language", "de,en-US;q=0.7,en;q=0.3") + .build(); + ws = getSite().getHttpClient().newWebSocket(request, new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, Response response) { + response.close(); + webSocket.send("{\"name\":\"get_hls_information\",\"arguments\":{},\"id\":1}"); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + JSONObject json = new JSONObject(text); + if(json.optString("name").equals("_response_") && json.optInt("id") == 1) { + //LOG.debug(json.toString(2)); + JSONObject args = json.getJSONObject("arguments"); + JSONArray playlists = args.getJSONArray("playlists_high_latency"); + JSONObject playlist = playlists.getJSONObject(0); + playlistUrl = playlist.getString("url"); + LOG.debug("Master Playlist: {}", playlistUrl); + synchronized (monitor) { + monitor.notify(); + } + } + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + LOG.debug("ws btxt {}", bytes.toString()); + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + LOG.debug("ws closed {} - {}", code, reason); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + LOG.debug("ws failure", t); + response.close(); + } + }); + }); + synchronized (monitor) { + monitor.wait(); + } + return true; + } + } + + public boolean closeWebsocket() { + ws.close(1000, ""); + ws = null; + return true; + } } From 64c82748dca521a642767d5c34ddb28fabf6af9e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 22 Jan 2019 22:42:46 +0100 Subject: [PATCH 06/26] Count the websocket uses wth AtomicInteger --- .../src/main/java/ctbrec/ui/JavaFxModel.java | 2 +- .../ui/sites/fc2live/Fc2LiveSiteUi.java | 28 ++++---- .../sites/fc2live/Fc2MergedHlsDownload.java | 16 ++++- .../java/ctbrec/sites/fc2live/Fc2Model.java | 66 +++++++++++++++---- 4 files changed, 78 insertions(+), 34 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/JavaFxModel.java b/client/src/main/java/ctbrec/ui/JavaFxModel.java index eafe3847..d021a648 100644 --- a/client/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/client/src/main/java/ctbrec/ui/JavaFxModel.java @@ -96,7 +96,7 @@ public class JavaFxModel implements Model { return pausedProperty; } - Model getDelegate() { + public Model getDelegate() { return delegate; } diff --git a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java index 52ae25a8..500bef6f 100644 --- a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java @@ -9,17 +9,18 @@ import ctbrec.Model; import ctbrec.sites.ConfigUI; import ctbrec.sites.fc2live.Fc2Live; import ctbrec.sites.fc2live.Fc2Model; +import ctbrec.ui.JavaFxModel; import ctbrec.ui.Player; import ctbrec.ui.TabProvider; import ctbrec.ui.sites.AbstractSiteUi; public class Fc2LiveSiteUi extends AbstractSiteUi { private static final transient Logger LOG = LoggerFactory.getLogger(Fc2LiveSiteUi.class); - private Fc2Live fc2live; + //private Fc2Live fc2live; private Fc2TabProvider tabProvider; public Fc2LiveSiteUi(Fc2Live fc2live) { - this.fc2live = fc2live; + //this.fc2live = fc2live; this.tabProvider = new Fc2TabProvider(fc2live); } @@ -41,22 +42,17 @@ public class Fc2LiveSiteUi extends AbstractSiteUi { @Override public boolean play(Model model) { new Thread(() -> { - Fc2Model m = (Fc2Model) model; + Fc2Model m; + if(model instanceof JavaFxModel) { + m = (Fc2Model) ((JavaFxModel)model).getDelegate(); + } else { + m = (Fc2Model) model; + } try { - boolean opened = m.openWebsocket(); - if(opened) { - LOG.debug("Opened new websocket for player"); - } else { - LOG.debug("Using existing websocket for player"); - } - LOG.debug("Starting player"); + m.openWebsocket(); + LOG.debug("Starting player for {}", model); Player.play(model, false); - if(opened) { - LOG.debug("Closing websocket for player"); - m.closeWebsocket(); - } else { - LOG.debug("Leaving websocket for player open"); - } + m.closeWebsocket(); } catch (InterruptedException | IOException e) { LOG.error("Error opening websocket connection", e); } diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2MergedHlsDownload.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2MergedHlsDownload.java index d49439f0..9b6d1235 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2MergedHlsDownload.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2MergedHlsDownload.java @@ -2,6 +2,9 @@ package ctbrec.sites.fc2live; import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import ctbrec.Config; import ctbrec.Model; import ctbrec.io.HttpClient; @@ -9,16 +12,23 @@ import ctbrec.recorder.download.MergedHlsDownload; public class Fc2MergedHlsDownload extends MergedHlsDownload { - private Fc2WebSocketClient ws; + private static final transient Logger LOG = LoggerFactory.getLogger(Fc2MergedHlsDownload.class); public Fc2MergedHlsDownload(HttpClient client) { super(client); - } @Override public void start(Model model, Config config) throws IOException { - super.start(model, config); + Fc2Model fc2Model = (Fc2Model) model; + try { + fc2Model.openWebsocket(); + super.start(model, config); + } catch (InterruptedException e) { + LOG.error("Couldn't start download for {}", model, e); + } finally { + fc2Model.closeWebsocket(); + } } @Override diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java index 28d605c1..57616338 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java @@ -6,6 +6,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; import org.json.JSONArray; @@ -22,10 +24,13 @@ 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 com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.io.HttpException; +import ctbrec.recorder.download.Download; import ctbrec.recorder.download.StreamSource; import okhttp3.FormBody; import okhttp3.Request; @@ -43,6 +48,7 @@ public class Fc2Model extends AbstractModel { private String version; private WebSocket ws; private String playlistUrl; + private AtomicInteger websocketUsage = new AtomicInteger(0); @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { @@ -98,16 +104,15 @@ public class Fc2Model extends AbstractModel { @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { try { - boolean opened = openWebsocket(); + openWebsocket(); List sources = new ArrayList<>(); LOG.debug("Paylist url {}", playlistUrl); sources.addAll(parseMasterPlaylist(playlistUrl)); - if(opened) { - closeWebsocket(); - } return sources; } catch (InterruptedException e1) { throw new ExecutionException(e1); + } finally { + closeWebsocket(); } } @@ -226,14 +231,18 @@ public class Fc2Model extends AbstractModel { /** * Opens a chat websocket connection. This connection is used to retrieve the HLS playlist url. It also has to be kept open as long as the HLS stream is - * "played" + * "played". Fc2Model keeps track of the number of objects, which tried to open or close the websocket. As long as at least one object is using the + * websocket, it is kept open. If the last object, which is using it, calls closeWebsocket, the websocket is closed. * - * @return true, if a new websocket connection is opened. If the connection was already open, this method returns false * @throws IOException */ - public boolean openWebsocket() throws InterruptedException, IOException { + public void openWebsocket() throws InterruptedException, IOException { + // TODO send heartbeat (maybe every minute) {"name":"heartbeat","arguments":{},"id":2} + + int usage = websocketUsage.incrementAndGet(); + LOG.debug("{} objects using the websocket for {}", usage, this); if(ws != null) { - return false; + return; } else { Object monitor = new Object(); loadModelInfo(); @@ -268,6 +277,10 @@ public class Fc2Model extends AbstractModel { synchronized (monitor) { monitor.notify(); } + } else if(json.optString("name").equals("user_count") || json.optString("name").equals("comment")) { + // ignore + } else { + LOG.debug("WS <-- {}", text); } } @@ -289,15 +302,40 @@ public class Fc2Model extends AbstractModel { }); }); synchronized (monitor) { - monitor.wait(); + // wait at max 10 seconds, otherwise we can assume, that the stream is not available + LOG.debug("No playlist response for 10 seconds"); + monitor.wait(TimeUnit.SECONDS.toMillis(10)); } - return true; } } - public boolean closeWebsocket() { - ws.close(1000, ""); - ws = null; - return true; + public void closeWebsocket() { + int websocketUsers = websocketUsage.decrementAndGet(); + LOG.debug("{} objects using the websocket for {}", websocketUsers, this); + if(websocketUsers == 0) { + LOG.debug("Closing the websocket for {}", this); + ws.close(1000, ""); + ws = null; + } + } + + @Override + public Download createDownload() { + if(Config.isServerMode()) { + return super.createDownload(); // TODO implement fc2 download for server + } else { + return new Fc2MergedHlsDownload(getSite().getHttpClient()); + } + } + + @Override + public void readSiteSpecificData(JsonReader reader) throws IOException { + reader.nextName(); + id = reader.nextString(); + } + + @Override + public void writeSiteSpecificData(JsonWriter writer) throws IOException { + writer.name("id").value(id); } } From 19afa9ce797d2f3a690f8da7157ef45a8867a14e Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 23 Jan 2019 14:12:01 +0100 Subject: [PATCH 07/26] Remove Fc2WebSocketClient This is now handled in Fc2Model --- .../sites/fc2live/Fc2WebSocketClient.java | 85 ------------------- 1 file changed, 85 deletions(-) delete mode 100644 common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java deleted file mode 100644 index 7f332e9b..00000000 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2WebSocketClient.java +++ /dev/null @@ -1,85 +0,0 @@ -package ctbrec.sites.fc2live; - -import org.json.JSONArray; -import org.json.JSONObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import ctbrec.Config; -import ctbrec.io.HttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; -import okio.ByteString; - -public class Fc2WebSocketClient { - - private static final transient Logger LOG = LoggerFactory.getLogger(Fc2WebSocketClient.class); - private String url; - private HttpClient client; - - public Fc2WebSocketClient(String url, HttpClient client) { - this.url = url; - this.client = client; - } - - String playlistUrl = ""; - public String getPlaylistUrl() throws InterruptedException { - LOG.debug("Connecting to {}", url); - Object monitor = new Object(); - - Request request = new Request.Builder() - .url(url) - .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) - .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") - .header("Accept-Language", "de,en-US;q=0.7,en;q=0.3") - .build(); - client.newWebSocket(request, new WebSocketListener() { - @Override - public void onOpen(WebSocket webSocket, Response response) { - response.close(); - webSocket.send("{\"name\":\"get_hls_information\",\"arguments\":{},\"id\":1}"); - } - - @Override - public void onMessage(WebSocket webSocket, String text) { - JSONObject json = new JSONObject(text); - if(json.optString("name").equals("_response_") && json.optInt("id") == 1) { - LOG.debug(json.toString(2)); - JSONObject args = json.getJSONObject("arguments"); - JSONArray playlists = args.getJSONArray("playlists_high_latency"); - JSONObject playlist = playlists.getJSONObject(0); - playlistUrl = playlist.getString("url"); - synchronized (monitor) { - monitor.notify(); - } - } - } - - @Override - public void onMessage(WebSocket webSocket, ByteString bytes) { - LOG.debug("ws btxt {}", bytes.toString()); - } - - @Override - public void onClosed(WebSocket webSocket, int code, String reason) { - LOG.debug("ws closed {} - {}", code, reason); - } - - @Override - public void onFailure(WebSocket webSocket, Throwable t, Response response) { - LOG.debug("ws failure", t); - response.close(); - synchronized (monitor) { - monitor.notify(); - } - } - }); - synchronized (monitor) { - monitor.wait(); - } - return playlistUrl; - } -} - From c8ffdbe616d1418bf519fd1a4f9ae1c9abf79ffc Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 23 Jan 2019 14:14:14 +0100 Subject: [PATCH 08/26] Send heartbeat every now and again The heartbeat has to sent every now and again to keep the stream going. Otherwise you will get a 403 after a few minutes when trying to access the playlist. --- .../java/ctbrec/sites/fc2live/Fc2Model.java | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java index 57616338..ef924c89 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java @@ -49,6 +49,8 @@ public class Fc2Model extends AbstractModel { private WebSocket ws; private String playlistUrl; private AtomicInteger websocketUsage = new AtomicInteger(0); + private long lastHeartBeat = System.currentTimeMillis(); + private int messageId = 1; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { @@ -237,8 +239,7 @@ public class Fc2Model extends AbstractModel { * @throws IOException */ public void openWebsocket() throws InterruptedException, IOException { - // TODO send heartbeat (maybe every minute) {"name":"heartbeat","arguments":{},"id":2} - + messageId = 1; int usage = websocketUsage.incrementAndGet(); LOG.debug("{} objects using the websocket for {}", usage, this); if(ws != null) { @@ -261,27 +262,41 @@ public class Fc2Model extends AbstractModel { @Override public void onOpen(WebSocket webSocket, Response response) { response.close(); - webSocket.send("{\"name\":\"get_hls_information\",\"arguments\":{},\"id\":1}"); + webSocket.send("{\"name\":\"get_hls_information\",\"arguments\":{},\"id\":" + (messageId++) + "}"); } @Override public void onMessage(WebSocket webSocket, String text) { JSONObject json = new JSONObject(text); - if(json.optString("name").equals("_response_") && json.optInt("id") == 1) { - //LOG.debug(json.toString(2)); - JSONObject args = json.getJSONObject("arguments"); - JSONArray playlists = args.getJSONArray("playlists_high_latency"); - JSONObject playlist = playlists.getJSONObject(0); - playlistUrl = playlist.getString("url"); - LOG.debug("Master Playlist: {}", playlistUrl); - synchronized (monitor) { - monitor.notify(); + if(json.optString("name").equals("_response_")) { + if(json.has("arguments")) { + JSONObject args = json.getJSONObject("arguments"); + if(args.has("playlists_high_latency")) { + JSONArray playlists = args.getJSONArray("playlists_high_latency"); + JSONObject playlist = playlists.getJSONObject(0); + playlistUrl = playlist.getString("url"); + LOG.debug("Master Playlist: {}", playlistUrl); + synchronized (monitor) { + monitor.notify(); + } + } else { + LOG.debug(json.toString()); + } } } else if(json.optString("name").equals("user_count") || json.optString("name").equals("comment")) { // ignore } else { LOG.debug("WS <-- {}", text); } + + // send heartbeat every now and again + long now = System.currentTimeMillis(); + if( (now - lastHeartBeat) > TimeUnit.SECONDS.toMillis(30)) { + webSocket.send("{\"name\":\"heartbeat\",\"arguments\":{},\"id\":" + messageId + "}"); + lastHeartBeat = now; + LOG.debug("Sending heartbeat (messageId: {})", messageId); + messageId++; + } } @Override From 6bb6637683c0d991a68536f95a9de67f38f320af Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Wed, 23 Jan 2019 18:27:34 +0100 Subject: [PATCH 09/26] Remove guest session cookies on start for LiveJasmin --- .../main/java/ctbrec/io/CookieJarImpl.java | 23 +++++++++++++++---- .../sites/jasmin/LiveJasminHttpClient.java | 12 ++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/ctbrec/io/CookieJarImpl.java b/common/src/main/java/ctbrec/io/CookieJarImpl.java index 324fb93a..add7b643 100644 --- a/common/src/main/java/ctbrec/io/CookieJarImpl.java +++ b/common/src/main/java/ctbrec/io/CookieJarImpl.java @@ -34,7 +34,7 @@ public class CookieJarImpl implements CookieJar { String name = oldCookie.name(); for (Cookie newCookie : cookies) { if(newCookie.name().equalsIgnoreCase(name)) { - LOG.debug("Replacing cookie {} {} -> {} [{}]", oldCookie.name(), oldCookie.value(), newCookie.value(), oldCookie.domain()); + LOG.trace("Replacing cookie {} {} -> {} [{}]", oldCookie.name(), oldCookie.value(), newCookie.value(), oldCookie.domain()); iterator.remove(); break; } @@ -42,11 +42,11 @@ public class CookieJarImpl implements CookieJar { } cookiesForUrl.addAll(cookies); cookieStore.put(host, cookiesForUrl); - LOG.debug("Adding cookie: {} for {}", cookiesForUrl, host); + LOG.trace("Adding cookie: {} for {}", cookiesForUrl, host); } else { cookieStore.put(host, cookies); - LOG.debug("Storing cookie: {} for {}", cookies, host); + LOG.trace("Storing cookie: {} for {}", cookies, host); } } @@ -54,9 +54,9 @@ public class CookieJarImpl implements CookieJar { public List loadForRequest(HttpUrl url) { String host = getDomain(url); List cookies = cookieStore.get(host); - LOG.debug("Cookies for {}", url); + LOG.trace("Cookies for {}", url); Optional.ofNullable(cookies).ifPresent(cookiez -> cookiez.forEach(c -> { - LOG.debug(" {} expires on:{}", c, c.expiresAt()); + LOG.trace(" {} expires on:{}", c, c.expiresAt()); })); //LOG.debug("Cookies for {}: {}", url.host(), cookies); return cookies != null ? cookies : new ArrayList(); @@ -72,6 +72,19 @@ public class CookieJarImpl implements CookieJar { throw new NoSuchElementException("No cookie named " + name + " for " + url.host() + " available"); } + public void deleteCookie(HttpUrl url, String name) { + List cookies = loadForRequest(url); + for (Iterator iterator = cookies.iterator(); iterator.hasNext();) { + Cookie cookie = iterator.next(); + if(Objects.equals(cookie.name(), name)) { + iterator.remove(); + LOG.debug("Removed cookie \"{}\" for {}", name, url.toString()); + return; + } + } + throw new NoSuchElementException("No cookie named " + name + " for " + url.host() + " available"); + } + private String getDomain(HttpUrl url) { // String host = url.host(); // if (host.startsWith("www.")) { diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java index 98c77107..dd625e90 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasminHttpClient.java @@ -20,6 +20,18 @@ public class LiveJasminHttpClient extends HttpClient { protected LiveJasminHttpClient() { super("livejasmin"); + + // delete session, if we are guests, because old guest sessions cause + // endless redirects + if(Config.getInstance().getSettings().livejasminUsername.isEmpty()) { + HttpUrl url = HttpUrl.parse("https://" + LiveJasmin.baseDomain); + try { + getCookieJar().deleteCookie(url, "session"); + } catch (NoSuchElementException e) { + LOG.debug("Session cookie not found -> let's go!"); + // fine, no session cookie means we are good to go + } + } } @Override From beeaca4beba0db4890144c524af3b677de8381db Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 24 Jan 2019 15:24:05 +0100 Subject: [PATCH 10/26] Implement login and follow/unfollow --- .../ui/sites/fc2live/Fc2FollowedTab.java | 41 ++++++ .../fc2live/Fc2FollowedUpdateService.java | 83 ++++++++++++ .../ui/sites/fc2live/Fc2TabProvider.java | 8 +- .../ui/sites/fc2live/Fc2UpdateService.java | 4 +- common/src/main/java/ctbrec/Settings.java | 2 + .../ctbrec/sites/fc2live/Fc2HttpClient.java | 126 ++++++++---------- .../java/ctbrec/sites/fc2live/Fc2Live.java | 7 +- .../java/ctbrec/sites/fc2live/Fc2Model.java | 51 +++++-- 8 files changed, 239 insertions(+), 83 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/fc2live/Fc2FollowedTab.java create mode 100644 client/src/main/java/ctbrec/ui/sites/fc2live/Fc2FollowedUpdateService.java diff --git a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2FollowedTab.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2FollowedTab.java new file mode 100644 index 00000000..86d53bc7 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2FollowedTab.java @@ -0,0 +1,41 @@ +package ctbrec.ui.sites.fc2live; + +import ctbrec.sites.fc2live.Fc2Live; +import ctbrec.ui.FollowedTab; +import ctbrec.ui.ThumbOverviewTab; +import javafx.geometry.Insets; +import javafx.scene.control.RadioButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.HBox; + +public class Fc2FollowedTab extends ThumbOverviewTab implements FollowedTab { + + public Fc2FollowedTab(Fc2Live fc2live) { + super("Followed", new Fc2FollowedUpdateService(fc2live), fc2live); + } + + @Override + protected void createGui() { + super.createGui(); + //addOnlineOfflineSelector(); + } + + @SuppressWarnings("unused") + 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) -> { + ((Fc2FollowedUpdateService)updateService).setShowOnline(online.isSelected()); + queue.clear(); + updateService.restart(); + }); + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2FollowedUpdateService.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2FollowedUpdateService.java new file mode 100644 index 00000000..5db22826 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2FollowedUpdateService.java @@ -0,0 +1,83 @@ +package ctbrec.ui.sites.fc2live; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONObject; + +import ctbrec.Model; +import ctbrec.io.HttpException; +import ctbrec.sites.fc2live.Fc2Live; +import ctbrec.sites.fc2live.Fc2Model; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class Fc2FollowedUpdateService extends PaginatedScheduledService { + + private Fc2Live fc2live; + + public Fc2FollowedUpdateService(Fc2Live fc2live) { + this.fc2live = fc2live; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + if(!fc2live.login()) { + throw new IOException("Login didn't work"); + } + + RequestBody body = new FormBody.Builder() + .add("mode", "list") + .add("page", Integer.toString(page - 1)) + .build(); + Request req = new Request.Builder() + .url(fc2live.getBaseUrl() + "/api/favoriteManager.php") + .header("Referer", fc2live.getBaseUrl()) + .header("Content-Type", "application/x-www-form-urlencoded") + .post(body) + .build(); + try(Response resp = fc2live.getHttpClient().execute(req)) { + if(resp.isSuccessful()) { + List models = new ArrayList<>(); + String content = resp.body().string(); + JSONObject json = new JSONObject(content); + if(json.optInt("status") == 1) { + JSONArray data = json.getJSONArray("data"); + for (int i = 0; i < data.length(); i++) { + JSONObject m = data.getJSONObject(i); + Fc2Model model = (Fc2Model) fc2live.createModel(m.getString("name")); + model.setId(m.getString("id")); + model.setUrl(Fc2Live.BASE_URL + '/' + model.getId()); + String previewUrl = m.optString("icon"); + if(previewUrl == null || previewUrl.trim().isEmpty()) { + previewUrl = "https://live-storage.fc2.com/thumb/" + model.getId() + "/thumb.jpg"; + } + model.setPreview(previewUrl); + model.setDescription(""); + models.add(model); + } + return models; + } else { + throw new IOException("Request was not successful: " + content); + } + } else { + throw new HttpException(resp.code(), resp.message()); + } + } + } + }; + } + + public void setShowOnline(boolean online) { + //this.online = online; + } +} diff --git a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2TabProvider.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2TabProvider.java index d6e448c9..a5a0b32f 100644 --- a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2TabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2TabProvider.java @@ -12,6 +12,7 @@ import javafx.scene.control.Tab; public class Fc2TabProvider extends TabProvider { private Fc2Live fc2live; + private Fc2FollowedTab followed; public Fc2TabProvider(Fc2Live fc2live) { this.fc2live = fc2live; @@ -21,6 +22,11 @@ public class Fc2TabProvider extends TabProvider { public List getTabs(Scene scene) { List tabs = new ArrayList<>(); tabs.add(createTab("Online", Fc2Live.BASE_URL + "/adult/contents/allchannellist.php")); + + followed = new Fc2FollowedTab(fc2live); + followed.setRecorder(fc2live.getRecorder()); + tabs.add(followed); + return tabs; } @@ -33,6 +39,6 @@ public class Fc2TabProvider extends TabProvider { @Override public Tab getFollowedTab() { - return null; + return followed; } } diff --git a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2UpdateService.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2UpdateService.java index 46c8cba7..d69b3a1d 100644 --- a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2UpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2UpdateService.java @@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.Model; +import ctbrec.io.HttpException; import ctbrec.sites.fc2live.Fc2Live; import ctbrec.sites.fc2live.Fc2Model; import ctbrec.ui.PaginatedScheduledService; @@ -76,8 +77,7 @@ public class Fc2UpdateService extends PaginatedScheduledService { .limit(modelsPerPage) .collect(Collectors.toList()); } else { - resp.close(); - throw new IOException("HTTP status " + resp.code() + " " + resp.message()); + throw new HttpException(resp.code(), resp.message()); } } } diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index 33956234..fcf52571 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -63,6 +63,8 @@ public class Settings { public String camsodaPassword = ""; public String cam4Username = ""; public String cam4Password = ""; + public String fc2liveUsername = ""; + public String fc2livePassword = ""; public String livejasminUsername = ""; public String livejasminPassword = ""; public String livejasminBaseUrl = "https://www.livejasmin.com"; diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java index 5f5ab795..8a70a70b 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java @@ -2,11 +2,17 @@ package ctbrec.sites.fc2live; import java.io.IOException; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ctbrec.Config; import ctbrec.io.HttpClient; +import ctbrec.io.HttpException; +import okhttp3.FormBody; import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; @@ -20,81 +26,63 @@ public class Fc2HttpClient extends HttpClient { @Override public boolean login() throws IOException { - // if(loggedIn) { - // return true; - // } - // - // if(checkLogin()) { - // loggedIn = true; - // LOG.debug("Logged in with cookies"); - // return true; - // } - // - // String username = Config.getInstance().getSettings().mfcUsername; - // String password = Config.getInstance().getSettings().mfcPassword; - // RequestBody body = new FormBody.Builder() - // .add("username", username) - // .add("password", password) - // .add("tz", "2") - // .add("ss", "1920x1080") - // .add("submit_login", "97") - // .build(); - // Request req = new Request.Builder() - // .url(Fc2Live.BASE_URL + "/php/login.php") - // .header("Referer", Fc2Live.BASE_URL) - // .header("Content-Type", "application/x-www-form-urlencoded") - // .post(body) - // .build(); - // Response resp = execute(req); - // if(resp.isSuccessful()) { - // String page = resp.body().string(); - // if(page.contains("Your username or password are incorrect")) { - // return false; - // } else { - // loggedIn = true; - // return true; - // } - // } else { - // resp.close(); - // LOG.error("Login failed {} {}", resp.code(), resp.message()); - // return false; - // } - return false; + if(loggedIn) { + return true; + } + + if(checkLogin()) { + loggedIn = true; + LOG.debug("Logged in with cookies"); + return true; + } + + String username = Config.getInstance().getSettings().fc2liveUsername; + String password = Config.getInstance().getSettings().fc2livePassword; + RequestBody body = new FormBody.Builder() + .add("email", username) + .add("pass", password) + .add("image.x", "0") + .add("image.y", "0") + .add("done", "") + .build(); + Request req = new Request.Builder() + .url("https://secure.id.fc2.com/index.php?mode=login&switch_language=en") + .header("Referer", "https://fc2.com/en/login.php") + .header("Content-Type", "application/x-www-form-urlencoded") + .post(body) + .build(); + try(Response resp = execute(req)) { + if(resp.isSuccessful()) { + String page = resp.body().string(); + if(page.contains("Invalid e-mail address or password")) { + return false; + } else { + loggedIn = true; + return true; + } + } else { + resp.close(); + LOG.error("Login failed {} {}", resp.code(), resp.message()); + return false; + } + } } private boolean checkLogin() throws IOException { - // Request req = new Request.Builder().url(Fc2Live.BASE_URL + "/php/account.php?request=status").build(); - // try(Response response = execute(req)) { - // if(response.isSuccessful()) { - // String content = response.body().string(); - // try { - // Elements tags = HtmlParser.getTags(content, "div.content > p > b"); - // tags.get(2).text(); - // return true; - // } catch(Exception e) { - // LOG.debug("Token tag not found. Login failed"); - // return false; - // } - // } else { - // throw new HttpException(response.code(), response.message()); - // } - // } - return false; + Request req = new Request.Builder().url(Fc2Live.BASE_URL + "/api/favoriteManager.php").build(); + try (Response response = execute(req)) { + if (response.isSuccessful()) { + String content = response.body().string(); + JSONObject json = new JSONObject(content); + return json.optInt("status") == 1; + } else { + throw new HttpException(response.code(), response.message()); + } + } } + @Override public WebSocket newWebSocket(Request req, WebSocketListener webSocketListener) { return client.newWebSocket(req, webSocketListener); } - - // public Cookie getCookie(String name) { - // CookieJar jar = client.cookieJar(); - // HttpUrl url = HttpUrl.parse(MyFreeCams.BASE_URI); - // List cookies = jar.loadForRequest(url); - // for (Cookie cookie : cookies) { - // if(Objects.equals(cookie.name(), name)) { - // return cookie; - // } - // } - // throw new NoSuchElementException("No cookie with name " + name); - // } } diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java index a795b00e..91c3e51a 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java @@ -2,6 +2,7 @@ package ctbrec.sites.fc2live; import java.io.IOException; +import ctbrec.Config; import ctbrec.Model; import ctbrec.io.HttpClient; import ctbrec.sites.AbstractSite; @@ -46,7 +47,7 @@ public class Fc2Live extends AbstractSite { @Override public boolean login() throws IOException { - return false; + return credentialsAvailable() && getHttpClient().login(); } @Override @@ -75,7 +76,7 @@ public class Fc2Live extends AbstractSite { @Override public boolean supportsFollow() { - return false; + return true; } @Override @@ -85,7 +86,7 @@ public class Fc2Live extends AbstractSite { @Override public boolean credentialsAvailable() { - return false; + return !Config.getInstance().getSettings().fc2liveUsername.isEmpty(); } } diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java index ef924c89..f4130d97 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java @@ -76,15 +76,19 @@ public class Fc2Model extends AbstractModel { .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) .header("X-Requested-With", "XMLHttpRequest") .build(); - LOG.debug("Fetching page {}", url); try(Response resp = getSite().getHttpClient().execute(req)) { if(resp.isSuccessful()) { String msg = resp.body().string(); JSONObject json = new JSONObject(msg); + // LOG.debug(json.toString(2)); JSONObject data = json.getJSONObject("data"); JSONObject channelData = data.getJSONObject("channel_data"); online = channelData.optInt("is_publish") == 1; onlineState = online ? State.ONLINE : State.OFFLINE; + if(channelData.optInt("fee") == 1) { + onlineState = State.PRIVATE; + online = false; + } version = channelData.optString("version"); } else { resp.close(); @@ -182,12 +186,15 @@ public class Fc2Model extends AbstractModel { if(resp.isSuccessful()) { String msg = resp.body().string(); JSONObject json = new JSONObject(msg); - String wssurl = json.getString("url"); - String token = json.getString("control_token"); - callback.accept(token, wssurl); + if(json.has("url")) { + String wssurl = json.getString("url"); + String token = json.getString("control_token"); + callback.accept(token, wssurl); + } else { + throw new IOException("Couldn't determine websocket url"); + } } else { - resp.close(); - throw new IOException("HTTP status " + resp.code() + " " + resp.message()); + throw new HttpException(resp.code(), resp.message()); } } } @@ -207,12 +214,40 @@ public class Fc2Model extends AbstractModel { @Override public boolean follow() throws IOException { - return false; + return followUnfollow("add"); } @Override public boolean unfollow() throws IOException { - return false; + return followUnfollow("remove"); + } + + private boolean followUnfollow(String mode) throws IOException { + if(!getSite().getHttpClient().login()) { + throw new IOException("Login didn't work"); + } + + RequestBody body = new FormBody.Builder() + .add("id", getId()) + .add("mode", mode) + .build(); + Request req = new Request.Builder() + .url(getSite().getBaseUrl() + "/api/favoriteManager.php") + .header("Referer", getUrl()) + .header("Content-Type", "application/x-www-form-urlencoded") + .post(body) + .build(); + try(Response resp = getSite().getHttpClient().execute(req)) { + if(resp.isSuccessful()) { + String content = resp.body().string(); + JSONObject json = new JSONObject(content); + return json.optInt("status") == 1; + } else { + resp.close(); + LOG.error("Login failed {} {}", resp.code(), resp.message()); + return false; + } + } } public void setId(String id) { From 86416e0402af57a0ac9e8d58a5a3863282063636 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 24 Jan 2019 15:38:35 +0100 Subject: [PATCH 11/26] Add config UI for FC2Live --- .../ui/sites/fc2live/Fc2LiveConfigUI.java | 86 +++++++++++++++++++ .../ui/sites/fc2live/Fc2LiveSiteUi.java | 10 ++- 2 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveConfigUI.java diff --git a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveConfigUI.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveConfigUI.java new file mode 100644 index 00000000..15728e9b --- /dev/null +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveConfigUI.java @@ -0,0 +1,86 @@ +package ctbrec.ui.sites.fc2live; + +import ctbrec.Config; +import ctbrec.Settings; +import ctbrec.sites.fc2live.Fc2Live; +import ctbrec.ui.DesktopIntegration; +import ctbrec.ui.settings.SettingsTab; +import ctbrec.ui.sites.AbstractConfigUI; +import javafx.geometry.Insets; +import javafx.scene.Parent; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +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; + +public class Fc2LiveConfigUI extends AbstractConfigUI { + private Fc2Live fc2live; + + public Fc2LiveConfigUI(Fc2Live fc2live) { + this.fc2live = fc2live; + } + + @Override + public Parent createConfigPanel() { + GridPane layout = SettingsTab.createGridLayout(); + Settings settings = Config.getInstance().getSettings(); + + int row = 0; + Label l = new Label("Active"); + layout.add(l, 0, row); + CheckBox enabled = new CheckBox(); + enabled.setSelected(!settings.disabledSites.contains(fc2live.getName())); + enabled.setOnAction((e) -> { + if(enabled.isSelected()) { + settings.disabledSites.remove(fc2live.getName()); + } else { + settings.disabledSites.add(fc2live.getName()); + } + save(); + }); + GridPane.setMargin(enabled, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + layout.add(enabled, 1, row++); + + layout.add(new Label("FC2Live User"), 0, row); + TextField username = new TextField(settings.fc2liveUsername); + username.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().fc2liveUsername)) { + Config.getInstance().getSettings().fc2liveUsername = username.getText(); + fc2live.getHttpClient().logout(); + save(); + } + }); + GridPane.setFillWidth(username, true); + GridPane.setHgrow(username, Priority.ALWAYS); + GridPane.setColumnSpan(username, 2); + layout.add(username, 1, row++); + + layout.add(new Label("FC2Live Password"), 0, row); + PasswordField password = new PasswordField(); + password.setText(settings.fc2livePassword); + password.textProperty().addListener((ob, o, n) -> { + if(!n.equals(Config.getInstance().getSettings().fc2livePassword)) { + Config.getInstance().getSettings().fc2livePassword = password.getText(); + fc2live.getHttpClient().logout(); + save(); + } + }); + GridPane.setFillWidth(password, true); + GridPane.setHgrow(password, Priority.ALWAYS); + GridPane.setColumnSpan(password, 2); + layout.add(password, 1, row++); + + Button createAccount = new Button("Create new Account"); + createAccount.setOnAction((e) -> DesktopIntegration.open(fc2live.getAffiliateLink())); + layout.add(createAccount, 1, row++); + 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/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java index 500bef6f..72d9a9d8 100644 --- a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java @@ -16,12 +16,14 @@ import ctbrec.ui.sites.AbstractSiteUi; public class Fc2LiveSiteUi extends AbstractSiteUi { private static final transient Logger LOG = LoggerFactory.getLogger(Fc2LiveSiteUi.class); - //private Fc2Live fc2live; + private Fc2Live fc2live; private Fc2TabProvider tabProvider; + private Fc2LiveConfigUI configUi; public Fc2LiveSiteUi(Fc2Live fc2live) { - //this.fc2live = fc2live; + this.fc2live = fc2live; this.tabProvider = new Fc2TabProvider(fc2live); + this.configUi = new Fc2LiveConfigUI(fc2live); } @Override @@ -31,12 +33,12 @@ public class Fc2LiveSiteUi extends AbstractSiteUi { @Override public ConfigUI getConfigUI() { - return null; + return configUi; } @Override public boolean login() throws IOException { - return false; + return fc2live.login(); } @Override From d346270da2ce78997056e29de46078f032f9ee50 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 24 Jan 2019 17:36:23 +0100 Subject: [PATCH 12/26] Show the number of active recordings in the window title Use the event system to show the number of active recordings in the window title. Requested in #155. --- .../java/ctbrec/ui/CamrecApplication.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 0e8cf692..057915aa 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -17,13 +17,16 @@ import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.eventbus.Subscribe; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import ctbrec.Config; +import ctbrec.Model; import ctbrec.StringUtil; import ctbrec.Version; +import ctbrec.event.Event; import ctbrec.event.EventBusHolder; import ctbrec.event.EventHandler; import ctbrec.event.EventHandlerConfiguration; @@ -69,9 +72,11 @@ public class CamrecApplication extends Application { private List sites = new ArrayList<>(); public static HttpClient httpClient; public static String title; + private Stage primaryStage; @Override public void start(Stage primaryStage) throws Exception { + this.primaryStage = primaryStage; logEnvironment(); sites.add(new BongaCams()); sites.add(new Cam4()); @@ -82,6 +87,7 @@ public class CamrecApplication extends Application { sites.add(new Streamate()); loadConfig(); registerAlertSystem(); + registerActiveRecordingsCounter(); createHttpClient(); hostServices = getHostServices(); createRecorder(); @@ -238,6 +244,24 @@ public class CamrecApplication extends Application { }).start(); } + private void registerActiveRecordingsCounter() { + EventBusHolder.BUS.register(new Object() { + @Subscribe + public void handleEvent(Event evt) { + if(evt.getType() == Event.Type.MODEL_STATUS_CHANGED || evt.getType() == Event.Type.RECORDING_STATUS_CHANGED) { + try { + List models = recorder.getOnlineModels(); + long count = models.stream().filter(m -> !recorder.isSuspended(m)).count(); + String _title = count > 0 ? "(" + count + ") " + title : title; + Platform.runLater(() -> primaryStage.setTitle(_title)); + } catch (Exception e) { + LOG.warn("Couldn't update window title", e); + } + } + } + }); + } + private void writeColorSchemeStyleSheet(Stage primaryStage) { File colorCss = new File(Config.getInstance().getConfigDir(), "color.css"); try(FileOutputStream fos = new FileOutputStream(colorCss)) { From a91819c2ca15cbdca03651763abe0a5b01f5a3ce Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 24 Jan 2019 19:00:02 +0100 Subject: [PATCH 13/26] Extend the recording name to include seconds and milliseconds This is necessary, because there are models, who stream on different sites with the same name as mentioned in #141. In that case it can happen that a recording for each site would be started within the same minute and one recording would overwrite the other. --- common/src/main/java/ctbrec/Config.java | 5 +++-- .../main/java/ctbrec/recorder/LocalRecorder.java | 13 ++++++------- .../java/ctbrec/recorder/download/HlsDownload.java | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/common/src/main/java/ctbrec/Config.java b/common/src/main/java/ctbrec/Config.java index 9170a5f1..085ab8ba 100644 --- a/common/src/main/java/ctbrec/Config.java +++ b/common/src/main/java/ctbrec/Config.java @@ -33,6 +33,7 @@ public class Config { private String filename; private List sites; private File configDir; + public static final String RECORDING_DATE_FORMAT = "yyyy-MM-dd_HH-mm-ss_SSS"; private Config(List sites) throws FileNotFoundException, IOException { this.sites = sites; @@ -134,7 +135,7 @@ public class Config { public File getFileForRecording(Model model) { File dirForRecording = getDirForRecording(model); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm"); + SimpleDateFormat sdf = new SimpleDateFormat(RECORDING_DATE_FORMAT); String startTime = sdf.format(new Date()); File targetFile = new File(dirForRecording, model.getName() + '_' + startTime + ".ts"); return targetFile; @@ -146,7 +147,7 @@ public class Config { return new File(getSettings().recordingsDir, model.getName()); case ONE_PER_RECORDING: File modelDir = new File(getSettings().recordingsDir, model.getName()); - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm"); + SimpleDateFormat sdf = new SimpleDateFormat(RECORDING_DATE_FORMAT); String startTime = sdf.format(new Date()); return new File(modelDir, startTime); case FLAT: diff --git a/common/src/main/java/ctbrec/recorder/LocalRecorder.java b/common/src/main/java/ctbrec/recorder/LocalRecorder.java index 52b49b6f..914f2335 100644 --- a/common/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/common/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -66,7 +66,6 @@ public class LocalRecorder implements Recorder { private static final transient Logger LOG = LoggerFactory.getLogger(LocalRecorder.class); private static final boolean IGNORE_CACHE = true; - private static final String DATE_FORMAT = "yyyy-MM-dd_HH-mm"; private List models = Collections.synchronizedList(new ArrayList<>()); private Map recordingProcesses = Collections.synchronizedMap(new HashMap<>()); @@ -466,17 +465,17 @@ public class LocalRecorder implements Recorder { private List listMergedRecordings() { File recordingsDir = new File(config.getSettings().recordingsDir); List possibleRecordings = new LinkedList<>(); - listRecursively(recordingsDir, possibleRecordings, (dir, name) -> name.matches(".*?_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}\\.(ts|mp4)")); - SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); + listRecursively(recordingsDir, possibleRecordings, (dir, name) -> name.matches(".*?_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}_\\d{3}\\.(ts|mp4)")); + SimpleDateFormat sdf = new SimpleDateFormat(Config.RECORDING_DATE_FORMAT); List recordings = new ArrayList<>(); for (File ts: possibleRecordings) { try { String filename = ts.getName(); int extLength = filename.length() - filename.lastIndexOf('.'); - String dateString = filename.substring(filename.length() - extLength - DATE_FORMAT.length(), filename.length() - extLength); + String dateString = filename.substring(filename.length() - extLength - Config.RECORDING_DATE_FORMAT.length(), filename.length() - extLength); Date startDate = sdf.parse(dateString); Recording recording = new Recording(); - recording.setModelName(filename.substring(0, filename.length() - extLength - 1 - DATE_FORMAT.length())); + recording.setModelName(filename.substring(0, filename.length() - extLength - 1 - Config.RECORDING_DATE_FORMAT.length())); recording.setStartDate(Instant.ofEpochMilli(startDate.getTime())); String path = ts.getAbsolutePath().replace(config.getSettings().recordingsDir, ""); if(!path.startsWith("/")) { @@ -541,11 +540,11 @@ public class LocalRecorder implements Recorder { // start going over valid directories for (File rec : recordingsDirs) { - SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); + SimpleDateFormat sdf = new SimpleDateFormat(Config.RECORDING_DATE_FORMAT); if (rec.isDirectory()) { try { // ignore directories, which are probably not created by ctbrec - if (rec.getName().length() != DATE_FORMAT.length()) { + if (rec.getName().length() != Config.RECORDING_DATE_FORMAT.length()) { continue; } // ignore empty directories diff --git a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java index 9eef5d7c..11a0d67c 100644 --- a/common/src/main/java/ctbrec/recorder/download/HlsDownload.java +++ b/common/src/main/java/ctbrec/recorder/download/HlsDownload.java @@ -56,7 +56,7 @@ public class HlsDownload extends AbstractHlsDownload { running = true; startTime = Instant.now(); super.model = model; - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm"); + SimpleDateFormat sdf = new SimpleDateFormat(Config.RECORDING_DATE_FORMAT); String startTime = sdf.format(new Date()); Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getName()); downloadDir = FileSystems.getDefault().getPath(modelDir.toString(), startTime); From 0f51be96c09f64c768135317d069475cc37ea4b5 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Thu, 24 Jan 2019 20:24:09 +0100 Subject: [PATCH 14/26] Implement FC2Live downloads for the server --- .../ctbrec/sites/fc2live/Fc2HlsDownload.java | 38 +++++++++++++++++++ .../java/ctbrec/sites/fc2live/Fc2Model.java | 6 +-- .../ctbrec/recorder/server/HttpServer.java | 2 + 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 common/src/main/java/ctbrec/sites/fc2live/Fc2HlsDownload.java diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2HlsDownload.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2HlsDownload.java new file mode 100644 index 00000000..1515593f --- /dev/null +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2HlsDownload.java @@ -0,0 +1,38 @@ +package ctbrec.sites.fc2live; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HttpClient; +import ctbrec.recorder.download.HlsDownload; + +public class Fc2HlsDownload extends HlsDownload { + + private static final transient Logger LOG = LoggerFactory.getLogger(Fc2HlsDownload.class); + + public Fc2HlsDownload(HttpClient client) { + super(client); + } + + @Override + public void start(Model model, Config config) throws IOException { + Fc2Model fc2Model = (Fc2Model) model; + try { + fc2Model.openWebsocket(); + super.start(model, config); + } catch (InterruptedException e) { + LOG.error("Couldn't start download for {}", model, e); + } finally { + fc2Model.closeWebsocket(); + } + } + + @Override + public void stop() { + super.stop(); + } +} diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java index f4130d97..f969c5c2 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java @@ -321,7 +321,7 @@ public class Fc2Model extends AbstractModel { } else if(json.optString("name").equals("user_count") || json.optString("name").equals("comment")) { // ignore } else { - LOG.debug("WS <-- {}", text); + LOG.debug("WS <-- {}: {}", getName(), text); } // send heartbeat every now and again @@ -329,7 +329,7 @@ public class Fc2Model extends AbstractModel { if( (now - lastHeartBeat) > TimeUnit.SECONDS.toMillis(30)) { webSocket.send("{\"name\":\"heartbeat\",\"arguments\":{},\"id\":" + messageId + "}"); lastHeartBeat = now; - LOG.debug("Sending heartbeat (messageId: {})", messageId); + LOG.debug("Sending heartbeat for {} (messageId: {})", getName(), messageId); messageId++; } } @@ -372,7 +372,7 @@ public class Fc2Model extends AbstractModel { @Override public Download createDownload() { if(Config.isServerMode()) { - return super.createDownload(); // TODO implement fc2 download for server + return new Fc2HlsDownload(getSite().getHttpClient()); } else { return new Fc2MergedHlsDownload(getSite().getHttpClient()); } diff --git a/server/src/main/java/ctbrec/recorder/server/HttpServer.java b/server/src/main/java/ctbrec/recorder/server/HttpServer.java index d36328c4..d8119f08 100644 --- a/server/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/server/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -32,6 +32,7 @@ import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; +import ctbrec.sites.fc2live.Fc2Live; import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.streamate.Streamate; @@ -83,6 +84,7 @@ public class HttpServer { sites.add(new Cam4()); sites.add(new Camsoda()); sites.add(new Chaturbate()); + sites.add(new Fc2Live()); sites.add(new LiveJasmin()); sites.add(new MyFreeCams()); sites.add(new Streamate()); From 70a9d65e4896c268192bba16735072ec066edd16 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 27 Jan 2019 14:05:45 +0100 Subject: [PATCH 15/26] Fix login for FC2Live FC2Live sends cookies with value "deleted" to invalidate previously set values. In the same request they send new values, too. That confused OkHttp. We now use a custom cookie jar for FC2Live, which ignores cookies with the value "deleted" --- .../fc2live/Fc2FollowedUpdateService.java | 2 +- .../src/main/java/ctbrec/io/HttpClient.java | 5 ++++ .../ctbrec/sites/fc2live/Fc2CookieJar.java | 25 ++++++++++++++++ .../ctbrec/sites/fc2live/Fc2HttpClient.java | 29 ++++++++++++++++--- 4 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 common/src/main/java/ctbrec/sites/fc2live/Fc2CookieJar.java diff --git a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2FollowedUpdateService.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2FollowedUpdateService.java index 5db22826..c97ce4d2 100644 --- a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2FollowedUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2FollowedUpdateService.java @@ -67,7 +67,7 @@ public class Fc2FollowedUpdateService extends PaginatedScheduledService { } return models; } else { - throw new IOException("Request was not successful: " + content); + throw new IOException("Request was not successful: " + json.toString()); } } else { throw new HttpException(resp.code(), resp.message()); diff --git a/common/src/main/java/ctbrec/io/HttpClient.java b/common/src/main/java/ctbrec/io/HttpClient.java index 5da3d0b9..e65e8983 100644 --- a/common/src/main/java/ctbrec/io/HttpClient.java +++ b/common/src/main/java/ctbrec/io/HttpClient.java @@ -43,9 +43,14 @@ public abstract class HttpClient { protected HttpClient(String name) { this.name = name; + cookieJar = createCookieJar(); reconfigure(); } + protected CookieJarImpl createCookieJar() { + return new CookieJarImpl(); + } + private void loadProxySettings() { ProxyType proxyType = Config.getInstance().getSettings().proxyType; switch (proxyType) { diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2CookieJar.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2CookieJar.java new file mode 100644 index 00000000..0331ce99 --- /dev/null +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2CookieJar.java @@ -0,0 +1,25 @@ +package ctbrec.sites.fc2live; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import ctbrec.io.CookieJarImpl; +import okhttp3.Cookie; +import okhttp3.HttpUrl; + +public class Fc2CookieJar extends CookieJarImpl { + + @Override + public void saveFromResponse(HttpUrl url, List cookies) { + List sanitizedCookies = new ArrayList<>(cookies); + for (Iterator iterator = sanitizedCookies.iterator(); iterator.hasNext();) { + Cookie cookie = iterator.next(); + if(cookie.value().equalsIgnoreCase("deleted")) { + // ignore and remove from list + iterator.remove(); + } + } + super.saveFromResponse(url, sanitizedCookies); + } +} diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java index 8a70a70b..62e342cd 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java @@ -7,6 +7,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Config; +import ctbrec.io.CookieJarImpl; import ctbrec.io.HttpClient; import ctbrec.io.HttpException; import okhttp3.FormBody; @@ -24,9 +25,15 @@ public class Fc2HttpClient extends HttpClient { super("fc2live"); } + @Override + protected CookieJarImpl createCookieJar() { + return new Fc2CookieJar(); + } + @Override public boolean login() throws IOException { - if(loggedIn) { + LOG.debug("Login"); + if (loggedIn) { return true; } @@ -54,14 +61,28 @@ public class Fc2HttpClient extends HttpClient { try(Response resp = execute(req)) { if(resp.isSuccessful()) { String page = resp.body().string(); + LOG.debug(page); if(page.contains("Invalid e-mail address or password")) { return false; } else { - loggedIn = true; - return true; + LOG.debug("Calling https://secure.id.fc2.com/?login=done"); + req = new Request.Builder() + .url("https://secure.id.fc2.com/?login=done") + .header("Referer", "https://secure.id.fc2.com/index.php?mode=login&switch_language=en") + .build(); + try (Response resp2 = execute(req)) { + if (resp.isSuccessful()) { + LOG.debug("Login complete"); + loggedIn = true; + return true; + } else { + LOG.debug("Login failed"); + loggedIn = false; + return false; + } + } } } else { - resp.close(); LOG.error("Login failed {} {}", resp.code(), resp.message()); return false; } From 76c5c2e6c6720a42281d750ff22dac2265a15e41 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 27 Jan 2019 14:10:45 +0100 Subject: [PATCH 16/26] Remove log message --- common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java | 1 - 1 file changed, 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java index 62e342cd..777e29e3 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java @@ -32,7 +32,6 @@ public class Fc2HttpClient extends HttpClient { @Override public boolean login() throws IOException { - LOG.debug("Login"); if (loggedIn) { return true; } From e815a863d639b2b079941a400494d01f7989c966 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 27 Jan 2019 14:58:22 +0100 Subject: [PATCH 17/26] Reduce log level for FC2Live websocket messages to TRACE --- common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java index f969c5c2..6a170419 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java @@ -321,7 +321,7 @@ public class Fc2Model extends AbstractModel { } else if(json.optString("name").equals("user_count") || json.optString("name").equals("comment")) { // ignore } else { - LOG.debug("WS <-- {}: {}", getName(), text); + LOG.trace("WS <-- {}: {}", getName(), text); } // send heartbeat every now and again From ba0c456f195338e4bbcd21af49fb73dc19772267 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 27 Jan 2019 14:58:22 +0100 Subject: [PATCH 18/26] Reduce log level for FC2Live websocket messages to TRACE --- common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java index f969c5c2..e525b841 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java @@ -315,13 +315,13 @@ public class Fc2Model extends AbstractModel { monitor.notify(); } } else { - LOG.debug(json.toString()); + LOG.trace(json.toString()); } } } else if(json.optString("name").equals("user_count") || json.optString("name").equals("comment")) { // ignore } else { - LOG.debug("WS <-- {}: {}", getName(), text); + LOG.trace("WS <-- {}: {}", getName(), text); } // send heartbeat every now and again @@ -329,7 +329,7 @@ public class Fc2Model extends AbstractModel { if( (now - lastHeartBeat) > TimeUnit.SECONDS.toMillis(30)) { webSocket.send("{\"name\":\"heartbeat\",\"arguments\":{},\"id\":" + messageId + "}"); lastHeartBeat = now; - LOG.debug("Sending heartbeat for {} (messageId: {})", getName(), messageId); + LOG.trace("Sending heartbeat for {} (messageId: {})", getName(), messageId); messageId++; } } From df38ed30274cad20b3de5b810cca2fef0f5f096f Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Sun, 27 Jan 2019 15:18:19 +0100 Subject: [PATCH 19/26] Sanitize name before creeating a model object Names can contain '/' on FC2Live, which messes with the directory structure of recordings --- common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java index 91c3e51a..c1d9e1c1 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java @@ -29,6 +29,7 @@ public class Fc2Live extends AbstractSite { @Override public Model createModel(String name) { + name = name.replace("/", "_"); Fc2Model model = new Fc2Model(); model.setSite(this); model.setName(name); From 182a9e079ec51fa4ad50cd67223b7a1d2a60950c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 28 Jan 2019 13:32:24 +0100 Subject: [PATCH 20/26] Update active recording counter, when a MODEL_ONLINE event arrives --- client/src/main/java/ctbrec/ui/CamrecApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java index 057915aa..e8cec189 100644 --- a/client/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/client/src/main/java/ctbrec/ui/CamrecApplication.java @@ -248,7 +248,7 @@ public class CamrecApplication extends Application { EventBusHolder.BUS.register(new Object() { @Subscribe public void handleEvent(Event evt) { - if(evt.getType() == Event.Type.MODEL_STATUS_CHANGED || evt.getType() == Event.Type.RECORDING_STATUS_CHANGED) { + if(evt.getType() == Event.Type.MODEL_ONLINE || evt.getType() == Event.Type.MODEL_STATUS_CHANGED || evt.getType() == Event.Type.RECORDING_STATUS_CHANGED) { try { List models = recorder.getOnlineModels(); long count = models.stream().filter(m -> !recorder.isSuspended(m)).count(); From 626d13f87a1b8ad3ef997deb32c7fe6b93e3063c Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 28 Jan 2019 13:32:42 +0100 Subject: [PATCH 21/26] Implement createModelFromUrl --- .../java/ctbrec/sites/jasmin/LiveJasmin.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java index 9bdcb55f..9f77389e 100644 --- a/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java +++ b/common/src/main/java/ctbrec/sites/jasmin/LiveJasmin.java @@ -178,4 +178,20 @@ public class LiveJasmin extends AbstractSite { private LiveJasminHttpClient getLiveJasminHttpClient() { return (LiveJasminHttpClient) httpClient; } + + @Override + public Model createModelFromUrl(String url) { + Matcher m = Pattern.compile("http.*?livejasmin\\.com.*?#!chat/(.*)").matcher(url); + if(m.find()) { + String name = m.group(1); + return createModel(name); + } + m = Pattern.compile("http.*?livejasmin\\.com.*?/chat-html5/(.*)").matcher(url); + if(m.find()) { + String name = m.group(1); + return createModel(name); + } + + return super.createModelFromUrl(url); + } } From 8e2abd2f1164c138cc19bb8bb7cec45d8e682603 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 28 Jan 2019 14:44:19 +0100 Subject: [PATCH 22/26] Implement createModelFromUrl for FC2Live --- .../java/ctbrec/sites/fc2live/Fc2Live.java | 20 +++++++++++++++++++ .../java/ctbrec/sites/fc2live/Fc2Model.java | 6 +++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java index c1d9e1c1..c0e19493 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java @@ -1,6 +1,8 @@ package ctbrec.sites.fc2live; import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import ctbrec.Config; import ctbrec.Model; @@ -36,6 +38,24 @@ public class Fc2Live extends AbstractSite { return model; } + @Override + public Model createModelFromUrl(String url) { + Matcher m = Pattern.compile("http.*?fc2.*?.com/(\\d+)/?").matcher(url); + if(m.find()) { + Fc2Model model = (Fc2Model) createModel(""); + model.setId(m.group(1)); + try { + model.loadModelInfo(); + model.setUrl(url); + } catch (IOException e) { + return null; + } + return model; + } + + return super.createModelFromUrl(url); + } + @Override public Double getTokenBalance() throws IOException { return 0d; diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java index e525b841..16b888ec 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java @@ -60,7 +60,7 @@ public class Fc2Model extends AbstractModel { return online; } - private void loadModelInfo() throws IOException { + void loadModelInfo() throws IOException { String url = Fc2Live.BASE_URL + "/api/memberApi.php"; RequestBody body = new FormBody.Builder() .add("channel", "1") @@ -90,6 +90,10 @@ public class Fc2Model extends AbstractModel { online = false; } version = channelData.optString("version"); + if (data.has("profile_data")) { + JSONObject profileData = data.getJSONObject("profile_data"); + setName(profileData.getString("name").replace('/', '_')); + } } else { resp.close(); throw new IOException("HTTP status " + resp.code() + " " + resp.message()); From 9f13a54eb6c03b542d9d38fd24a1ed6c1d8fcee1 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 28 Jan 2019 15:22:48 +0100 Subject: [PATCH 23/26] Remove misleading log message --- .../main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java | 4 +++- common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java index 72d9a9d8..5f9a174f 100644 --- a/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java +++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java @@ -12,6 +12,7 @@ import ctbrec.sites.fc2live.Fc2Model; import ctbrec.ui.JavaFxModel; import ctbrec.ui.Player; import ctbrec.ui.TabProvider; +import ctbrec.ui.controls.Dialogs; import ctbrec.ui.sites.AbstractSiteUi; public class Fc2LiveSiteUi extends AbstractSiteUi { @@ -56,7 +57,8 @@ public class Fc2LiveSiteUi extends AbstractSiteUi { Player.play(model, false); m.closeWebsocket(); } catch (InterruptedException | IOException e) { - LOG.error("Error opening websocket connection", e); + LOG.error("Error playing the stream", e); + Dialogs.showError("Player", "Error playing the stream", e); } }).start(); return true; diff --git a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java index 16b888ec..00dcf236 100644 --- a/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java +++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java @@ -357,8 +357,10 @@ public class Fc2Model extends AbstractModel { }); synchronized (monitor) { // wait at max 10 seconds, otherwise we can assume, that the stream is not available - LOG.debug("No playlist response for 10 seconds"); - monitor.wait(TimeUnit.SECONDS.toMillis(10)); + monitor.wait(TimeUnit.SECONDS.toMillis(20)); + } + if(playlistUrl == null) { + throw new IOException("No playlist response for 20 seconds"); } } } From 2fe6ec0e8fb803f9ee4074c7a4e4561ba53b6ee4 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 28 Jan 2019 17:26:53 +0100 Subject: [PATCH 24/26] Fix #156 Make sure, that the tray icon is created only once. Also move the notification methods from OS to DesktopIntegration. --- .../java/ctbrec/ui/DesktopIntegration.java | 72 +++++++++++++++++++ .../ctbrec/ui/event/ShowNotification.java | 4 +- .../ui/settings/ActionSettingsPanel.java | 4 +- common/src/main/java/ctbrec/OS.java | 66 ----------------- 4 files changed, 76 insertions(+), 70 deletions(-) diff --git a/client/src/main/java/ctbrec/ui/DesktopIntegration.java b/client/src/main/java/ctbrec/ui/DesktopIntegration.java index 5601732a..26743502 100644 --- a/client/src/main/java/ctbrec/ui/DesktopIntegration.java +++ b/client/src/main/java/ctbrec/ui/DesktopIntegration.java @@ -1,6 +1,12 @@ package ctbrec.ui; +import java.awt.AWTException; import java.awt.Desktop; +import java.awt.Image; +import java.awt.SystemTray; +import java.awt.Toolkit; +import java.awt.TrayIcon; +import java.awt.TrayIcon.MessageType; import java.io.File; import java.io.IOException; import java.net.URI; @@ -8,6 +14,8 @@ import java.net.URI; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import ctbrec.OS; +import ctbrec.io.StreamRedirectThread; import javafx.geometry.Insets; import javafx.scene.control.Alert; import javafx.scene.control.Label; @@ -18,6 +26,9 @@ public class DesktopIntegration { private static final transient Logger LOG = LoggerFactory.getLogger(DesktopIntegration.class); + private static SystemTray tray; + private static TrayIcon trayIcon; + public static void open(String uri) { try { CamrecApplication.hostServices.showDocument(uri); @@ -95,4 +106,65 @@ public class DesktopIntegration { info.getDialogPane().setExpanded(true); info.show(); } + + public static void notification(String title, String header, String msg) { + if(OS.getOsType() == OS.TYPE.LINUX) { + notifyLinux(title, header, msg); + } else if(OS.getOsType() == OS.TYPE.WINDOWS) { + notifyWindows(title, header, msg); + } else if(OS.getOsType() == OS.TYPE.MAC) { + notifyMac(title, header, msg); + } else { + // unknown system, try systemtray notification anyways + notifySystemTray(title, header, msg); + } + } + + private static void notifyLinux(String title, String header, String msg) { + try { + Process p = Runtime.getRuntime().exec(new String[] { + "notify-send", + "-u", "normal", + "-t", "5000", + "-a", title, + header, + msg.replaceAll("-", "\\\\-").replaceAll("\\s", "\\\\ "), + "--icon=dialog-information" + }); + new Thread(new StreamRedirectThread(p.getInputStream(), System.out)).start(); + new Thread(new StreamRedirectThread(p.getErrorStream(), System.err)).start(); + } catch (IOException e1) { + LOG.error("Notification failed", e1); + } + } + + private static void notifyWindows(String title, String header, String msg) { + notifySystemTray(title, header, msg); + } + + private static void notifyMac(String title, String header, String msg) { + notifySystemTray(title, header, msg); + } + + private synchronized static void notifySystemTray(String title, String header, String msg) { + if(SystemTray.isSupported()) { + if(tray == null) { + LOG.debug("Creating tray icon"); + tray = SystemTray.getSystemTray(); + Image image = Toolkit.getDefaultToolkit().createImage(DesktopIntegration.class.getResource("/icon64.png")); + trayIcon = new TrayIcon(image, title); + trayIcon.setImageAutoSize(true); + trayIcon.setToolTip(title); + try { + tray.add(trayIcon); + } catch (AWTException e) { + LOG.error("Coulnd't add tray icon", e); + } + } + LOG.debug("Display tray message"); + trayIcon.displayMessage(header, msg, MessageType.INFO); + } else { + LOG.error("SystemTray notifications not supported by this OS"); + } + } } diff --git a/client/src/main/java/ctbrec/ui/event/ShowNotification.java b/client/src/main/java/ctbrec/ui/event/ShowNotification.java index 4d91350d..58c371ae 100644 --- a/client/src/main/java/ctbrec/ui/event/ShowNotification.java +++ b/client/src/main/java/ctbrec/ui/event/ShowNotification.java @@ -1,13 +1,13 @@ package ctbrec.ui.event; import ctbrec.Model; -import ctbrec.OS; import ctbrec.event.Action; import ctbrec.event.Event; import ctbrec.event.EventHandlerConfiguration.ActionConfiguration; import ctbrec.event.ModelStateChangedEvent; import ctbrec.event.RecordingStateChangedEvent; import ctbrec.ui.CamrecApplication; +import ctbrec.ui.DesktopIntegration; public class ShowNotification extends Action { @@ -33,7 +33,7 @@ public class ShowNotification extends Action { default: msg = evt.getDescription(); } - OS.notification(CamrecApplication.title, header, msg); + DesktopIntegration.notification(CamrecApplication.title, header, msg); } @Override diff --git a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java index 182ca2d5..012e6515 100644 --- a/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java +++ b/client/src/main/java/ctbrec/ui/settings/ActionSettingsPanel.java @@ -14,7 +14,6 @@ import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.Model; -import ctbrec.OS; import ctbrec.Recording; import ctbrec.StringUtil; import ctbrec.event.Event; @@ -29,6 +28,7 @@ import ctbrec.event.ModelStatePredicate; import ctbrec.event.RecordingStatePredicate; import ctbrec.recorder.Recorder; import ctbrec.ui.CamrecApplication; +import ctbrec.ui.DesktopIntegration; import ctbrec.ui.controls.FileSelectionBox; import ctbrec.ui.controls.ProgramSelectionBox; import ctbrec.ui.controls.Wizard; @@ -266,7 +266,7 @@ public class ActionSettingsPanel extends TitledPane { testNotification.setOnAction(evt -> { DateTimeFormatter format = DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM); ZonedDateTime time = ZonedDateTime.now(); - OS.notification(CamrecApplication.title, "Test Notification", "Oi, what's up! " + format.format(time)); + DesktopIntegration.notification(CamrecApplication.title, "Test Notification", "Oi, what's up! " + format.format(time)); }); testNotification.disableProperty().bind(showNotification.selectedProperty().not()); diff --git a/common/src/main/java/ctbrec/OS.java b/common/src/main/java/ctbrec/OS.java index 268e8cbf..4214d4d0 100644 --- a/common/src/main/java/ctbrec/OS.java +++ b/common/src/main/java/ctbrec/OS.java @@ -1,13 +1,6 @@ package ctbrec; -import java.awt.AWTException; -import java.awt.Image; -import java.awt.SystemTray; -import java.awt.Toolkit; -import java.awt.TrayIcon; -import java.awt.TrayIcon.MessageType; import java.io.File; -import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; @@ -18,8 +11,6 @@ import java.util.Map.Entry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import ctbrec.io.StreamRedirectThread; - public class OS { private static final transient Logger LOG = LoggerFactory.getLogger(OS.class); @@ -129,61 +120,4 @@ public class OS { } return env; } - - public static void notification(String title, String header, String msg) { - if(OS.getOsType() == OS.TYPE.LINUX) { - notifyLinux(title, header, msg); - } else if(OS.getOsType() == OS.TYPE.WINDOWS) { - notifyWindows(title, header, msg); - } else if(OS.getOsType() == OS.TYPE.MAC) { - notifyMac(title, header, msg); - } else { - // unknown system, try systemtray notification anyways - notifySystemTray(title, header, msg); - } - } - - private static void notifyLinux(String title, String header, String msg) { - try { - Process p = Runtime.getRuntime().exec(new String[] { - "notify-send", - "-u", "normal", - "-t", "5000", - "-a", title, - header, - msg.replaceAll("-", "\\\\-").replaceAll("\\s", "\\\\ "), - "--icon=dialog-information" - }); - new Thread(new StreamRedirectThread(p.getInputStream(), System.out)).start(); - new Thread(new StreamRedirectThread(p.getErrorStream(), System.err)).start(); - } catch (IOException e1) { - LOG.error("Notification failed", e1); - } - } - - private static void notifyWindows(String title, String header, String msg) { - notifySystemTray(title, header, msg); - } - - private static void notifyMac(String title, String header, String msg) { - notifySystemTray(title, header, msg); - } - - private static void notifySystemTray(String title, String header, String msg) { - if(SystemTray.isSupported()) { - SystemTray tray = SystemTray.getSystemTray(); - Image image = Toolkit.getDefaultToolkit().createImage(OS.class.getResource("/icon64.png")); - TrayIcon trayIcon = new TrayIcon(image, title); - trayIcon.setImageAutoSize(true); - trayIcon.setToolTip(title); - try { - tray.add(trayIcon); - } catch (AWTException e) { - LOG.error("Coulnd't add tray icon", e); - } - trayIcon.displayMessage(header, msg, MessageType.INFO); - } else { - LOG.error("SystemTray notifications not supported by this OS"); - } - } } From 1e37da9918810a14995a9e8e88e649e3f920da77 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 29 Jan 2019 14:25:19 +0100 Subject: [PATCH 25/26] Update changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45dd69d5..ca791591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +1.18.0 +======================== +* Added FC2Live +* Fix #156 Multiple Windows 10 notification icons +* Implemented adding LiceJasmin models by URL +* Added active recording counter to the title (#155) +* Fix #141: Added seconds and milliseconds to recording timestamp + !!! Caution !!! Existing recordings won't show up on the recordings + tab unless you change the filename to match the new format + 1.17.1 ======================== * Improved LiveJasmin recordings. Login is not required anymore (thanks to M1h43ly) From 4f541c0570abb8833f73c230a32771ca2f6cbb1f Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Tue, 29 Jan 2019 14:25:54 +0100 Subject: [PATCH 26/26] Increased version to 1.18.0 --- client/pom.xml | 2 +- common/pom.xml | 2 +- master/pom.xml | 2 +- server/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index a133d018..2d2c72b7 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.17.1 + 1.18.0 ../master diff --git a/common/pom.xml b/common/pom.xml index dfc22d57..ff344a72 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.17.1 + 1.18.0 ../master diff --git a/master/pom.xml b/master/pom.xml index 409000fa..cb7adcf2 100644 --- a/master/pom.xml +++ b/master/pom.xml @@ -6,7 +6,7 @@ ctbrec master pom - 1.17.1 + 1.18.0 ../common diff --git a/server/pom.xml b/server/pom.xml index 22018611..227394d6 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -8,7 +8,7 @@ ctbrec master - 1.17.1 + 1.18.0 ../master