forked from j62/ctbrec
Merge branch 'dev' into camsoda
Conflicts: src/main/java/ctbrec/Settings.java src/main/java/ctbrec/recorder/server/HttpServer.java src/main/java/ctbrec/ui/CamrecApplication.java src/main/java/ctbrec/ui/ThumbCell.java
This commit is contained in:
commit
b476c452ad
|
@ -1,3 +1,9 @@
|
||||||
|
1.6.1
|
||||||
|
========================
|
||||||
|
* Fixed UI freeze, which occured for a high number of recorded models
|
||||||
|
* Added Cam4
|
||||||
|
* Updated the embedded JRE for the Windows bundles to 8u192
|
||||||
|
|
||||||
1.6.0
|
1.6.0
|
||||||
========================
|
========================
|
||||||
* Added support for multiple cam sites
|
* Added support for multiple cam sites
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
DIR=$(dirname $0)
|
||||||
|
pushd $DIR
|
||||||
|
JAVA_HOME="$DIR/jre/Contents/Home"
|
||||||
|
JAVA="$JAVA_HOME/bin/java"
|
||||||
|
$JAVA -version
|
||||||
|
$JAVA -cp ${name.final}.jar ctbrec.ui.Launcher
|
||||||
|
popd
|
5
pom.xml
5
pom.xml
|
@ -5,7 +5,7 @@
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>ctbrec</artifactId>
|
<artifactId>ctbrec</artifactId>
|
||||||
<version>1.6.0</version>
|
<version>1.6.1</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
@ -66,6 +66,7 @@
|
||||||
<descriptor>src/assembly/win64-jre.xml</descriptor>
|
<descriptor>src/assembly/win64-jre.xml</descriptor>
|
||||||
<descriptor>src/assembly/win32-jre.xml</descriptor>
|
<descriptor>src/assembly/win32-jre.xml</descriptor>
|
||||||
<descriptor>src/assembly/linux.xml</descriptor>
|
<descriptor>src/assembly/linux.xml</descriptor>
|
||||||
|
<descriptor>src/assembly/macos-jre.xml</descriptor>
|
||||||
</descriptors>
|
</descriptors>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
|
@ -77,7 +78,7 @@
|
||||||
<version>1.7.22</version>
|
<version>1.7.22</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<id>l4j-clui</id>
|
<id>l4j-win</id>
|
||||||
<phase>package</phase>
|
<phase>package</phase>
|
||||||
<goals>
|
<goals>
|
||||||
<goal>launch4j</goal>
|
<goal>launch4j</goal>
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
DIR=$(dirname $0)
|
||||||
|
pushd $DIR
|
||||||
|
JAVA_HOME="$DIR/jre/Contents/Home"
|
||||||
|
JAVA="$JAVA_HOME/bin/java"
|
||||||
|
$JAVA -version
|
||||||
|
$JAVA -cp ${name.final}.jar -Dctbrec.config=server.json ctbrec.recorder.server.HttpServer
|
||||||
|
popd
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<assembly>
|
||||||
|
<id>macos-jre</id>
|
||||||
|
<formats>
|
||||||
|
<format>zip</format>
|
||||||
|
</formats>
|
||||||
|
<includeBaseDirectory>false</includeBaseDirectory>
|
||||||
|
<files>
|
||||||
|
<file>
|
||||||
|
<source>${project.basedir}/ctbrec-macos.sh</source>
|
||||||
|
<outputDirectory>ctbrec</outputDirectory>
|
||||||
|
<filtered>true</filtered>
|
||||||
|
</file>
|
||||||
|
<file>
|
||||||
|
<source>${project.basedir}/server-macos.sh</source>
|
||||||
|
<outputDirectory>ctbrec</outputDirectory>
|
||||||
|
<filtered>true</filtered>
|
||||||
|
</file>
|
||||||
|
<file>
|
||||||
|
<source>${project.build.directory}/${name.final}.jar</source>
|
||||||
|
<outputDirectory>ctbrec</outputDirectory>
|
||||||
|
</file>
|
||||||
|
</files>
|
||||||
|
<fileSets>
|
||||||
|
<fileSet>
|
||||||
|
<directory>jre/jre1.8.0_192_macos</directory>
|
||||||
|
<includes>
|
||||||
|
<include>**/*</include>
|
||||||
|
</includes>
|
||||||
|
<outputDirectory>ctbrec/jre</outputDirectory>
|
||||||
|
<filtered>false</filtered>
|
||||||
|
</fileSet>
|
||||||
|
</fileSets>
|
||||||
|
</assembly>
|
|
@ -22,7 +22,7 @@
|
||||||
</files>
|
</files>
|
||||||
<fileSets>
|
<fileSets>
|
||||||
<fileSet>
|
<fileSet>
|
||||||
<directory>jre/jre1.8.0_181_win32</directory>
|
<directory>jre/jre1.8.0_192_win32</directory>
|
||||||
<includes>
|
<includes>
|
||||||
<include>**/*</include>
|
<include>**/*</include>
|
||||||
</includes>
|
</includes>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
</files>
|
</files>
|
||||||
<fileSets>
|
<fileSets>
|
||||||
<fileSet>
|
<fileSet>
|
||||||
<directory>jre/jre1.8.0_181_win64</directory>
|
<directory>jre/jre1.8.0_192_win64</directory>
|
||||||
<includes>
|
<includes>
|
||||||
<include>**/*</include>
|
<include>**/*</include>
|
||||||
</includes>
|
</includes>
|
||||||
|
|
|
@ -26,6 +26,8 @@ public class Settings {
|
||||||
public String mfcPassword = "";
|
public String mfcPassword = "";
|
||||||
public String camsodaUsername = "";
|
public String camsodaUsername = "";
|
||||||
public String camsodaPassword = "";
|
public String camsodaPassword = "";
|
||||||
|
public String cam4Username;
|
||||||
|
public String cam4Password;
|
||||||
public String lastDownloadDir = "";
|
public String lastDownloadDir = "";
|
||||||
|
|
||||||
public List<Model> models = new ArrayList<Model>();
|
public List<Model> models = new ArrayList<Model>();
|
||||||
|
|
|
@ -69,7 +69,7 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
||||||
}
|
}
|
||||||
return model;
|
return model;
|
||||||
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
|
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
|
||||||
throw new IOException("Couldn't instantiate mode class [" + type + "]", e);
|
throw new IOException("Couldn't instantiate model class [" + type + "]", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -166,7 +166,12 @@ public class LocalRecorder implements Recorder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Model> getModelsRecording() {
|
public List<Model> getModelsRecording() {
|
||||||
return Collections.unmodifiableList(new ArrayList<>(models));
|
lock.lock();
|
||||||
|
try {
|
||||||
|
return Collections.unmodifiableList(new ArrayList<>(models));
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -308,24 +313,19 @@ public class LocalRecorder implements Recorder {
|
||||||
public void run() {
|
public void run() {
|
||||||
running = true;
|
running = true;
|
||||||
while (running) {
|
while (running) {
|
||||||
lock.lock();
|
for (Model model : getModelsRecording()) {
|
||||||
try {
|
try {
|
||||||
for (Model model : getModelsRecording()) {
|
if (!recordingProcesses.containsKey(model)) {
|
||||||
try {
|
boolean isOnline = model.isOnline(IGNORE_CACHE);
|
||||||
if (!recordingProcesses.containsKey(model)) {
|
LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline"));
|
||||||
boolean isOnline = model.isOnline(IGNORE_CACHE);
|
if (isOnline) {
|
||||||
LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline"));
|
LOG.info("Model {}'s room back to public. Starting recording", model);
|
||||||
if (isOnline) {
|
startRecordingProcess(model);
|
||||||
LOG.info("Model {}'s room back to public. Starting recording", model);
|
|
||||||
startRecordingProcess(model);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.error("Couldn't check if model {} is online", model.getName(), e);
|
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Couldn't check if model {} is online", model.getName(), e);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import java.util.concurrent.Executors;
|
||||||
import com.iheartradio.m3u8.Encoding;
|
import com.iheartradio.m3u8.Encoding;
|
||||||
import com.iheartradio.m3u8.Format;
|
import com.iheartradio.m3u8.Format;
|
||||||
import com.iheartradio.m3u8.ParseException;
|
import com.iheartradio.m3u8.ParseException;
|
||||||
|
import com.iheartradio.m3u8.ParsingMode;
|
||||||
import com.iheartradio.m3u8.PlaylistException;
|
import com.iheartradio.m3u8.PlaylistException;
|
||||||
import com.iheartradio.m3u8.PlaylistParser;
|
import com.iheartradio.m3u8.PlaylistParser;
|
||||||
import com.iheartradio.m3u8.data.MediaPlaylist;
|
import com.iheartradio.m3u8.data.MediaPlaylist;
|
||||||
|
@ -41,7 +42,7 @@ public abstract class AbstractHlsDownload implements Download {
|
||||||
Response response = client.execute(request);
|
Response response = client.execute(request);
|
||||||
try {
|
try {
|
||||||
InputStream inputStream = response.body().byteStream();
|
InputStream inputStream = response.body().byteStream();
|
||||||
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
|
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
||||||
Playlist playlist = parser.parse();
|
Playlist playlist = parser.parse();
|
||||||
if(playlist.hasMediaPlaylist()) {
|
if(playlist.hasMediaPlaylist()) {
|
||||||
MediaPlaylist mediaPlaylist = playlist.getMediaPlaylist();
|
MediaPlaylist mediaPlaylist = playlist.getMediaPlaylist();
|
||||||
|
|
|
@ -20,6 +20,7 @@ import ctbrec.Config;
|
||||||
import ctbrec.recorder.LocalRecorder;
|
import ctbrec.recorder.LocalRecorder;
|
||||||
import ctbrec.recorder.Recorder;
|
import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
|
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.mfc.MyFreeCams;
|
import ctbrec.sites.mfc.MyFreeCams;
|
||||||
|
@ -64,6 +65,7 @@ public class HttpServer {
|
||||||
sites.add(new Chaturbate());
|
sites.add(new Chaturbate());
|
||||||
sites.add(new MyFreeCams());
|
sites.add(new MyFreeCams());
|
||||||
sites.add(new Camsoda());
|
sites.add(new Camsoda());
|
||||||
|
sites.add(new Cam4());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addShutdownHook() {
|
private void addShutdownHook() {
|
||||||
|
|
|
@ -0,0 +1,155 @@
|
||||||
|
package ctbrec.sites.cam4;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import ctbrec.recorder.Recorder;
|
||||||
|
import ctbrec.sites.AbstractSite;
|
||||||
|
import ctbrec.ui.DesktopIntergation;
|
||||||
|
import ctbrec.ui.SettingsTab;
|
||||||
|
import ctbrec.ui.TabProvider;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
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 Cam4 extends AbstractSite {
|
||||||
|
|
||||||
|
public static final String BASE_URI = "https://www.cam4.com";
|
||||||
|
|
||||||
|
public static final String AFFILIATE_LINK = BASE_URI + "/?referrerId=1514a80d87b5effb456cca02f6743aa1";
|
||||||
|
|
||||||
|
private HttpClient httpClient;
|
||||||
|
private Recorder recorder;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Cam4";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return BASE_URI;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffiliateLink() {
|
||||||
|
return AFFILIATE_LINK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRecorder(Recorder recorder) {
|
||||||
|
this.recorder = recorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TabProvider getTabProvider() {
|
||||||
|
return new Cam4TabProvider(this, recorder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Model createModel(String name) {
|
||||||
|
Cam4Model m = new Cam4Model();
|
||||||
|
m.setSite(this);
|
||||||
|
m.setName(name);
|
||||||
|
m.setUrl(getBaseUrl() + '/' + name + '/');
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getTokenBalance() throws IOException {
|
||||||
|
if (!credentialsAvailable()) {
|
||||||
|
throw new IOException("Not logged in");
|
||||||
|
}
|
||||||
|
return ((Cam4HttpClient)getHttpClient()).getTokenBalance();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBuyTokensLink() {
|
||||||
|
return getAffiliateLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void login() throws IOException {
|
||||||
|
if (credentialsAvailable()) {
|
||||||
|
boolean success = getHttpClient().login();
|
||||||
|
LoggerFactory.getLogger(getClass()).debug("Login success: {}", success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient getHttpClient() {
|
||||||
|
if(httpClient == null) {
|
||||||
|
httpClient = new Cam4HttpClient();
|
||||||
|
}
|
||||||
|
return httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
getHttpClient().shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() throws IOException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsTips() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsFollow() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSiteForModel(Model m) {
|
||||||
|
return m instanceof Cam4Model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean credentialsAvailable() {
|
||||||
|
String username = Config.getInstance().getSettings().cam4Username;
|
||||||
|
return username != null && !username.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Node getConfigurationGui() {
|
||||||
|
GridPane layout = SettingsTab.createGridLayout();
|
||||||
|
layout.add(new Label("Cam4 User"), 0, 0);
|
||||||
|
TextField username = new TextField(Config.getInstance().getSettings().cam4Username);
|
||||||
|
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().cam4Username = username.getText());
|
||||||
|
GridPane.setFillWidth(username, true);
|
||||||
|
GridPane.setHgrow(username, Priority.ALWAYS);
|
||||||
|
GridPane.setColumnSpan(username, 2);
|
||||||
|
layout.add(username, 1, 0);
|
||||||
|
|
||||||
|
layout.add(new Label("Cam4 Password"), 0, 1);
|
||||||
|
PasswordField password = new PasswordField();
|
||||||
|
password.setText(Config.getInstance().getSettings().cam4Password);
|
||||||
|
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().cam4Password = password.getText());
|
||||||
|
GridPane.setFillWidth(password, true);
|
||||||
|
GridPane.setHgrow(password, Priority.ALWAYS);
|
||||||
|
GridPane.setColumnSpan(password, 2);
|
||||||
|
layout.add(password, 1, 1);
|
||||||
|
|
||||||
|
Button createAccount = new Button("Create new Account");
|
||||||
|
createAccount.setOnAction((e) -> DesktopIntergation.open(Cam4.AFFILIATE_LINK));
|
||||||
|
layout.add(createAccount, 1, 2);
|
||||||
|
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,75 @@
|
||||||
|
package ctbrec.sites.cam4;
|
||||||
|
|
||||||
|
import ctbrec.ui.FollowedTab;
|
||||||
|
import ctbrec.ui.ThumbOverviewTab;
|
||||||
|
import javafx.concurrent.WorkerStateEvent;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.RadioButton;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
public class Cam4FollowedTab extends ThumbOverviewTab implements FollowedTab {
|
||||||
|
private Label status;
|
||||||
|
|
||||||
|
public Cam4FollowedTab(Cam4 cam4) {
|
||||||
|
super("Followed", new Cam4FollowedUpdateService(cam4), cam4);
|
||||||
|
status = new Label("Logging in...");
|
||||||
|
grid.getChildren().add(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createGui() {
|
||||||
|
super.createGui();
|
||||||
|
addOnlineOfflineSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
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) -> {
|
||||||
|
((Cam4FollowedUpdateService)updateService).setShowOnline(online.isSelected());
|
||||||
|
queue.clear();
|
||||||
|
updateService.restart();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSuccess() {
|
||||||
|
grid.getChildren().remove(status);
|
||||||
|
super.onSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onFail(WorkerStateEvent event) {
|
||||||
|
status.setText("Login failed");
|
||||||
|
super.onFail(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void selected() {
|
||||||
|
status.setText("Logging in...");
|
||||||
|
super.selected();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScene(Scene scene) {
|
||||||
|
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||||
|
if(this.isSelected()) {
|
||||||
|
if(event.getCode() == KeyCode.DELETE) {
|
||||||
|
follow(selectedThumbCells, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
package ctbrec.sites.cam4;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.jsoup.select.Elements;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.ui.HtmlParser;
|
||||||
|
import ctbrec.ui.PaginatedScheduledService;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class Cam4FollowedUpdateService extends PaginatedScheduledService {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(Cam4FollowedUpdateService.class);
|
||||||
|
private Cam4 site;
|
||||||
|
private boolean showOnline = true;
|
||||||
|
|
||||||
|
public Cam4FollowedUpdateService(Cam4 site) {
|
||||||
|
this.site = site;
|
||||||
|
ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
|
||||||
|
@Override
|
||||||
|
public Thread newThread(Runnable r) {
|
||||||
|
Thread t = new Thread(r);
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.setName("ThumbOverviewTab UpdateService");
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setExecutor(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<List<Model>> createTask() {
|
||||||
|
return new Task<List<Model>>() {
|
||||||
|
@Override
|
||||||
|
public List<Model> call() throws IOException {
|
||||||
|
List<Model> models = new ArrayList<>();
|
||||||
|
String username = Config.getInstance().getSettings().cam4Username;
|
||||||
|
String url = site.getBaseUrl() + '/' + username + "/edit/friends_favorites";
|
||||||
|
Request req = new Request.Builder().url(url).build();
|
||||||
|
Response response = site.getHttpClient().execute(req, true);
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
String content = response.body().string();
|
||||||
|
Elements cells = HtmlParser.getTags(content, "div#favorites div.ff_thumb");
|
||||||
|
for (Element cell : cells) {
|
||||||
|
String cellHtml = cell.html();
|
||||||
|
Element link = HtmlParser.getTag(cellHtml, "div.ff_img a");
|
||||||
|
String path = link.attr("href");
|
||||||
|
String modelName = path.substring(1);
|
||||||
|
Cam4Model model = (Cam4Model) site.createModel(modelName);
|
||||||
|
model.setPreview("https://snapshots.xcdnpro.com/thumbnails/"+model.getName()+"?s=" + System.currentTimeMillis());
|
||||||
|
model.setOnlineState(parseOnlineState(cellHtml));
|
||||||
|
models.add(model);
|
||||||
|
}
|
||||||
|
return models.stream()
|
||||||
|
.filter(m -> {
|
||||||
|
try {
|
||||||
|
return m.isOnline() == showOnline;
|
||||||
|
} catch (IOException | ExecutionException | InterruptedException e) {
|
||||||
|
LOG.error("Couldn't determine online state", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
} else {
|
||||||
|
IOException e = new IOException(response.code() + " " + response.message());
|
||||||
|
response.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseOnlineState(String cellHtml) {
|
||||||
|
Element state = HtmlParser.getTag(cellHtml, "div.ff_name div");
|
||||||
|
return state.attr("class").equals("online") ? "NORMAL" : "OFFLINE";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setShowOnline(boolean online) {
|
||||||
|
this.showOnline = online;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package ctbrec.sites.cam4;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.HttpCookie;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import okhttp3.Cookie;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class Cam4HttpClient extends HttpClient {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(Cam4HttpClient.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean login() throws IOException {
|
||||||
|
if(loggedIn) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
|
Runnable showDialog = () -> {
|
||||||
|
// login with javafx WebView
|
||||||
|
Cam4LoginDialog loginDialog = new Cam4LoginDialog();
|
||||||
|
|
||||||
|
// transfer cookies from WebView to OkHttp cookie jar
|
||||||
|
transferCookies(loginDialog);
|
||||||
|
|
||||||
|
try {
|
||||||
|
queue.put(true);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOG.error("Error while signaling termination", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if(Platform.isFxApplicationThread()) {
|
||||||
|
showDialog.run();
|
||||||
|
} else {
|
||||||
|
Platform.runLater(showDialog);
|
||||||
|
try {
|
||||||
|
queue.take();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOG.error("Error while waiting for login dialog to close", e);
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loggedIn = checkLoginSuccess();
|
||||||
|
return loggedIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check, if the login worked by requesting unchecked mail
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private boolean checkLoginSuccess() throws IOException {
|
||||||
|
String mailUrl = Cam4.BASE_URI + "/mail/unreadThreads";
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(mailUrl)
|
||||||
|
.addHeader("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.build();
|
||||||
|
Response response = execute(req);
|
||||||
|
if(response.isSuccessful() && response.body().contentLength() > 0) {
|
||||||
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
|
return json.has("status") && Objects.equals("success", json.getString("status"));
|
||||||
|
} else {
|
||||||
|
response.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void transferCookies(Cam4LoginDialog loginDialog) {
|
||||||
|
HttpUrl redirectedUrl = HttpUrl.parse(loginDialog.getUrl());
|
||||||
|
List<Cookie> cookies = new ArrayList<>();
|
||||||
|
for (HttpCookie webViewCookie : loginDialog.getCookies()) {
|
||||||
|
Cookie cookie = Cookie.parse(redirectedUrl, webViewCookie.toString());
|
||||||
|
cookies.add(cookie);
|
||||||
|
}
|
||||||
|
cookieJar.saveFromResponse(redirectedUrl, cookies);
|
||||||
|
|
||||||
|
HttpUrl origUrl = HttpUrl.parse(Cam4LoginDialog.URL);
|
||||||
|
cookies = new ArrayList<>();
|
||||||
|
for (HttpCookie webViewCookie : loginDialog.getCookies()) {
|
||||||
|
Cookie cookie = Cookie.parse(origUrl, webViewCookie.toString());
|
||||||
|
cookies.add(cookie);
|
||||||
|
}
|
||||||
|
cookieJar.saveFromResponse(origUrl, cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getTokenBalance() throws IOException {
|
||||||
|
if(!loggedIn) {
|
||||||
|
login();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException("Not implemented, yet");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package ctbrec.sites.cam4;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.CookieHandler;
|
||||||
|
import java.net.CookieManager;
|
||||||
|
import java.net.HttpCookie;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import javafx.concurrent.Worker.State;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.ProgressIndicator;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.web.WebEngine;
|
||||||
|
import javafx.scene.web.WebView;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
public class Cam4LoginDialog {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(Cam4LoginDialog.class);
|
||||||
|
public static final String URL = Cam4.BASE_URI + "/login";
|
||||||
|
private List<HttpCookie> cookies = null;
|
||||||
|
private String url;
|
||||||
|
private Region veil;
|
||||||
|
private ProgressIndicator p;
|
||||||
|
|
||||||
|
public Cam4LoginDialog() {
|
||||||
|
Stage stage = new Stage();
|
||||||
|
stage.setTitle("Cam4 Login");
|
||||||
|
InputStream icon = getClass().getResourceAsStream("/icon.png");
|
||||||
|
stage.getIcons().add(new Image(icon));
|
||||||
|
CookieManager cookieManager = new CookieManager();
|
||||||
|
CookieHandler.setDefault(cookieManager);
|
||||||
|
WebView webView = createWebView(stage);
|
||||||
|
|
||||||
|
veil = new Region();
|
||||||
|
veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.4)");
|
||||||
|
p = new ProgressIndicator();
|
||||||
|
p.setMaxSize(140, 140);
|
||||||
|
|
||||||
|
StackPane stackPane = new StackPane();
|
||||||
|
stackPane.getChildren().addAll(webView, veil, p);
|
||||||
|
|
||||||
|
stage.setScene(new Scene(stackPane, 480, 854));
|
||||||
|
stage.showAndWait();
|
||||||
|
cookies = cookieManager.getCookieStore().getCookies();
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebView createWebView(Stage stage) {
|
||||||
|
WebView browser = new WebView();
|
||||||
|
WebEngine webEngine = browser.getEngine();
|
||||||
|
webEngine.setJavaScriptEnabled(true);
|
||||||
|
webEngine.locationProperty().addListener((obs, oldV, newV) -> {
|
||||||
|
try {
|
||||||
|
URL _url = new URL(newV);
|
||||||
|
if (Objects.equals(_url.getPath(), "/")) {
|
||||||
|
stage.close();
|
||||||
|
}
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
LOG.error("Couldn't parse new url {}", newV, e);
|
||||||
|
}
|
||||||
|
url = newV.toString();
|
||||||
|
});
|
||||||
|
webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> {
|
||||||
|
if (newState == State.SUCCEEDED) {
|
||||||
|
String username = Config.getInstance().getSettings().cam4Username;
|
||||||
|
if (username != null && !username.trim().isEmpty()) {
|
||||||
|
webEngine.executeScript("$('input[name=username]').attr('value','" + username + "')");
|
||||||
|
}
|
||||||
|
String password = Config.getInstance().getSettings().cam4Password;
|
||||||
|
if (password != null && !password.trim().isEmpty()) {
|
||||||
|
webEngine.executeScript("$('input[name=password]').attr('value','" + password + "')");
|
||||||
|
}
|
||||||
|
webEngine.executeScript("$('div[class~=navbar]').css('display','none')");
|
||||||
|
webEngine.executeScript("$('div#footer').css('display','none')");
|
||||||
|
webEngine.executeScript("$('div#content').css('padding','0')");
|
||||||
|
veil.setVisible(false);
|
||||||
|
p.setVisible(false);
|
||||||
|
} else if (newState == State.CANCELLED || newState == State.FAILED) {
|
||||||
|
veil.setVisible(false);
|
||||||
|
p.setVisible(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
webEngine.load(URL);
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<HttpCookie> getCookies() {
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
package ctbrec.sites.cam4;
|
||||||
|
|
||||||
|
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 org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
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 ctbrec.AbstractModel;
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
import ctbrec.sites.Site;
|
||||||
|
import ctbrec.ui.HtmlParser;
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class Cam4Model extends AbstractModel {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(Cam4Model.class);
|
||||||
|
private Cam4 site;
|
||||||
|
private String playlistUrl;
|
||||||
|
private String onlineState = "offline";
|
||||||
|
private int[] resolution = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
||||||
|
return isOnline(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||||
|
if(ignoreCache || onlineState == null) {
|
||||||
|
loadModelDetails();
|
||||||
|
}
|
||||||
|
return Objects.equals("NORMAL", onlineState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadModelDetails() throws IOException {
|
||||||
|
String url = "https://www.cam4.de.com/getBroadcasting?usernames=" + getName();
|
||||||
|
LOG.debug("Loading model details {}", url);
|
||||||
|
Request req = new Request.Builder().url(url).build();
|
||||||
|
Response response = site.getHttpClient().execute(req);
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
JSONArray json = new JSONArray(response.body().string());
|
||||||
|
if(json.length() == 0) {
|
||||||
|
throw new IOException("Couldn't fetch model details");
|
||||||
|
}
|
||||||
|
JSONObject details = json.getJSONObject(0);
|
||||||
|
onlineState = details.getString("showType");
|
||||||
|
playlistUrl = details.getString("hlsPreviewUrl");
|
||||||
|
if(details.has("resolution")) {
|
||||||
|
String res = details.getString("resolution");
|
||||||
|
String[] tokens = res.split(":");
|
||||||
|
resolution = new int[] {Integer.parseInt(tokens[0]), Integer.parseInt(tokens[1])};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
IOException io = new IOException(response.code() + " " + response.message());
|
||||||
|
response.close();
|
||||||
|
throw io;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||||
|
return onlineState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPlaylistUrl() throws IOException {
|
||||||
|
if(playlistUrl == null) {
|
||||||
|
loadModelDetails();
|
||||||
|
}
|
||||||
|
return playlistUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||||
|
MasterPlaylist masterPlaylist = getMasterPlaylist();
|
||||||
|
List<StreamSource> sources = new ArrayList<>();
|
||||||
|
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||||
|
if (playlist.hasStreamInfo()) {
|
||||||
|
StreamSource src = new StreamSource();
|
||||||
|
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||||
|
src.height = playlist.getStreamInfo().getResolution().height;
|
||||||
|
String masterUrl = getPlaylistUrl();
|
||||||
|
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||||
|
String segmentUri = baseUrl + playlist.getUri();
|
||||||
|
src.mediaPlaylistUrl = segmentUri;
|
||||||
|
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||||
|
sources.add(src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sources;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException {
|
||||||
|
LOG.trace("Loading master playlist {}", getPlaylistUrl());
|
||||||
|
Request req = new Request.Builder().url(getPlaylistUrl()).build();
|
||||||
|
Response response = site.getHttpClient().execute(req);
|
||||||
|
try {
|
||||||
|
InputStream inputStream = response.body().byteStream();
|
||||||
|
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
|
||||||
|
Playlist playlist = parser.parse();
|
||||||
|
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||||
|
return master;
|
||||||
|
} finally {
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateCacheEntries() {
|
||||||
|
resolution = null;
|
||||||
|
playlistUrl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receiveTip(int tokens) throws IOException {
|
||||||
|
throw new RuntimeException("Not implemented for Cam4, yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
|
||||||
|
if(resolution == null) {
|
||||||
|
if(failFast) {
|
||||||
|
return new int[2];
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
loadModelDetails();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ExecutionException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean follow() throws IOException {
|
||||||
|
String url = site.getBaseUrl() + "/profiles/addFriendFavorite?action=addFavorite&object=" + getName() + "&_=" + System.currentTimeMillis();
|
||||||
|
Request req = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.addHeader("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.build();
|
||||||
|
Response response = site.getHttpClient().execute(req, true);
|
||||||
|
boolean success = response.isSuccessful();
|
||||||
|
response.close();
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean unfollow() throws IOException {
|
||||||
|
// get model user id
|
||||||
|
String url = site.getBaseUrl() + '/' + getName();
|
||||||
|
Request req = new Request.Builder().url(url).build();
|
||||||
|
Response response = site.getHttpClient().execute(req, true);
|
||||||
|
String broadCasterId = null;
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
String content = response.body().string();
|
||||||
|
try {
|
||||||
|
Element tag = HtmlParser.getTag(content, "input[name=\"broadcasterId\"]");
|
||||||
|
broadCasterId = tag.attr("value");
|
||||||
|
} catch(Exception e) {
|
||||||
|
LOG.debug(content);
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// send unfollow request
|
||||||
|
String username = Config.getInstance().getSettings().cam4Username;
|
||||||
|
url = site.getBaseUrl() + '/' + username + "/edit/friends_favorites";
|
||||||
|
RequestBody body = new FormBody.Builder()
|
||||||
|
.add("deleteFavorites", broadCasterId)
|
||||||
|
.add("simpleresult", "true")
|
||||||
|
.build();
|
||||||
|
req = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.post(body)
|
||||||
|
.addHeader("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.build();
|
||||||
|
response = site.getHttpClient().execute(req, true);
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
return Objects.equals(response.body().string(), "Ok");
|
||||||
|
} else {
|
||||||
|
response.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSite(Site site) {
|
||||||
|
if(site instanceof Cam4) {
|
||||||
|
this.site = (Cam4) site;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Site has to be an instance of Cam4");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Site getSite() {
|
||||||
|
return site;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlaylistUrl(String playlistUrl) {
|
||||||
|
this.playlistUrl = playlistUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnlineState(String onlineState) {
|
||||||
|
this.onlineState = onlineState;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package ctbrec.sites.cam4;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import ctbrec.recorder.Recorder;
|
||||||
|
import ctbrec.ui.TabProvider;
|
||||||
|
import ctbrec.ui.ThumbOverviewTab;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Tab;
|
||||||
|
|
||||||
|
public class Cam4TabProvider extends TabProvider {
|
||||||
|
|
||||||
|
private Cam4 cam4;
|
||||||
|
private Recorder recorder;
|
||||||
|
|
||||||
|
public Cam4TabProvider(Cam4 cam4, Recorder recorder) {
|
||||||
|
this.cam4 = cam4;
|
||||||
|
this.recorder = recorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Tab> getTabs(Scene scene) {
|
||||||
|
List<Tab> tabs = new ArrayList<>();
|
||||||
|
|
||||||
|
tabs.add(createTab("Female", cam4.getBaseUrl() + "/directoryResults?online=true&gender=female&orderBy=MOST_VIEWERS"));
|
||||||
|
tabs.add(createTab("Male", cam4.getBaseUrl() + "/directoryResults?online=true&gender=male&orderBy=MOST_VIEWERS"));
|
||||||
|
tabs.add(createTab("Couples", cam4.getBaseUrl() + "/directoryResults?online=true&broadcastType=male_group&broadcastType=female_group&broadcastType=male_female_group&orderBy=MOST_VIEWERS"));
|
||||||
|
tabs.add(createTab("HD", cam4.getBaseUrl() + "/directoryResults?online=true&hd=true&orderBy=MOST_VIEWERS"));
|
||||||
|
|
||||||
|
Cam4FollowedTab followed = new Cam4FollowedTab(cam4);
|
||||||
|
followed.setRecorder(recorder);
|
||||||
|
tabs.add(followed);
|
||||||
|
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tab createTab(String name, String url) {
|
||||||
|
Cam4UpdateService updateService = new Cam4UpdateService(url, false, cam4);
|
||||||
|
ThumbOverviewTab tab = new ThumbOverviewTab(name, updateService, cam4);
|
||||||
|
tab.setRecorder(recorder);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
package ctbrec.sites.cam4;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.util.StringUtil;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.jsoup.select.Elements;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.ui.HtmlParser;
|
||||||
|
import ctbrec.ui.PaginatedScheduledService;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class Cam4UpdateService extends PaginatedScheduledService {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(Cam4UpdateService.class);
|
||||||
|
private String url;
|
||||||
|
private Cam4 site;
|
||||||
|
private boolean loginRequired;
|
||||||
|
|
||||||
|
public Cam4UpdateService(String url, boolean loginRequired, Cam4 site) {
|
||||||
|
this.site = site;
|
||||||
|
this.url = url;
|
||||||
|
this.loginRequired = loginRequired;
|
||||||
|
|
||||||
|
ExecutorService executor = Executors.newSingleThreadExecutor(new ThreadFactory() {
|
||||||
|
@Override
|
||||||
|
public Thread newThread(Runnable r) {
|
||||||
|
Thread t = new Thread(r);
|
||||||
|
t.setDaemon(true);
|
||||||
|
t.setName("ThumbOverviewTab UpdateService");
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setExecutor(executor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<List<Model>> createTask() {
|
||||||
|
return new Task<List<Model>>() {
|
||||||
|
@Override
|
||||||
|
public List<Model> call() throws IOException {
|
||||||
|
if(loginRequired && StringUtil.isBlank(Config.getInstance().getSettings().cam4Username)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
} else {
|
||||||
|
String url = Cam4UpdateService.this.url + "&page=" + page;
|
||||||
|
LOG.debug("Fetching page {}", url);
|
||||||
|
Request request = new Request.Builder().url(url).build();
|
||||||
|
Response response = site.getHttpClient().execute(request, loginRequired);
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
|
String html = json.getString("html");
|
||||||
|
Elements profilesBoxes = HtmlParser.getTags(html, "div[class~=profileDataBox]");
|
||||||
|
List<Model> models = new ArrayList<>(profilesBoxes.size());
|
||||||
|
for (Element profileBox : profilesBoxes) {
|
||||||
|
String boxHtml = profileBox.html();
|
||||||
|
Element profileLink = HtmlParser.getTag(boxHtml, "a.profile-preview");
|
||||||
|
String path = profileLink.attr("href");
|
||||||
|
String slug = path.substring(1);
|
||||||
|
Cam4Model model = (Cam4Model) site.createModel(slug);
|
||||||
|
String playlistUrl = profileLink.attr("data-hls-preview-url");
|
||||||
|
model.setPlaylistUrl(playlistUrl);
|
||||||
|
model.setPreview("https://snapshots.xcdnpro.com/thumbnails/"+model.getName()+"?s=" + System.currentTimeMillis());
|
||||||
|
model.setDescription(parseDesription(boxHtml));
|
||||||
|
models.add(model);
|
||||||
|
}
|
||||||
|
response.close();
|
||||||
|
return models;
|
||||||
|
} else {
|
||||||
|
int code = response.code();
|
||||||
|
response.close();
|
||||||
|
throw new IOException("HTTP status " + code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseDesription(String boxHtml) {
|
||||||
|
try {
|
||||||
|
return HtmlParser.getText(boxHtml, "div[class~=statusMsg2]");
|
||||||
|
} catch(Exception e) {
|
||||||
|
LOG.trace("Couldn't parse description for room");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -26,7 +26,6 @@ import com.squareup.moshi.Moshi;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
import ctbrec.Settings;
|
|
||||||
import ctbrec.io.HttpClient;
|
import ctbrec.io.HttpClient;
|
||||||
import ctbrec.recorder.Recorder;
|
import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.sites.AbstractSite;
|
import ctbrec.sites.AbstractSite;
|
||||||
|
@ -121,8 +120,7 @@ public class Chaturbate extends AbstractSite {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void login() {
|
public void login() {
|
||||||
Settings settings = Config.getInstance().getSettings();
|
if (credentialsAvailable()) {
|
||||||
if (settings.username != null && !settings.username.isEmpty()) {
|
|
||||||
new Thread() {
|
new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.recorder.RemoteRecorder;
|
import ctbrec.recorder.RemoteRecorder;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import ctbrec.sites.camsoda.Camsoda;
|
import ctbrec.sites.camsoda.Camsoda;
|
||||||
|
import ctbrec.sites.cam4.Cam4;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
import ctbrec.sites.chaturbate.Chaturbate;
|
||||||
import ctbrec.sites.mfc.MyFreeCams;
|
import ctbrec.sites.mfc.MyFreeCams;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
|
@ -62,6 +63,7 @@ public class CamrecApplication extends Application {
|
||||||
sites.add(new Chaturbate());
|
sites.add(new Chaturbate());
|
||||||
sites.add(new MyFreeCams());
|
sites.add(new MyFreeCams());
|
||||||
sites.add(new Camsoda());
|
sites.add(new Camsoda());
|
||||||
|
sites.add(new Cam4());
|
||||||
loadConfig();
|
loadConfig();
|
||||||
createHttpClient();
|
createHttpClient();
|
||||||
bus = new AsyncEventBus(Executors.newSingleThreadExecutor());
|
bus = new AsyncEventBus(Executors.newSingleThreadExecutor());
|
||||||
|
|
|
@ -6,6 +6,7 @@ import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -50,7 +51,11 @@ public class CookieJarImpl implements CookieJar {
|
||||||
public List<Cookie> loadForRequest(HttpUrl url) {
|
public List<Cookie> loadForRequest(HttpUrl url) {
|
||||||
String host = getHost(url);
|
String host = getHost(url);
|
||||||
List<Cookie> cookies = cookieStore.get(host);
|
List<Cookie> cookies = cookieStore.get(host);
|
||||||
LOG.debug("Cookies for {}: {}", url.host(), cookies);
|
LOG.debug("Cookies for {}", url);
|
||||||
|
Optional.ofNullable(cookies).ifPresent(cookiez -> cookiez.forEach(c -> {
|
||||||
|
LOG.debug(" {} expires on:{}", c, c.expiresAt());
|
||||||
|
}));
|
||||||
|
//LOG.debug("Cookies for {}: {}", url.host(), cookies);
|
||||||
return cookies != null ? cookies : new ArrayList<Cookie>();
|
return cookies != null ? cookies : new ArrayList<Cookie>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -208,6 +208,8 @@ public class ThumbCell extends StackPane {
|
||||||
LOG.trace("Removing invalid resolution value for {}", model.getName());
|
LOG.trace("Removing invalid resolution value for {}", model.getName());
|
||||||
model.invalidateCacheEntries();
|
model.invalidateCacheEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Thread.sleep(500);
|
||||||
} catch (IOException | InterruptedException e1) {
|
} catch (IOException | InterruptedException e1) {
|
||||||
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1);
|
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1);
|
||||||
} catch(ExecutionException e) {
|
} catch(ExecutionException e) {
|
||||||
|
@ -419,13 +421,11 @@ public class ThumbCell extends StackPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setModel(Model model) {
|
public void setModel(Model model) {
|
||||||
//this.model = model;
|
|
||||||
this.model.setName(model.getName());
|
this.model.setName(model.getName());
|
||||||
this.model.setDescription(model.getDescription());
|
this.model.setDescription(model.getDescription());
|
||||||
this.model.setPreview(model.getPreview());
|
this.model.setPreview(model.getPreview());
|
||||||
this.model.setTags(model.getTags());
|
this.model.setTags(model.getTags());
|
||||||
this.model.setUrl(model.getUrl());
|
this.model.setUrl(model.getUrl());
|
||||||
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue