forked from j62/ctbrec
Merge branch 'dev'
This commit is contained in:
commit
181c8663aa
10
CHANGELOG.md
10
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)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<parent>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<version>1.17.1</version>
|
||||
<version>1.18.0</version>
|
||||
<relativePath>../master</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -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<Site> 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<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) {
|
||||
File colorCss = new File(Config.getInstance().getConfigDir(), "color.css");
|
||||
try(FileOutputStream fos = new FileOutputStream(colorCss)) {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -96,7 +96,7 @@ public class JavaFxModel implements Model {
|
|||
return pausedProperty;
|
||||
}
|
||||
|
||||
Model getDelegate() {
|
||||
public Model getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
|
||||
<logger name="ctbrec.ui.ExternalBrowser" level="DEBUG"/>
|
||||
<logger name="ctbrec.LoggingInterceptor" level="INFO"/>
|
||||
<logger name="ctbrec.recorder.Chaturbate" level="INFO" />
|
||||
<logger name="ctbrec.recorder.server.HlsServlet" level="INFO"/>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<parent>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<version>1.17.1</version>
|
||||
<version>1.18.0</version>
|
||||
<relativePath>../master</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ public class Config {
|
|||
private String filename;
|
||||
private List<Site> sites;
|
||||
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 {
|
||||
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:
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<Model> models = Collections.synchronizedList(new ArrayList<>());
|
||||
private Map<Model, Download> recordingProcesses = Collections.synchronizedMap(new HashMap<>());
|
||||
|
@ -466,17 +465,17 @@ public class LocalRecorder implements Recorder {
|
|||
private List<Recording> listMergedRecordings() {
|
||||
File recordingsDir = new File(config.getSettings().recordingsDir);
|
||||
List<File> 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<Recording> 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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<version>1.17.1</version>
|
||||
<version>1.18.0</version>
|
||||
|
||||
<modules>
|
||||
<module>../common</module>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<parent>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>master</artifactId>
|
||||
<version>1.17.1</version>
|
||||
<version>1.18.0</version>
|
||||
<relativePath>../master</relativePath>
|
||||
</parent>
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in New Issue