forked from j62/ctbrec
1
0
Fork 0

Merge branch 'dev'

This commit is contained in:
0xboobface 2019-01-29 14:26:36 +01:00
commit 181c8663aa
45 changed files with 1351 additions and 108 deletions

View File

@ -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 1.17.1
======================== ========================
* Improved LiveJasmin recordings. Login is not required anymore (thanks to M1h43ly) * Improved LiveJasmin recordings. Login is not required anymore (thanks to M1h43ly)

View File

@ -8,7 +8,7 @@
<parent> <parent>
<groupId>ctbrec</groupId> <groupId>ctbrec</groupId>
<artifactId>master</artifactId> <artifactId>master</artifactId>
<version>1.17.1</version> <version>1.18.0</version>
<relativePath>../master</relativePath> <relativePath>../master</relativePath>
</parent> </parent>

View File

@ -17,13 +17,16 @@ import java.util.concurrent.TimeUnit;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe;
import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi; import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types; import com.squareup.moshi.Types;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model;
import ctbrec.StringUtil; import ctbrec.StringUtil;
import ctbrec.Version; import ctbrec.Version;
import ctbrec.event.Event;
import ctbrec.event.EventBusHolder; import ctbrec.event.EventBusHolder;
import ctbrec.event.EventHandler; import ctbrec.event.EventHandler;
import ctbrec.event.EventHandlerConfiguration; import ctbrec.event.EventHandlerConfiguration;
@ -37,6 +40,7 @@ import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.cam4.Cam4; import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.streamate.Streamate; import ctbrec.sites.streamate.Streamate;
@ -69,19 +73,23 @@ public class CamrecApplication extends Application {
private List<Site> sites = new ArrayList<>(); private List<Site> sites = new ArrayList<>();
public static HttpClient httpClient; public static HttpClient httpClient;
public static String title; public static String title;
private Stage primaryStage;
@Override @Override
public void start(Stage primaryStage) throws Exception { public void start(Stage primaryStage) throws Exception {
this.primaryStage = primaryStage;
logEnvironment(); logEnvironment();
sites.add(new BongaCams()); sites.add(new BongaCams());
sites.add(new Cam4()); sites.add(new Cam4());
sites.add(new Camsoda()); sites.add(new Camsoda());
sites.add(new Chaturbate()); sites.add(new Chaturbate());
sites.add(new Fc2Live());
sites.add(new LiveJasmin()); sites.add(new LiveJasmin());
sites.add(new MyFreeCams()); sites.add(new MyFreeCams());
sites.add(new Streamate()); sites.add(new Streamate());
loadConfig(); loadConfig();
registerAlertSystem(); registerAlertSystem();
registerActiveRecordingsCounter();
createHttpClient(); createHttpClient();
hostServices = getHostServices(); hostServices = getHostServices();
createRecorder(); createRecorder();
@ -238,6 +246,24 @@ public class CamrecApplication extends Application {
}).start(); }).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<Model> 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) { private void writeColorSchemeStyleSheet(Stage primaryStage) {
File colorCss = new File(Config.getInstance().getConfigDir(), "color.css"); File colorCss = new File(Config.getInstance().getConfigDir(), "color.css");
try(FileOutputStream fos = new FileOutputStream(colorCss)) { try(FileOutputStream fos = new FileOutputStream(colorCss)) {

View File

@ -1,6 +1,12 @@
package ctbrec.ui; package ctbrec.ui;
import java.awt.AWTException;
import java.awt.Desktop; 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.File;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@ -8,6 +14,8 @@ import java.net.URI;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.OS;
import ctbrec.io.StreamRedirectThread;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Label; import javafx.scene.control.Label;
@ -18,6 +26,9 @@ public class DesktopIntegration {
private static final transient Logger LOG = LoggerFactory.getLogger(DesktopIntegration.class); private static final transient Logger LOG = LoggerFactory.getLogger(DesktopIntegration.class);
private static SystemTray tray;
private static TrayIcon trayIcon;
public static void open(String uri) { public static void open(String uri) {
try { try {
CamrecApplication.hostServices.showDocument(uri); CamrecApplication.hostServices.showDocument(uri);
@ -95,4 +106,65 @@ public class DesktopIntegration {
info.getDialogPane().setExpanded(true); info.getDialogPane().setExpanded(true);
info.show(); 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");
}
}
} }

View File

@ -31,6 +31,7 @@ public class ExternalBrowser implements AutoCloseable {
private Socket socket; private Socket socket;
private Thread reader; private Thread reader;
private volatile boolean stopped = true; private volatile boolean stopped = true;
private Object ready = new Object();
public static ExternalBrowser getInstance() { public static ExternalBrowser getInstance() {
return INSTANCE; return INSTANCE;
@ -51,6 +52,9 @@ public class ExternalBrowser implements AutoCloseable {
LOG.debug("Browser started"); LOG.debug("Browser started");
connectToRemoteControlSocket(); connectToRemoteControlSocket();
synchronized (ready) {
ready.wait();
}
if(LOG.isTraceEnabled()) { if(LOG.isTraceEnabled()) {
LOG.debug("Connected to remote control server. Sending config {}", jsonConfig); LOG.debug("Connected to remote control server. Sending config {}", jsonConfig);
} else { } else {
@ -131,10 +135,12 @@ public class ExternalBrowser implements AutoCloseable {
try { try {
BufferedReader br = new BufferedReader(new InputStreamReader(in)); BufferedReader br = new BufferedReader(new InputStreamReader(in));
String line; String line;
synchronized (ready) {
ready.notify();
}
while( !Thread.interrupted() && (line = br.readLine()) != null ) { while( !Thread.interrupted() && (line = br.readLine()) != null ) {
LOG.debug("Browser output: {}", line); LOG.debug("Browser output: {}", line);
if(!line.startsWith("{")) { if(!line.startsWith("{")) {
System.err.println(line);
} else { } else {
if(messageListener != null) { if(messageListener != null) {
messageListener.accept(line); messageListener.accept(line);

View File

@ -96,7 +96,7 @@ public class JavaFxModel implements Model {
return pausedProperty; return pausedProperty;
} }
Model getDelegate() { public Model getDelegate() {
return delegate; return delegate;
} }

View File

@ -24,6 +24,10 @@ public class Player {
private static PlayerThread playerThread; private static PlayerThread playerThread;
public static boolean play(String url) { public static boolean play(String url) {
return play(url, true);
}
public static boolean play(String url, boolean async) {
boolean singlePlayer = Config.getInstance().getSettings().singlePlayer; boolean singlePlayer = Config.getInstance().getSettings().singlePlayer;
try { try {
if (singlePlayer && playerThread != null && playerThread.isRunning()) { if (singlePlayer && playerThread != null && playerThread.isRunning()) {
@ -31,6 +35,9 @@ public class Player {
} }
playerThread = new PlayerThread(url); playerThread = new PlayerThread(url);
if(!async) {
playerThread.join();
}
return true; return true;
} catch (Exception e1) { } catch (Exception e1) {
LOG.error("Couldn't start player", e1); LOG.error("Couldn't start player", e1);
@ -54,6 +61,10 @@ public class Player {
} }
public static boolean play(Model model) { public static boolean play(Model model) {
return play(model, true);
}
public static boolean play(Model model, boolean async) {
try { try {
if(model.isOnline(true)) { if(model.isOnline(true)) {
boolean singlePlayer = Config.getInstance().getSettings().singlePlayer; boolean singlePlayer = Config.getInstance().getSettings().singlePlayer;
@ -64,7 +75,7 @@ public class Player {
Collections.sort(sources); Collections.sort(sources);
StreamSource best = sources.get(sources.size()-1); StreamSource best = sources.get(sources.size()-1);
LOG.debug("Playing {}", best.getMediaPlaylistUrl()); LOG.debug("Playing {}", best.getMediaPlaylistUrl());
return Player.play(best.getMediaPlaylistUrl()); return Player.play(best.getMediaPlaylistUrl(), async);
} else { } else {
Platform.runLater(() -> { Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION); Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION);

View File

@ -2,6 +2,7 @@ package ctbrec.ui;
import java.io.IOException; import java.io.IOException;
import ctbrec.Model;
import ctbrec.sites.ConfigUI; import ctbrec.sites.ConfigUI;
public interface SiteUI { public interface SiteUI {
@ -9,4 +10,5 @@ public interface SiteUI {
public TabProvider getTabProvider(); public TabProvider getTabProvider();
public ConfigUI getConfigUI(); public ConfigUI getConfigUI();
public boolean login() throws IOException; public boolean login() throws IOException;
public boolean play(Model model);
} }

View File

@ -5,6 +5,7 @@ import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.cam4.Cam4; import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.streamate.Streamate; 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.cam4.Cam4SiteUi;
import ctbrec.ui.sites.camsoda.CamsodaSiteUi; import ctbrec.ui.sites.camsoda.CamsodaSiteUi;
import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi; import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi;
import ctbrec.ui.sites.fc2live.Fc2LiveSiteUi;
import ctbrec.ui.sites.jasmin.LiveJasminSiteUi; import ctbrec.ui.sites.jasmin.LiveJasminSiteUi;
import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi; import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi;
import ctbrec.ui.sites.streamate.StreamateSiteUi; import ctbrec.ui.sites.streamate.StreamateSiteUi;
@ -22,6 +24,7 @@ public class SiteUiFactory {
private static Cam4SiteUi cam4SiteUi; private static Cam4SiteUi cam4SiteUi;
private static CamsodaSiteUi camsodaSiteUi; private static CamsodaSiteUi camsodaSiteUi;
private static ChaturbateSiteUi ctbSiteUi; private static ChaturbateSiteUi ctbSiteUi;
private static Fc2LiveSiteUi fc2SiteUi;
private static LiveJasminSiteUi jasminSiteUi; private static LiveJasminSiteUi jasminSiteUi;
private static MyFreeCamsSiteUi mfcSiteUi; private static MyFreeCamsSiteUi mfcSiteUi;
private static StreamateSiteUi streamateSiteUi; private static StreamateSiteUi streamateSiteUi;
@ -47,6 +50,11 @@ public class SiteUiFactory {
ctbSiteUi = new ChaturbateSiteUi((Chaturbate) site); ctbSiteUi = new ChaturbateSiteUi((Chaturbate) site);
} }
return ctbSiteUi; return ctbSiteUi;
} else if (site instanceof Fc2Live) {
if (fc2SiteUi == null) {
fc2SiteUi = new Fc2LiveSiteUi((Fc2Live) site);
}
return fc2SiteUi;
} else if (site instanceof MyFreeCams) { } else if (site instanceof MyFreeCams) {
if (mfcSiteUi == null) { if (mfcSiteUi == null) {
mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site); mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site);

View File

@ -2,7 +2,8 @@ package ctbrec.ui.action;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.ui.Player; import ctbrec.ui.SiteUI;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.controls.Toast; import ctbrec.ui.controls.Toast;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.scene.Cursor; import javafx.scene.Cursor;
@ -21,7 +22,8 @@ public class PlayAction {
public void execute() { public void execute() {
source.setCursor(Cursor.WAIT); source.setCursor(Cursor.WAIT);
new Thread(() -> { new Thread(() -> {
boolean started = Player.play(selectedModel); SiteUI siteUI = SiteUiFactory.getUi(selectedModel.getSite());
boolean started = siteUI.play(selectedModel);
Platform.runLater(() -> { Platform.runLater(() -> {
if (started && Config.getInstance().getSettings().showPlayerStarting) { if (started && Config.getInstance().getSettings().showPlayerStarting) {
Toast.makeText(source.getScene(), "Starting Player", 2000, 500, 500); Toast.makeText(source.getScene(), "Starting Player", 2000, 500, 500);

View File

@ -1,13 +1,13 @@
package ctbrec.ui.event; package ctbrec.ui.event;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.OS;
import ctbrec.event.Action; import ctbrec.event.Action;
import ctbrec.event.Event; import ctbrec.event.Event;
import ctbrec.event.EventHandlerConfiguration.ActionConfiguration; import ctbrec.event.EventHandlerConfiguration.ActionConfiguration;
import ctbrec.event.ModelStateChangedEvent; import ctbrec.event.ModelStateChangedEvent;
import ctbrec.event.RecordingStateChangedEvent; import ctbrec.event.RecordingStateChangedEvent;
import ctbrec.ui.CamrecApplication; import ctbrec.ui.CamrecApplication;
import ctbrec.ui.DesktopIntegration;
public class ShowNotification extends Action { public class ShowNotification extends Action {
@ -33,7 +33,7 @@ public class ShowNotification extends Action {
default: default:
msg = evt.getDescription(); msg = evt.getDescription();
} }
OS.notification(CamrecApplication.title, header, msg); DesktopIntegration.notification(CamrecApplication.title, header, msg);
} }
@Override @Override

View File

@ -14,7 +14,6 @@ import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.OS;
import ctbrec.Recording; import ctbrec.Recording;
import ctbrec.StringUtil; import ctbrec.StringUtil;
import ctbrec.event.Event; import ctbrec.event.Event;
@ -29,6 +28,7 @@ import ctbrec.event.ModelStatePredicate;
import ctbrec.event.RecordingStatePredicate; import ctbrec.event.RecordingStatePredicate;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.ui.CamrecApplication; import ctbrec.ui.CamrecApplication;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.controls.FileSelectionBox; import ctbrec.ui.controls.FileSelectionBox;
import ctbrec.ui.controls.ProgramSelectionBox; import ctbrec.ui.controls.ProgramSelectionBox;
import ctbrec.ui.controls.Wizard; import ctbrec.ui.controls.Wizard;
@ -266,7 +266,7 @@ public class ActionSettingsPanel extends TitledPane {
testNotification.setOnAction(evt -> { testNotification.setOnAction(evt -> {
DateTimeFormatter format = DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM); DateTimeFormatter format = DateTimeFormatter.ofLocalizedTime(FormatStyle.MEDIUM);
ZonedDateTime time = ZonedDateTime.now(); 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()); testNotification.disableProperty().bind(showNotification.selectedProperty().not());

View File

@ -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);
}
}

View File

@ -10,11 +10,11 @@ import org.slf4j.LoggerFactory;
import ctbrec.sites.ConfigUI; import ctbrec.sites.ConfigUI;
import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.bonga.BongaCamsHttpClient; import ctbrec.sites.bonga.BongaCamsHttpClient;
import ctbrec.ui.SiteUI;
import ctbrec.ui.TabProvider; import ctbrec.ui.TabProvider;
import ctbrec.ui.controls.Dialogs; 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 static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsSiteUi.class);
private BongaCamsTabProvider tabProvider; private BongaCamsTabProvider tabProvider;

View File

@ -10,12 +10,12 @@ import org.slf4j.LoggerFactory;
import ctbrec.sites.ConfigUI; import ctbrec.sites.ConfigUI;
import ctbrec.sites.cam4.Cam4; import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.cam4.Cam4HttpClient; import ctbrec.sites.cam4.Cam4HttpClient;
import ctbrec.ui.SiteUI;
import ctbrec.ui.TabProvider; import ctbrec.ui.TabProvider;
import ctbrec.ui.controls.Dialogs; import ctbrec.ui.controls.Dialogs;
import ctbrec.ui.sites.AbstractSiteUi;
import javafx.application.Platform; 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 static final transient Logger LOG = LoggerFactory.getLogger(Cam4SiteUi.class);
private Cam4TabProvider tabProvider; private Cam4TabProvider tabProvider;

View File

@ -7,10 +7,10 @@ import org.slf4j.LoggerFactory;
import ctbrec.sites.ConfigUI; import ctbrec.sites.ConfigUI;
import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.camsoda.Camsoda;
import ctbrec.ui.SiteUI;
import ctbrec.ui.TabProvider; 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); private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaSiteUi.class);

View File

@ -4,10 +4,10 @@ import java.io.IOException;
import ctbrec.sites.ConfigUI; import ctbrec.sites.ConfigUI;
import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.ui.SiteUI;
import ctbrec.ui.TabProvider; import ctbrec.ui.TabProvider;
import ctbrec.ui.sites.AbstractSiteUi;
public class ChaturbateSiteUi implements SiteUI { public class ChaturbateSiteUi extends AbstractSiteUi {
private ChaturbateTabProvider tabProvider; private ChaturbateTabProvider tabProvider;
private ChaturbateConfigUi configUi; private ChaturbateConfigUi configUi;

View File

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

View File

@ -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<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> 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<Model> 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<Tab> getTabs(Scene scene) {
List<Tab> 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;
}
}

View File

@ -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<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> 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<Fc2Model> 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());
}
}
}
};
}
}

View File

@ -11,11 +11,11 @@ import org.slf4j.LoggerFactory;
import ctbrec.sites.ConfigUI; import ctbrec.sites.ConfigUI;
import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.jasmin.LiveJasminHttpClient; import ctbrec.sites.jasmin.LiveJasminHttpClient;
import ctbrec.ui.SiteUI;
import ctbrec.ui.TabProvider; import ctbrec.ui.TabProvider;
import ctbrec.ui.controls.Dialogs; 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 static final transient Logger LOG = LoggerFactory.getLogger(LiveJasminSiteUi.class);
private LiveJasmin liveJasmin; private LiveJasmin liveJasmin;

View File

@ -4,10 +4,10 @@ import java.io.IOException;
import ctbrec.sites.ConfigUI; import ctbrec.sites.ConfigUI;
import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.ui.SiteUI;
import ctbrec.ui.TabProvider; import ctbrec.ui.TabProvider;
import ctbrec.ui.sites.AbstractSiteUi;
public class MyFreeCamsSiteUi implements SiteUI { public class MyFreeCamsSiteUi extends AbstractSiteUi {
private MyFreeCamsTabProvider tabProvider; private MyFreeCamsTabProvider tabProvider;
private MyFreeCamsConfigUI configUi; private MyFreeCamsConfigUI configUi;

View File

@ -4,10 +4,10 @@ import java.io.IOException;
import ctbrec.sites.ConfigUI; import ctbrec.sites.ConfigUI;
import ctbrec.sites.streamate.Streamate; import ctbrec.sites.streamate.Streamate;
import ctbrec.ui.SiteUI;
import ctbrec.ui.TabProvider; import ctbrec.ui.TabProvider;
import ctbrec.ui.sites.AbstractSiteUi;
public class StreamateSiteUi implements SiteUI { public class StreamateSiteUi extends AbstractSiteUi {
private StreamateTabProvider tabProvider; private StreamateTabProvider tabProvider;
private StreamateConfigUI configUi; private StreamateConfigUI configUi;

View File

@ -27,6 +27,7 @@
<appender-ref ref="FILE" /> <appender-ref ref="FILE" />
</root> </root>
<logger name="ctbrec.ui.ExternalBrowser" level="DEBUG"/>
<logger name="ctbrec.LoggingInterceptor" level="INFO"/> <logger name="ctbrec.LoggingInterceptor" level="INFO"/>
<logger name="ctbrec.recorder.Chaturbate" level="INFO" /> <logger name="ctbrec.recorder.Chaturbate" level="INFO" />
<logger name="ctbrec.recorder.server.HlsServlet" level="INFO"/> <logger name="ctbrec.recorder.server.HlsServlet" level="INFO"/>

View File

@ -8,7 +8,7 @@
<parent> <parent>
<groupId>ctbrec</groupId> <groupId>ctbrec</groupId>
<artifactId>master</artifactId> <artifactId>master</artifactId>
<version>1.17.1</version> <version>1.18.0</version>
<relativePath>../master</relativePath> <relativePath>../master</relativePath>
</parent> </parent>

View File

@ -33,6 +33,7 @@ public class Config {
private String filename; private String filename;
private List<Site> sites; private List<Site> sites;
private File configDir; private File configDir;
public static final String RECORDING_DATE_FORMAT = "yyyy-MM-dd_HH-mm-ss_SSS";
private Config(List<Site> sites) throws FileNotFoundException, IOException { private Config(List<Site> sites) throws FileNotFoundException, IOException {
this.sites = sites; this.sites = sites;
@ -134,7 +135,7 @@ public class Config {
public File getFileForRecording(Model model) { public File getFileForRecording(Model model) {
File dirForRecording = getDirForRecording(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()); String startTime = sdf.format(new Date());
File targetFile = new File(dirForRecording, model.getName() + '_' + startTime + ".ts"); File targetFile = new File(dirForRecording, model.getName() + '_' + startTime + ".ts");
return targetFile; return targetFile;
@ -146,7 +147,7 @@ public class Config {
return new File(getSettings().recordingsDir, model.getName()); return new File(getSettings().recordingsDir, model.getName());
case ONE_PER_RECORDING: case ONE_PER_RECORDING:
File modelDir = new File(getSettings().recordingsDir, model.getName()); 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()); String startTime = sdf.format(new Date());
return new File(modelDir, startTime); return new File(modelDir, startTime);
case FLAT: case FLAT:

View File

@ -1,13 +1,6 @@
package ctbrec; 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.File;
import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.file.Path; import java.nio.file.Path;
@ -18,8 +11,6 @@ import java.util.Map.Entry;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.io.StreamRedirectThread;
public class OS { public class OS {
private static final transient Logger LOG = LoggerFactory.getLogger(OS.class); private static final transient Logger LOG = LoggerFactory.getLogger(OS.class);
@ -129,61 +120,4 @@ public class OS {
} }
return env; 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");
}
}
} }

View File

@ -63,6 +63,8 @@ public class Settings {
public String camsodaPassword = ""; public String camsodaPassword = "";
public String cam4Username = ""; public String cam4Username = "";
public String cam4Password = ""; public String cam4Password = "";
public String fc2liveUsername = "";
public String fc2livePassword = "";
public String livejasminUsername = ""; public String livejasminUsername = "";
public String livejasminPassword = ""; public String livejasminPassword = "";
public String livejasminBaseUrl = "https://www.livejasmin.com"; public String livejasminBaseUrl = "https://www.livejasmin.com";

View File

@ -43,9 +43,14 @@ public abstract class HttpClient {
protected HttpClient(String name) { protected HttpClient(String name) {
this.name = name; this.name = name;
cookieJar = createCookieJar();
reconfigure(); reconfigure();
} }
protected CookieJarImpl createCookieJar() {
return new CookieJarImpl();
}
private void loadProxySettings() { private void loadProxySettings() {
ProxyType proxyType = Config.getInstance().getSettings().proxyType; ProxyType proxyType = Config.getInstance().getSettings().proxyType;
switch (proxyType) { switch (proxyType) {

View File

@ -66,7 +66,6 @@ public class LocalRecorder implements Recorder {
private static final transient Logger LOG = LoggerFactory.getLogger(LocalRecorder.class); private static final transient Logger LOG = LoggerFactory.getLogger(LocalRecorder.class);
private static final boolean IGNORE_CACHE = true; private static final boolean IGNORE_CACHE = true;
private static final String DATE_FORMAT = "yyyy-MM-dd_HH-mm";
private List<Model> models = Collections.synchronizedList(new ArrayList<>()); private List<Model> models = Collections.synchronizedList(new ArrayList<>());
private Map<Model, Download> recordingProcesses = Collections.synchronizedMap(new HashMap<>()); private Map<Model, Download> recordingProcesses = Collections.synchronizedMap(new HashMap<>());
@ -466,17 +465,17 @@ public class LocalRecorder implements Recorder {
private List<Recording> listMergedRecordings() { private List<Recording> listMergedRecordings() {
File recordingsDir = new File(config.getSettings().recordingsDir); File recordingsDir = new File(config.getSettings().recordingsDir);
List<File> possibleRecordings = new LinkedList<>(); List<File> possibleRecordings = new LinkedList<>();
listRecursively(recordingsDir, possibleRecordings, (dir, name) -> name.matches(".*?_\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}\\.(ts|mp4)")); 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(DATE_FORMAT); SimpleDateFormat sdf = new SimpleDateFormat(Config.RECORDING_DATE_FORMAT);
List<Recording> recordings = new ArrayList<>(); List<Recording> recordings = new ArrayList<>();
for (File ts: possibleRecordings) { for (File ts: possibleRecordings) {
try { try {
String filename = ts.getName(); String filename = ts.getName();
int extLength = filename.length() - filename.lastIndexOf('.'); 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); Date startDate = sdf.parse(dateString);
Recording recording = new Recording(); 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())); recording.setStartDate(Instant.ofEpochMilli(startDate.getTime()));
String path = ts.getAbsolutePath().replace(config.getSettings().recordingsDir, ""); String path = ts.getAbsolutePath().replace(config.getSettings().recordingsDir, "");
if(!path.startsWith("/")) { if(!path.startsWith("/")) {
@ -541,11 +540,11 @@ public class LocalRecorder implements Recorder {
// start going over valid directories // start going over valid directories
for (File rec : recordingsDirs) { for (File rec : recordingsDirs) {
SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); SimpleDateFormat sdf = new SimpleDateFormat(Config.RECORDING_DATE_FORMAT);
if (rec.isDirectory()) { if (rec.isDirectory()) {
try { try {
// ignore directories, which are probably not created by ctbrec // 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; continue;
} }
// ignore empty directories // ignore empty directories

View File

@ -32,6 +32,7 @@ import ctbrec.Config;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.sites.fc2live.Fc2Live;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
@ -53,7 +54,15 @@ public abstract class AbstractHlsDownload implements Download {
protected SegmentPlaylist getNextSegments(String segments) throws IOException, ParseException, PlaylistException { protected SegmentPlaylist getNextSegments(String segments) throws IOException, ParseException, PlaylistException {
URL segmentsUrl = new URL(segments); 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)) { try(Response response = client.execute(request)) {
if(response.isSuccessful()) { if(response.isSuccessful()) {
// String body = response.body().string(); // String body = response.body().string();
@ -73,11 +82,11 @@ public abstract class AbstractHlsDownload implements Download {
if(!uri.startsWith("http")) { if(!uri.startsWith("http")) {
String _url = segmentsUrl.toString(); String _url = segmentsUrl.toString();
_url = _url.substring(0, _url.lastIndexOf('/') + 1); _url = _url.substring(0, _url.lastIndexOf('/') + 1);
String segmentUri = _url + uri; uri = _url + uri;
}
lsp.totalDuration += trackData.getTrackInfo().duration; lsp.totalDuration += trackData.getTrackInfo().duration;
lsp.lastSegDuration = trackData.getTrackInfo().duration; lsp.lastSegDuration = trackData.getTrackInfo().duration;
lsp.segments.add(segmentUri); lsp.segments.add(uri);
}
} }
return lsp; return lsp;
} }

View File

@ -56,7 +56,7 @@ public class HlsDownload extends AbstractHlsDownload {
running = true; running = true;
startTime = Instant.now(); startTime = Instant.now();
super.model = model; 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()); String startTime = sdf.format(new Date());
Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getName()); Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getName());
downloadDir = FileSystems.getDefault().getPath(modelDir.toString(), startTime); downloadDir = FileSystems.getDefault().getPath(modelDir.toString(), startTime);

View File

@ -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<Cookie> cookies) {
List<Cookie> sanitizedCookies = new ArrayList<>(cookies);
for (Iterator<Cookie> 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);
}
}

View File

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

View File

@ -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);
}
}

View File

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

View File

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

View File

@ -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<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
try {
openWebsocket();
List<StreamSource> 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<StreamSource> parseMasterPlaylist(String playlistUrl) throws IOException, ParseException, PlaylistException {
List<StreamSource> 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<String, String> 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);
}
}

View File

@ -178,4 +178,20 @@ public class LiveJasmin extends AbstractSite {
private LiveJasminHttpClient getLiveJasminHttpClient() { private LiveJasminHttpClient getLiveJasminHttpClient() {
return (LiveJasminHttpClient) httpClient; 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);
}
} }

View File

@ -6,7 +6,7 @@
<groupId>ctbrec</groupId> <groupId>ctbrec</groupId>
<artifactId>master</artifactId> <artifactId>master</artifactId>
<packaging>pom</packaging> <packaging>pom</packaging>
<version>1.17.1</version> <version>1.18.0</version>
<modules> <modules>
<module>../common</module> <module>../common</module>

View File

@ -8,7 +8,7 @@
<parent> <parent>
<groupId>ctbrec</groupId> <groupId>ctbrec</groupId>
<artifactId>master</artifactId> <artifactId>master</artifactId>
<version>1.17.1</version> <version>1.18.0</version>
<relativePath>../master</relativePath> <relativePath>../master</relativePath>
</parent> </parent>

View File

@ -32,6 +32,7 @@ import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.cam4.Cam4; import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.jasmin.LiveJasmin; import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.streamate.Streamate; import ctbrec.sites.streamate.Streamate;
@ -83,6 +84,7 @@ public class HttpServer {
sites.add(new Cam4()); sites.add(new Cam4());
sites.add(new Camsoda()); sites.add(new Camsoda());
sites.add(new Chaturbate()); sites.add(new Chaturbate());
sites.add(new Fc2Live());
sites.add(new LiveJasmin()); sites.add(new LiveJasmin());
sites.add(new MyFreeCams()); sites.add(new MyFreeCams());
sites.add(new Streamate()); sites.add(new Streamate());