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:
0xboobface 2018-10-30 19:06:20 +01:00
commit b476c452ad
24 changed files with 1013 additions and 28 deletions

View File

@ -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
========================
* Added support for multiple cam sites

9
ctbrec-macos.sh Executable file
View File

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

View File

@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ctbrec</groupId>
<artifactId>ctbrec</artifactId>
<version>1.6.0</version>
<version>1.6.1</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@ -66,6 +66,7 @@
<descriptor>src/assembly/win64-jre.xml</descriptor>
<descriptor>src/assembly/win32-jre.xml</descriptor>
<descriptor>src/assembly/linux.xml</descriptor>
<descriptor>src/assembly/macos-jre.xml</descriptor>
</descriptors>
</configuration>
</execution>
@ -77,7 +78,7 @@
<version>1.7.22</version>
<executions>
<execution>
<id>l4j-clui</id>
<id>l4j-win</id>
<phase>package</phase>
<goals>
<goal>launch4j</goal>

9
server-macos.sh Executable file
View File

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

View File

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

View File

@ -22,7 +22,7 @@
</files>
<fileSets>
<fileSet>
<directory>jre/jre1.8.0_181_win32</directory>
<directory>jre/jre1.8.0_192_win32</directory>
<includes>
<include>**/*</include>
</includes>

View File

@ -22,7 +22,7 @@
</files>
<fileSets>
<fileSet>
<directory>jre/jre1.8.0_181_win64</directory>
<directory>jre/jre1.8.0_192_win64</directory>
<includes>
<include>**/*</include>
</includes>

View File

@ -26,6 +26,8 @@ public class Settings {
public String mfcPassword = "";
public String camsodaUsername = "";
public String camsodaPassword = "";
public String cam4Username;
public String cam4Password;
public String lastDownloadDir = "";
public List<Model> models = new ArrayList<Model>();

View File

@ -69,7 +69,7 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
}
return model;
} 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);
}
}

View File

@ -166,7 +166,12 @@ public class LocalRecorder implements Recorder {
@Override
public List<Model> getModelsRecording() {
return Collections.unmodifiableList(new ArrayList<>(models));
lock.lock();
try {
return Collections.unmodifiableList(new ArrayList<>(models));
} finally {
lock.unlock();
}
}
@Override
@ -308,24 +313,19 @@ public class LocalRecorder implements Recorder {
public void run() {
running = true;
while (running) {
lock.lock();
try {
for (Model model : getModelsRecording()) {
try {
if (!recordingProcesses.containsKey(model)) {
boolean isOnline = model.isOnline(IGNORE_CACHE);
LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline"));
if (isOnline) {
LOG.info("Model {}'s room back to public. Starting recording", model);
startRecordingProcess(model);
}
for (Model model : getModelsRecording()) {
try {
if (!recordingProcesses.containsKey(model)) {
boolean isOnline = model.isOnline(IGNORE_CACHE);
LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline"));
if (isOnline) {
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 {

View File

@ -13,6 +13,7 @@ import java.util.concurrent.Executors;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.ParsingMode;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MediaPlaylist;
@ -41,7 +42,7 @@ public abstract class AbstractHlsDownload implements Download {
Response response = client.execute(request);
try {
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();
if(playlist.hasMediaPlaylist()) {
MediaPlaylist mediaPlaylist = playlist.getMediaPlaylist();

View File

@ -20,6 +20,7 @@ import ctbrec.Config;
import ctbrec.recorder.LocalRecorder;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.mfc.MyFreeCams;
@ -64,6 +65,7 @@ public class HttpServer {
sites.add(new Chaturbate());
sites.add(new MyFreeCams());
sites.add(new Camsoda());
sites.add(new Cam4());
}
private void addShutdownHook() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,7 +26,6 @@ import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.Settings;
import ctbrec.io.HttpClient;
import ctbrec.recorder.Recorder;
import ctbrec.sites.AbstractSite;
@ -121,8 +120,7 @@ public class Chaturbate extends AbstractSite {
@Override
public void login() {
Settings settings = Config.getInstance().getSettings();
if (settings.username != null && !settings.username.isEmpty()) {
if (credentialsAvailable()) {
new Thread() {
@Override
public void run() {

View File

@ -28,6 +28,7 @@ import ctbrec.recorder.Recorder;
import ctbrec.recorder.RemoteRecorder;
import ctbrec.sites.Site;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.mfc.MyFreeCams;
import javafx.application.Application;
@ -62,6 +63,7 @@ public class CamrecApplication extends Application {
sites.add(new Chaturbate());
sites.add(new MyFreeCams());
sites.add(new Camsoda());
sites.add(new Cam4());
loadConfig();
createHttpClient();
bus = new AsyncEventBus(Executors.newSingleThreadExecutor());

View File

@ -6,6 +6,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -50,7 +51,11 @@ public class CookieJarImpl implements CookieJar {
public List<Cookie> loadForRequest(HttpUrl url) {
String host = getHost(url);
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>();
}

View File

@ -208,6 +208,8 @@ public class ThumbCell extends StackPane {
LOG.trace("Removing invalid resolution value for {}", model.getName());
model.invalidateCacheEntries();
}
Thread.sleep(500);
} catch (IOException | InterruptedException e1) {
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1);
} catch(ExecutionException e) {
@ -419,13 +421,11 @@ public class ThumbCell extends StackPane {
}
public void setModel(Model model) {
//this.model = model;
this.model.setName(model.getName());
this.model.setDescription(model.getDescription());
this.model.setPreview(model.getPreview());
this.model.setTags(model.getTags());
this.model.setUrl(model.getUrl());
update();
}