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());