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)
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/client/src/main/java/ctbrec/ui/CamrecApplication.java b/client/src/main/java/ctbrec/ui/CamrecApplication.java
index 0e8cf692..e87efb53 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;
@@ -37,6 +40,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;
@@ -69,19 +73,23 @@ 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());
sites.add(new Camsoda());
sites.add(new Chaturbate());
+ sites.add(new Fc2Live());
sites.add(new LiveJasmin());
sites.add(new MyFreeCams());
sites.add(new Streamate());
loadConfig();
registerAlertSystem();
+ registerActiveRecordingsCounter();
createHttpClient();
hostServices = getHostServices();
createRecorder();
@@ -238,6 +246,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_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();
+ 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)) {
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/ExternalBrowser.java b/client/src/main/java/ctbrec/ui/ExternalBrowser.java
index f2eb374a..e15ec675 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,10 +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("Browser output: {}", line);
if(!line.startsWith("{")) {
- System.err.println(line);
} else {
if(messageListener != null) {
messageListener.accept(line);
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/Player.java b/client/src/main/java/ctbrec/ui/Player.java
index bace7e78..516a5ac4 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;
@@ -64,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/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 48728605..1a7ce137 100644
--- a/client/src/main/java/ctbrec/ui/SiteUiFactory.java
+++ b/client/src/main/java/ctbrec/ui/SiteUiFactory.java
@@ -5,6 +5,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;
@@ -12,6 +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.Fc2LiveSiteUi;
import ctbrec.ui.sites.jasmin.LiveJasminSiteUi;
import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi;
import ctbrec.ui.sites.streamate.StreamateSiteUi;
@@ -22,6 +24,7 @@ public class SiteUiFactory {
private static Cam4SiteUi cam4SiteUi;
private static CamsodaSiteUi camsodaSiteUi;
private static ChaturbateSiteUi ctbSiteUi;
+ private static Fc2LiveSiteUi fc2SiteUi;
private static LiveJasminSiteUi jasminSiteUi;
private static MyFreeCamsSiteUi mfcSiteUi;
private static StreamateSiteUi streamateSiteUi;
@@ -47,6 +50,11 @@ public class SiteUiFactory {
ctbSiteUi = new ChaturbateSiteUi((Chaturbate) site);
}
return ctbSiteUi;
+ } else if (site instanceof Fc2Live) {
+ if (fc2SiteUi == null) {
+ fc2SiteUi = new Fc2LiveSiteUi((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/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/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/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/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..c97ce4d2
--- /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: " + json.toString());
+ }
+ } 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/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
new file mode 100644
index 00000000..5f9a174f
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2LiveSiteUi.java
@@ -0,0 +1,66 @@
+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.JavaFxModel;
+import ctbrec.ui.Player;
+import ctbrec.ui.TabProvider;
+import ctbrec.ui.controls.Dialogs;
+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;
+ private Fc2LiveConfigUI configUi;
+
+ public Fc2LiveSiteUi(Fc2Live fc2live) {
+ this.fc2live = fc2live;
+ this.tabProvider = new Fc2TabProvider(fc2live);
+ this.configUi = new Fc2LiveConfigUI(fc2live);
+ }
+
+ @Override
+ public TabProvider getTabProvider() {
+ return tabProvider;
+ }
+
+ @Override
+ public ConfigUI getConfigUI() {
+ return configUi;
+ }
+
+ @Override
+ public boolean login() throws IOException {
+ return fc2live.login();
+ }
+
+ @Override
+ public boolean play(Model model) {
+ new Thread(() -> {
+ Fc2Model m;
+ if(model instanceof JavaFxModel) {
+ m = (Fc2Model) ((JavaFxModel)model).getDelegate();
+ } else {
+ m = (Fc2Model) model;
+ }
+ try {
+ m.openWebsocket();
+ LOG.debug("Starting player for {}", model);
+ Player.play(model, false);
+ m.closeWebsocket();
+ } catch (InterruptedException | IOException e) {
+ LOG.error("Error playing the stream", e);
+ Dialogs.showError("Player", "Error playing the stream", e);
+ }
+ }).start();
+ return true;
+ }
+}
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..a5a0b32f
--- /dev/null
+++ b/client/src/main/java/ctbrec/ui/sites/fc2live/Fc2TabProvider.java
@@ -0,0 +1,44 @@
+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;
+ private Fc2FollowedTab followed;
+
+ 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"));
+
+ followed = new Fc2FollowedTab(fc2live);
+ followed.setRecorder(fc2live.getRecorder());
+ tabs.add(followed);
+
+ 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 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
new file mode 100644
index 00000000..d69b3a1d
--- /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.io.HttpException;
+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 {
+ throw new HttpException(resp.code(), resp.message());
+ }
+ }
+ }
+ };
+ }
+}
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;
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 @@
+
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/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/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");
- }
- }
}
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/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/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/AbstractHlsDownload.java b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java
index 604138c8..375a700f 100644
--- a/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java
+++ b/common/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java
@@ -32,6 +32,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;
@@ -53,7 +54,15 @@ public abstract class AbstractHlsDownload implements Download {
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();
@@ -73,11 +82,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;
}
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);
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/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/Fc2HttpClient.java b/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java
new file mode 100644
index 00000000..777e29e3
--- /dev/null
+++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2HttpClient.java
@@ -0,0 +1,108 @@
+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.CookieJarImpl;
+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;
+
+public class Fc2HttpClient extends HttpClient {
+
+ private static final transient Logger LOG = LoggerFactory.getLogger(Fc2HttpClient.class);
+
+ public Fc2HttpClient() {
+ super("fc2live");
+ }
+
+ @Override
+ protected CookieJarImpl createCookieJar() {
+ return new Fc2CookieJar();
+ }
+
+ @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().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();
+ LOG.debug(page);
+ if(page.contains("Invalid e-mail address or password")) {
+ return false;
+ } else {
+ 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 {
+ LOG.error("Login failed {} {}", resp.code(), resp.message());
+ return false;
+ }
+ }
+ }
+
+ private boolean checkLogin() throws IOException {
+ 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);
+ }
+}
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..c0e19493
--- /dev/null
+++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Live.java
@@ -0,0 +1,113 @@
+package ctbrec.sites.fc2live;
+
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import ctbrec.Config;
+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 + "/?afid=98987181";
+ }
+
+ @Override
+ public Model createModel(String name) {
+ name = name.replace("/", "_");
+ Fc2Model model = new Fc2Model();
+ model.setSite(this);
+ model.setName(name);
+ 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;
+ }
+
+ @Override
+ public String getBuyTokensLink() {
+ return getAffiliateLink();
+ }
+
+ @Override
+ public boolean login() throws IOException {
+ return credentialsAvailable() && getHttpClient().login();
+ }
+
+ @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 true;
+ }
+
+ @Override
+ public boolean isSiteForModel(Model m) {
+ return m instanceof Fc2Model;
+ }
+
+ @Override
+ public boolean credentialsAvailable() {
+ return !Config.getInstance().getSettings().fc2liveUsername.isEmpty();
+ }
+
+}
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..9b6d1235
--- /dev/null
+++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2MergedHlsDownload.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.MergedHlsDownload;
+
+public class Fc2MergedHlsDownload extends MergedHlsDownload {
+
+ 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 {
+ 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
new file mode 100644
index 00000000..00dcf236
--- /dev/null
+++ b/common/src/main/java/ctbrec/sites/fc2live/Fc2Model.java
@@ -0,0 +1,397 @@
+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.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
+
+import org.json.JSONArray;
+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 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;
+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);
+ private String id;
+ private int viewerCount;
+ private boolean online;
+ private String version;
+ 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 {
+ if(ignoreCache) {
+ loadModelInfo();
+ }
+ return online;
+ }
+
+ 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();
+ 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");
+ 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());
+ }
+ }
+ }
+
+ @Override
+ public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
+ if(failFast) {
+ return onlineState;
+ } else if(Objects.equals(onlineState, State.UNKNOWN)){
+ loadModelInfo();
+ }
+ return onlineState;
+ }
+
+ @Override
+ public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
+ try {
+ openWebsocket();
+ List sources = new ArrayList<>();
+ LOG.debug("Paylist url {}", playlistUrl);
+ sources.addAll(parseMasterPlaylist(playlistUrl));
+ return sources;
+ } catch (InterruptedException e1) {
+ throw new ExecutionException(e1);
+ } finally {
+ closeWebsocket();
+ }
+ }
+
+ 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);
+ 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 {
+ throw new HttpException(resp.code(), resp.message());
+ }
+ }
+ }
+
+ @Override
+ public void invalidateCacheEntries() {
+ }
+
+ @Override
+ public void receiveTip(Double tokens) throws IOException {
+ }
+
+ @Override
+ public int[] getStreamResolution(boolean failFast) throws ExecutionException {
+ return new int[2];
+ }
+
+ @Override
+ public boolean follow() throws IOException {
+ return followUnfollow("add");
+ }
+
+ @Override
+ public boolean unfollow() throws IOException {
+ 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) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public int getViewerCount() {
+ return viewerCount;
+ }
+
+ 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". 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.
+ *
+ * @throws IOException
+ */
+ public void openWebsocket() throws InterruptedException, IOException {
+ messageId = 1;
+ int usage = websocketUsage.incrementAndGet();
+ LOG.debug("{} objects using the websocket for {}", usage, this);
+ if(ws != null) {
+ return;
+ } 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\":" + (messageId++) + "}");
+ }
+
+ @Override
+ public void onMessage(WebSocket webSocket, String text) {
+ JSONObject json = new JSONObject(text);
+ 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.trace(json.toString());
+ }
+ }
+ } else if(json.optString("name").equals("user_count") || json.optString("name").equals("comment")) {
+ // ignore
+ } else {
+ LOG.trace("WS <-- {}: {}", getName(), 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.trace("Sending heartbeat for {} (messageId: {})", getName(), messageId);
+ messageId++;
+ }
+ }
+
+ @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) {
+ // wait at max 10 seconds, otherwise we can assume, that the stream is not available
+ monitor.wait(TimeUnit.SECONDS.toMillis(20));
+ }
+ if(playlistUrl == null) {
+ throw new IOException("No playlist response for 20 seconds");
+ }
+ }
+ }
+
+ 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 new Fc2HlsDownload(getSite().getHttpClient());
+ } 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);
+ }
+}
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);
+ }
}
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
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());