Merge branch 'dev'

This commit is contained in:
0xboobface 2018-11-04 21:39:24 +01:00
commit 5de66ce373
37 changed files with 1649 additions and 119 deletions

View File

@ -1,3 +1,10 @@
1.7.0
========================
* Added CamSoda
* Added detection of model name changes for MyFreeCams
* Added setting to define a maximum resolution
* Fixed sorting by date in recordings table
1.6.1
========================
* Fixed UI freeze, which occured for a high number of recorded models

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.1</version>
<version>1.7.0</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

@ -2,14 +2,11 @@ package ctbrec;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.recorder.download.StreamSource;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
public abstract class AbstractModel implements Model {
@ -20,6 +17,11 @@ public abstract class AbstractModel implements Model {
private List<String> tags = new ArrayList<>();
private int streamUrlIndex = -1;
@Override
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
return isOnline(false);
}
@Override
public String getUrl() {
return url;
@ -81,16 +83,13 @@ public abstract class AbstractModel implements Model {
}
@Override
public String getSegmentPlaylistUrl() throws IOException, ExecutionException, ParseException, PlaylistException {
List<StreamSource> streamSources = getStreamSources();
String url = null;
if(getStreamUrlIndex() >= 0 && getStreamUrlIndex() < streamSources.size()) {
url = streamSources.get(getStreamUrlIndex()).getMediaPlaylistUrl();
} else {
Collections.sort(streamSources);
url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl();
}
return url;
public void readSiteSpecificData(JsonReader reader) throws IOException {
// noop default implementation, can be overriden by concrete models
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
// noop default implementation, can be overriden by concrete models
}
@Override

View File

@ -6,6 +6,8 @@ import java.util.concurrent.ExecutionException;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.recorder.download.StreamSource;
import ctbrec.sites.Site;
@ -27,7 +29,6 @@ public interface Model {
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException;
public String getOnlineState(boolean failFast) throws IOException, ExecutionException;
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException;
public String getSegmentPlaylistUrl() throws IOException, ExecutionException, ParseException, PlaylistException;
public void invalidateCacheEntries();
public void receiveTip(int tokens) throws IOException;
public int[] getStreamResolution(boolean failFast) throws ExecutionException;
@ -35,4 +36,6 @@ public interface Model {
public boolean unfollow() throws IOException;
public void setSite(Site site);
public Site getSite();
public void writeSiteSpecificData(JsonWriter writer) throws IOException;
public void readSiteSpecificData(JsonReader reader) throws IOException;
}

View File

@ -24,6 +24,8 @@ public class Settings {
public String password = ""; // chaturbate password TODO maybe rename this onetime
public String mfcUsername = "";
public String mfcPassword = "";
public String camsodaUsername = "";
public String camsodaPassword = "";
public String cam4Username;
public String cam4Password;
public String lastDownloadDir = "";
@ -32,6 +34,7 @@ public class Settings {
public boolean determineResolution = false;
public boolean requireAuthentication = false;
public boolean chooseStreamQuality = false;
public int maximumResolution = 0;
public byte[] key = null;
public ProxyType proxyType = ProxyType.DIRECT;
public String proxyHost;

View File

@ -1,4 +1,4 @@
package ctbrec.ui;
package ctbrec.io;
import java.util.ArrayList;
import java.util.HashMap;
@ -34,6 +34,7 @@ public class CookieJarImpl implements CookieJar {
if(newCookie.name().equalsIgnoreCase(name)) {
LOG.debug("Replacing cookie {} {} -> {} [{}]", oldCookie.name(), oldCookie.value(), newCookie.value(), oldCookie.domain());
iterator.remove();
break;
}
}
}

View File

@ -7,7 +7,6 @@ import java.util.concurrent.TimeUnit;
import ctbrec.Config;
import ctbrec.Settings.ProxyType;
import ctbrec.ui.CookieJarImpl;
import okhttp3.ConnectionPool;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;

View File

@ -32,45 +32,51 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
String url = null;
String type = null;
int streamUrlIndex = -1;
Model model = null;
while(reader.hasNext()) {
Token token = reader.peek();
if(token == Token.NAME) {
String key = reader.nextName();
if(key.equals("name")) {
name = reader.nextString();
} else if(key.equals("description")) {
description = reader.nextString();
} else if(key.equals("url")) {
url = reader.nextString();
} else if(key.equals("type")) {
type = reader.nextString();
} else if(key.equals("streamUrlIndex")) {
streamUrlIndex = reader.nextInt();
try {
Token token = reader.peek();
if(token == Token.NAME) {
String key = reader.nextName();
if(key.equals("name")) {
name = reader.nextString();
model.setName(name);
} else if(key.equals("description")) {
description = reader.nextString();
model.setDescription(description);
} else if(key.equals("url")) {
url = reader.nextString();
model.setUrl(url);
} else if(key.equals("type")) {
type = reader.nextString();
Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()));
model = (Model) modelClass.newInstance();
} else if(key.equals("streamUrlIndex")) {
streamUrlIndex = reader.nextInt();
model.setStreamUrlIndex(streamUrlIndex);
} else if(key.equals("siteSpecific")) {
reader.beginObject();
model.readSiteSpecificData(reader);
reader.endObject();
}
} else {
reader.skipValue();
}
} else {
reader.skipValue();
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new IOException("Couldn't instantiate model class [" + type + "]", e);
}
}
reader.endObject();
try {
Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()));
Model model = (Model) modelClass.newInstance();
model.setName(name);
model.setDescription(description);
model.setUrl(url);
model.setStreamUrlIndex(streamUrlIndex);
if(sites != null) {
for (Site site : sites) {
if(site.isSiteForModel(model)) {
model.setSite(site);
}
if(sites != null) {
for (Site site : sites) {
if(site.isSiteForModel(model)) {
model.setSite(site);
}
}
return model;
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
throw new IOException("Couldn't instantiate model class [" + type + "]", e);
}
return model;
}
@Override
@ -81,6 +87,10 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
writeValueIfSet(writer, "description", model.getDescription());
writeValueIfSet(writer, "url", model.getUrl());
writer.name("streamUrlIndex").value(model.getStreamUrlIndex());
writer.name("siteSpecific");
writer.beginObject();
model.writeSiteSpecificData(writer);
writer.endObject();
writer.endObject();
}

View File

@ -6,10 +6,16 @@ import java.io.InputStream;
import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
@ -20,12 +26,16 @@ import com.iheartradio.m3u8.data.MediaPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.TrackData;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.io.HttpClient;
import okhttp3.Request;
import okhttp3.Response;
public abstract class AbstractHlsDownload implements Download {
private static final transient Logger LOG = LoggerFactory.getLogger(AbstractHlsDownload.class);
ExecutorService downloadThreadPool = Executors.newFixedThreadPool(5);
HttpClient client;
volatile boolean running = false;
@ -69,6 +79,34 @@ public abstract class AbstractHlsDownload implements Download {
}
}
String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException {
List<StreamSource> streamSources = model.getStreamSources();
String url = null;
if(model.getStreamUrlIndex() >= 0 && model.getStreamUrlIndex() < streamSources.size()) {
url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl();
} else {
Collections.sort(streamSources);
// filter out stream resolutions, which are too high
int maxRes = Config.getInstance().getSettings().maximumResolution;
if(maxRes > 0) {
for (Iterator<StreamSource> iterator = streamSources.iterator(); iterator.hasNext();) {
StreamSource streamSource = iterator.next();
if(streamSource.height > 0 && maxRes < streamSource.height) {
LOG.trace("Res too high {} > {}", streamSource.height, maxRes);
iterator.remove();
}
}
}
if(streamSources.isEmpty()) {
throw new ExecutionException(new RuntimeException("No stream left in playlist"));
} else {
url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl();
}
}
return url;
}
@Override
public boolean isAlive() {
return alive;

View File

@ -48,7 +48,7 @@ public class HlsDownload extends AbstractHlsDownload {
throw new IOException(model.getName() +"'s room is not public");
}
String segments = model.getSegmentPlaylistUrl();
String segments = getSegmentPlaylistUrl(model);
if(segments != null) {
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
Files.createDirectories(downloadDir);

View File

@ -101,7 +101,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
target = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-00000.ts"));
}
String segments = model.getSegmentPlaylistUrl();
String segments = getSegmentPlaylistUrl(model);
mergeThread = createMergeThread(target, null, true);
mergeThread.start();
if(segments != null) {

View File

@ -21,6 +21,7 @@ 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;
@ -53,7 +54,9 @@ public class HttpServer {
}
recorder = new LocalRecorder(config);
for (Site site : sites) {
site.init();
if(site.isEnabled()) {
site.init();
}
}
startHttpServer();
}
@ -61,6 +64,7 @@ public class HttpServer {
private void createSites() {
sites.add(new Chaturbate());
sites.add(new MyFreeCams());
sites.add(new Camsoda());
sites.add(new Cam4());
}

View File

@ -1,5 +1,6 @@
package ctbrec.sites.cam4;
import java.io.File;
import java.io.InputStream;
import java.net.CookieHandler;
import java.net.CookieManager;
@ -13,6 +14,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.OS;
import javafx.concurrent.Worker.State;
import javafx.scene.Scene;
import javafx.scene.control.ProgressIndicator;
@ -71,24 +73,29 @@ public class Cam4LoginDialog {
});
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);
try {
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')");
} catch(Exception e) {
LOG.warn("Couldn't auto fill username and password for Cam4", e);
}
} else if (newState == State.CANCELLED || newState == State.FAILED) {
veil.setVisible(false);
p.setVisible(false);
}
});
webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine"));
webEngine.load(URL);
return browser;
}

View File

@ -0,0 +1,171 @@
package ctbrec.sites.camsoda;
import java.io.IOException;
import org.json.JSONObject;
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;
import okhttp3.Request;
import okhttp3.Response;
public class Camsoda extends AbstractSite {
public static final String BASE_URI = "https://www.camsoda.com";
private Recorder recorder;
private HttpClient httpClient;
@Override
public String getName() {
return "CamSoda";
}
@Override
public String getBaseUrl() {
return BASE_URI;
}
@Override
public String getAffiliateLink() {
return BASE_URI;
}
@Override
public void setRecorder(Recorder recorder) {
this.recorder = recorder;
}
@Override
public TabProvider getTabProvider() {
return new CamsodaTabProvider(this, recorder);
}
@Override
public Model createModel(String name) {
CamsodaModel model = new CamsodaModel();
model.setName(name);
model.setUrl(getBaseUrl() + "/" + name);
model.setSite(this);
return model;
}
@Override
public Integer getTokenBalance() throws IOException {
if (!credentialsAvailable()) {
throw new IOException("Account settings not available");
}
String username = Config.getInstance().getSettings().camsodaUsername;
String url = BASE_URI + "/api/v1/user/" + username;
Request request = new Request.Builder().url(url).build();
Response response = getHttpClient().execute(request, true);
if(response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
if(json.has("user")) {
JSONObject user = json.getJSONObject("user");
if(user.has("tokens")) {
return user.getInt("tokens");
}
}
} else {
throw new IOException(response.code() + " " + response.message());
}
throw new RuntimeException("Tokens not found in response");
}
@Override
public String getBuyTokensLink() {
return getBaseUrl();
}
@Override
public void login() throws IOException {
if(credentialsAvailable()) {
getHttpClient().login();
}
}
@Override
public HttpClient getHttpClient() {
if(httpClient == null) {
httpClient = new CamsodaHttpClient();
}
return httpClient;
}
@Override
public void init() throws IOException {
}
@Override
public void shutdown() {
if(httpClient != null) {
httpClient.shutdown();
}
}
@Override
public boolean supportsTips() {
return true;
}
@Override
public boolean supportsFollow() {
return true;
}
@Override
public boolean isSiteForModel(Model m) {
return m instanceof CamsodaModel;
}
@Override
public boolean credentialsAvailable() {
String username = Config.getInstance().getSettings().camsodaUsername;
return username != null && !username.trim().isEmpty();
}
@Override
public Node getConfigurationGui() {
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("CamSoda User"), 0, 0);
TextField username = new TextField(Config.getInstance().getSettings().camsodaUsername);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().camsodaUsername = username.getText());
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(new Label("CamSoda Password"), 0, 1);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().camsodaPassword);
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().camsodaPassword = 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(getAffiliateLink()));
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,80 @@
package ctbrec.sites.camsoda;
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 CamsodaFollowedTab extends ThumbOverviewTab implements FollowedTab {
private Label status;
boolean showOnline = true;
public CamsodaFollowedTab(String title, Camsoda camsoda) {
super(title, new CamsodaFollowedUpdateService(camsoda), camsoda);
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) -> {
queue.clear();
((CamsodaFollowedUpdateService)updateService).showOnline(online.isSelected());
updateService.restart();
});
}
@Override
protected void onSuccess() {
grid.getChildren().remove(status);
super.onSuccess();
}
@Override
protected void onFail(WorkerStateEvent event) {
String msg = "";
if (event.getSource().getException() != null) {
msg = ": " + event.getSource().getException().getMessage();
}
status.setText("Login failed" + msg);
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,73 @@
package ctbrec.sites.camsoda;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import org.json.JSONArray;
import org.json.JSONObject;
import ctbrec.Model;
import ctbrec.ui.PaginatedScheduledService;
import javafx.concurrent.Task;
import okhttp3.Request;
import okhttp3.Response;
public class CamsodaFollowedUpdateService extends PaginatedScheduledService {
private Camsoda camsoda;
private boolean showOnline = true;
public CamsodaFollowedUpdateService(Camsoda camsoda) {
this.camsoda = camsoda;
}
@Override
protected Task<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException {
List<Model> models = new ArrayList<>();
String url = camsoda.getBaseUrl() + "/api/v1/user/current";
Request request = new Request.Builder().url(url).build();
Response response = camsoda.getHttpClient().execute(request, true);
if (response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
if(json.has("status") && json.getBoolean("status")) {
JSONObject user = json.getJSONObject("user");
JSONArray following = user.getJSONArray("following");
for (int i = 0; i < following.length(); i++) {
JSONObject m = following.getJSONObject(i);
CamsodaModel model = (CamsodaModel) camsoda.createModel(m.getString("followname"));
boolean online = m.getInt("online") == 1;
model.setOnlineState(online ? "online" : "offline");
model.setPreview("https://md.camsoda.com/thumbs/" + model.getName() + ".jpg");
models.add(model);
}
return models.stream()
.filter((m) -> {
try {
return m.isOnline() == showOnline;
} catch (IOException | ExecutionException | InterruptedException e) {
return false;
}
}).collect(Collectors.toList());
} else {
response.close();
return Collections.emptyList();
}
} else {
int code = response.code();
response.close();
throw new IOException("HTTP status " + code);
}
}
};
}
void showOnline(boolean online) {
this.showOnline = online;
}
}

View File

@ -0,0 +1,151 @@
package ctbrec.sites.camsoda;
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.jsoup.nodes.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import ctbrec.sites.cam4.Cam4LoginDialog;
import ctbrec.ui.HtmlParser;
import javafx.application.Platform;
import okhttp3.Cookie;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.Response;
public class CamsodaHttpClient extends HttpClient {
private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaHttpClient.class);
private String csrfToken = null;
@Override
public boolean login() throws IOException {
if(loggedIn) {
return true;
}
String url = Camsoda.BASE_URI + "/api/v1/auth/login";
FormBody body = new FormBody.Builder()
.add("username", Config.getInstance().getSettings().camsodaUsername)
.add("password", Config.getInstance().getSettings().camsodaPassword)
.build();
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
Response response = execute(request);
if(response.isSuccessful()) {
JSONObject resp = new JSONObject(response.body().string());
if(resp.has("error")) {
String error = resp.getString("error");
if (Objects.equals(error, "Please confirm that you are not a robot.")) {
//return loginWithDialog();
throw new IOException("CamSoda requested to solve a captcha. Please try again in a while (maybe 15 min).");
} else {
throw new IOException(resp.getString("error"));
}
} else {
return true;
}
} else {
throw new IOException(response.code() + " " + response.message());
}
}
@SuppressWarnings("unused")
private boolean loginWithDialog() throws IOException {
BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>();
Runnable showDialog = () -> {
// login with javafx WebView
CamsodaLoginDialog loginDialog = new CamsodaLoginDialog();
// 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
* @throws IOException
*/
private boolean checkLoginSuccess() throws IOException {
String url = Camsoda.BASE_URI + "/api/v1/user/current";
Request request = new Request.Builder().url(url).build();
try(Response response = execute(request)) {
if(response.isSuccessful()) {
JSONObject resp = new JSONObject(response.body().string());
return resp.optBoolean("status");
} else {
return false;
}
}
}
private void transferCookies(CamsodaLoginDialog 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 String getCsrfToken() throws IOException {
if(csrfToken == null) {
String url = Camsoda.BASE_URI;
Request request = new Request.Builder().url(url).build();
Response resp = execute(request, true);
if(resp.isSuccessful()) {
Element meta = HtmlParser.getTag(resp.body().string(), "meta[name=\"_token\"]");
csrfToken = meta.attr("content");
} else {
IOException e = new IOException(resp.code() + " " + resp.message());
resp.close();
throw e;
}
}
return csrfToken;
}
}

View File

@ -0,0 +1,109 @@
package ctbrec.sites.camsoda;
import java.io.File;
import java.io.InputStream;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.HttpCookie;
import java.util.Base64;
import java.util.List;
import ctbrec.OS;
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;
// FIXME this dialog does not help, because google's recaptcha does not work
// with WebView even though it does work in Cam4LoginDialog
public class CamsodaLoginDialog {
public static final String URL = Camsoda.BASE_URI;
private List<HttpCookie> cookies = null;
private String url;
private Region veil;
private ProgressIndicator p;
public CamsodaLoginDialog() {
Stage stage = new Stage();
stage.setTitle("CamSoda 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(1, 1, 1)");
p = new ProgressIndicator();
p.setMaxSize(140, 140);
p.setVisible(true);
veil.visibleProperty().bind(p.visibleProperty());
StackPane stackPane = new StackPane();
stackPane.getChildren().addAll(webView, veil, p);
stage.setScene(new Scene(stackPane, 400, 358));
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();
System.out.println(newV.toString());
});
webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> {
if (newState == State.SUCCEEDED) {
webEngine.executeScript("document.querySelector('a[ng-click=\"signin();\"]').click()");
p.setVisible(false);
// TODO make this work
// String username = Config.getInstance().getSettings().camsodaUsername;
// if (username != null && !username.trim().isEmpty()) {
// webEngine.executeScript("document.querySelector('input[name=\"loginUsername\"]').value = '" + username + "'");
// }
// String password = Config.getInstance().getSettings().camsodaPassword;
// if (password != null && !password.trim().isEmpty()) {
// webEngine.executeScript("document.querySelector('input[name=\"loginPassword\"]').value = '" + password + "'");
// }
} else if (newState == State.CANCELLED || newState == State.FAILED) {
p.setVisible(false);
}
});
webEngine.setUserStyleSheetLocation("data:text/css;base64," + Base64.getEncoder().encodeToString(CUSTOM_STYLE.getBytes()));
webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine"));
webEngine.load(URL);
return browser;
}
public List<HttpCookie> getCookies() {
return cookies;
}
public String getUrl() {
return url;
}
private static final String CUSTOM_STYLE = ""
+ ".ngdialog.ngdialog-theme-custom { padding: 0 !important }"
+ ".ngdialog-overlay { background: black !important; }";
}

View File

@ -0,0 +1,270 @@
package ctbrec.sites.camsoda;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
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 ctbrec.AbstractModel;
import ctbrec.recorder.download.StreamSource;
import ctbrec.sites.Site;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class CamsodaModel extends AbstractModel {
private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaModel.class);
private String streamUrl;
private Site site;
private List<StreamSource> streamSources = null;
private String status = "n/a";
private float sortOrder = 0;
private static Cache<String, int[]> streamResolutionCache = CacheBuilder.newBuilder()
.initialCapacity(10_000)
.maximumSize(10_000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build();
public String getStreamUrl() throws IOException {
if(streamUrl == null) {
// load model
loadModel();
}
return streamUrl;
}
private void loadModel() throws IOException {
String modelUrl = site.getBaseUrl() + "/api/v1/user/" + getName();
Request req = new Request.Builder().url(modelUrl).build();
Response response = site.getHttpClient().execute(req);
try {
JSONObject result = new JSONObject(response.body().string());
if(result.getBoolean("status")) {
JSONObject chat = result.getJSONObject("user").getJSONObject("chat");
status = chat.getString("status");
if(chat.has("edge_servers")) {
String edgeServer = chat.getJSONArray("edge_servers").getString(0);
String streamName = chat.getString("stream_name");
streamUrl = "https://" + edgeServer + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8";
}
} else {
throw new IOException("Result was not ok");
}
} finally {
response.close();
}
}
@Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
if(ignoreCache) {
loadModel();
}
return Objects.equals(status, "online");
}
@Override
public String getOnlineState(boolean failFast) throws IOException, ExecutionException {
if(failFast) {
return status;
} else {
if(status.equals("n/a")) {
loadModel();
}
return status;
}
}
public void setOnlineState(String state) {
this.status = state;
}
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
String streamUrl = getStreamUrl();
if(streamUrl == null) {
return Collections.emptyList();
}
Request req = new Request.Builder().url(streamUrl).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();
PlaylistData playlistData = master.getPlaylists().get(0);
StreamSource streamsource = new StreamSource();
streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", 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;
}
streamSources = Collections.singletonList(streamsource);
} finally {
response.close();
}
return streamSources;
}
@Override
public void invalidateCacheEntries() {
streamSources = null;
streamResolutionCache.invalidate(getName());
}
@Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
int[] resolution = streamResolutionCache.getIfPresent(getName());
if(resolution != null) {
return resolution;
} else {
if(failFast) {
return new int[] {0,0};
} else {
try {
List<StreamSource> streamSources = getStreamSources();
if(streamSources.isEmpty()) {
return new int[] {0,0};
} else {
StreamSource src = streamSources.get(0);
resolution = new int[] {src.width, src.height};
streamResolutionCache.put(getName(), resolution);
return resolution;
}
} catch (IOException | ParseException | PlaylistException e) {
throw new ExecutionException(e);
}
}
}
}
@Override
public void receiveTip(int tokens) throws IOException {
String csrfToken = ((CamsodaHttpClient)site.getHttpClient()).getCsrfToken();
String url = site.getBaseUrl() + "/api/v1/tip/" + getName();
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
LOG.debug("Sending tip {}", url);
RequestBody body = new FormBody.Builder()
.add("amount", Integer.toString(tokens))
.add("comment", "")
.build();
Request request = new Request.Builder()
.url(url)
.post(body)
.addHeader("Referer", Camsoda.BASE_URI + '/' + getName())
.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0")
.addHeader("Accept", "application/json, text/plain, */*")
.addHeader("Accept-Language", "en")
.addHeader("X-CSRF-Token", csrfToken)
.build();
try(Response response = site.getHttpClient().execute(request, true)) {
if(!response.isSuccessful()) {
throw new IOException("HTTP status " + response.code() + " " + response.message());
}
}
}
}
@Override
public boolean follow() throws IOException {
String url = Camsoda.BASE_URI + "/api/v1/follow/" + getName();
LOG.debug("Sending follow request {}", url);
String csrfToken = ((CamsodaHttpClient)site.getHttpClient()).getCsrfToken();
Request request = new Request.Builder()
.url(url)
.post(RequestBody.create(null, ""))
.addHeader("Referer", Camsoda.BASE_URI + '/' + getName())
.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0")
.addHeader("Accept", "application/json, text/plain, */*")
.addHeader("Accept-Language", "en")
.addHeader("X-CSRF-Token", csrfToken)
.build();
Response resp = site.getHttpClient().execute(request, true);
if (resp.isSuccessful()) {
resp.close();
return true;
} else {
resp.close();
throw new IOException("HTTP status " + resp.code() + " " + resp.message());
}
}
@Override
public boolean unfollow() throws IOException {
String url = Camsoda.BASE_URI + "/api/v1/unfollow/" + getName();
LOG.debug("Sending follow request {}", url);
String csrfToken = ((CamsodaHttpClient)site.getHttpClient()).getCsrfToken();
Request request = new Request.Builder()
.url(url)
.post(RequestBody.create(null, ""))
.addHeader("Referer", Camsoda.BASE_URI + '/' + getName())
.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0")
.addHeader("Accept", "application/json, text/plain, */*")
.addHeader("Accept-Language", "en")
.addHeader("X-CSRF-Token", csrfToken)
.build();
Response resp = site.getHttpClient().execute(request, true);
if (resp.isSuccessful()) {
resp.close();
return true;
} else {
resp.close();
throw new IOException("HTTP status " + resp.code() + " " + resp.message());
}
}
@Override
public void setSite(Site site) {
if(site instanceof Camsoda) {
this.site = site;
} else {
throw new IllegalArgumentException("Site has to be an instance of Camsoda");
}
}
@Override
public Site getSite() {
return site;
}
public void setStreamUrl(String streamUrl) {
this.streamUrl = streamUrl;
}
public float getSortOrder() {
return sortOrder;
}
public void setSortOrder(float sortOrder) {
this.sortOrder = sortOrder;
}
}

View File

@ -0,0 +1,286 @@
package ctbrec.sites.camsoda;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.DesktopIntergation;
import ctbrec.ui.TabSelectionListener;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TitledPane;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import okhttp3.Request;
import okhttp3.Response;
public class CamsodaShowsTab extends Tab implements TabSelectionListener {
private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaShowsTab.class);
private Camsoda camsoda;
private Recorder recorder;
private GridPane showList;
private ProgressIndicator progressIndicator;
public CamsodaShowsTab(Camsoda camsoda, Recorder recorder) {
this.camsoda = camsoda;
this.recorder = recorder;
createGui();
}
private void createGui() {
showList = new GridPane();
showList.setPadding(new Insets(5));
showList.setHgap(5);
showList.setVgap(5);
progressIndicator = new ProgressIndicator();
progressIndicator.setPrefSize(100, 100);
setContent(progressIndicator);
setClosable(false);
setText("Shows");
}
@Override
public void selected() {
Task<List<ShowBox>> task = new Task<List<ShowBox>>() {
@Override
protected List<ShowBox> call() throws Exception {
String url = camsoda.getBaseUrl() + "/api/v1/user/model_shows";
Request req = new Request.Builder().url(url).build();
Response response = camsoda.getHttpClient().execute(req);
if (response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
if (json.optInt("success") == 1) {
List<ShowBox> boxes = new ArrayList<>();
JSONArray results = json.getJSONArray("results");
for (int i = 0; i < results.length(); i++) {
JSONObject result = results.getJSONObject(i);
String modelUrl = camsoda.getBaseUrl() + result.getString("url");
String name = modelUrl.substring(modelUrl.lastIndexOf('/') + 1);
Model model = camsoda.createModel(name);
ZonedDateTime startTime = parseUtcTime(result.getString("start"));
ZonedDateTime endTime = parseUtcTime(result.getString("end"));
boxes.add(new ShowBox(model, startTime, endTime));
}
return boxes;
} else {
LOG.error("Couldn't load upcoming camsoda shows. Unexpected response: {}", json.toString());
showErrorDialog("Oh no!", "Couldn't load upcoming CamSoda shows", "Got an unexpected response from server");
}
} else {
response.close();
showErrorDialog("Oh no!", "Couldn't load upcoming CamSoda shows", "Got an unexpected response from server");
LOG.error("Couldn't load upcoming camsoda shows: {} {}", response.code(), response.message());
}
return Collections.emptyList();
}
private ZonedDateTime parseUtcTime(String string) {
DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
TemporalAccessor ta = formatter.parse(string.replace(" UTC", ""));
Instant instant = Instant.from(ta);
return ZonedDateTime.ofInstant(instant, ZoneId.systemDefault());
}
@Override
protected void done() {
super.done();
Platform.runLater(() -> {
try {
List<ShowBox> boxes = get();
showList.getChildren().clear();
int index = 0;
for (ShowBox showBox : boxes) {
showList.add(showBox, index % 2, index++ / 2);
GridPane.setMargin(showBox, new Insets(20, 20, 0, 20));
}
} catch (Exception e) {
LOG.error("Couldn't load upcoming camsoda shows", e);
}
setContent(new ScrollPane(showList));
});
}
};
new Thread(task).start();
}
@Override
public void deselected() {
}
private void showErrorDialog(String title, String head, String msg) {
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(head);
alert.setContentText(msg);
alert.showAndWait();
});
}
private class ShowBox extends TitledPane {
BorderPane root = new BorderPane();
int thumbSize = 200;
public ShowBox(Model model, ZonedDateTime startTime, ZonedDateTime endTime) {
setText(model.getName());
setPrefHeight(268);
setContent(root);
ImageView thumb = new ImageView();
thumb.setPreserveRatio(true);
thumb.setFitHeight(thumbSize);
loadImage(model, thumb);
root.setLeft(new ProgressIndicator());
BorderPane.setMargin(thumb, new Insets(10, 30, 10, 10));
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
GridPane grid = new GridPane();
grid.add(createLabel("Start", true), 0, 0);
grid.add(createLabel(formatter.format(startTime), false), 1, 0);
grid.add(createLabel("End", true), 0, 1);
grid.add(createLabel(formatter.format(endTime), false), 1, 1);
Button record = new Button("Record Model");
record.setTooltip(new Tooltip(record.getText()));
record.setOnAction((evt) -> record(model));
grid.add(record, 1, 2);
GridPane.setMargin(record, new Insets(10));
Button follow = new Button("Follow");
follow.setTooltip(new Tooltip(follow.getText()));
follow.setOnAction((evt) -> follow(model));
grid.add(follow, 1, 3);
GridPane.setMargin(follow, new Insets(10));
Button openInBrowser = new Button("Open in Browser");
openInBrowser.setTooltip(new Tooltip(openInBrowser.getText()));
openInBrowser.setOnAction((evt) -> DesktopIntergation.open(model.getUrl()));
grid.add(openInBrowser, 1, 4);
GridPane.setMargin(openInBrowser, new Insets(10));
root.setCenter(grid);
loadImage(model, thumb);
record.prefWidthProperty().bind(openInBrowser.widthProperty());
follow.prefWidthProperty().bind(openInBrowser.widthProperty());
}
private void follow(Model model) {
setCursor(Cursor.WAIT);
new Thread(() -> {
try {
model.follow();
} catch (Exception e) {
LOG.error("Couldn't follow model {}", model, e);
showErrorDialog("Oh no!", "Couldn't follow model", e.getMessage());
} finally {
Platform.runLater(() -> {
setCursor(Cursor.DEFAULT);
});
}
}).start();
}
private void record(Model model) {
setCursor(Cursor.WAIT);
new Thread(() -> {
try {
recorder.startRecording(model);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
showErrorDialog("Oh no!", "Couldn't add model to the recorder", "Recorder error: " + e.getMessage());
} finally {
Platform.runLater(() -> {
setCursor(Cursor.DEFAULT);
});
}
}).start();
}
private void loadImage(Model model, ImageView thumb) {
new Thread(() -> {
try {
String url = camsoda.getBaseUrl() + "/api/v1/user/" + model.getName();
Request detailRequest = new Request.Builder().url(url).build();
Response resp = camsoda.getHttpClient().execute(detailRequest);
if (resp.isSuccessful()) {
JSONObject json = new JSONObject(resp.body().string());
if (json.optBoolean("status") && json.has("user")) {
JSONObject user = json.getJSONObject("user");
if (user.has("settings")) {
JSONObject settings = user.getJSONObject("settings");
String imageUrl;
if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
imageUrl = getClass().getResource("/image_not_found.png").toString();
} else {
if (settings.has("offline_picture")) {
imageUrl = settings.getString("offline_picture");
} else {
imageUrl = "https:" + user.getString("thumb");
}
}
Platform.runLater(() -> {
Image img = new Image(imageUrl, 1000, thumbSize, true, true, true);
img.progressProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
if (newValue.doubleValue() == 1.0) {
thumb.setImage(img);
root.setLeft(thumb);
}
}
});
});
}
}
}
resp.close();
} catch (Exception e) {
LOG.error("Couldn't load model details", e);
}
}).start();
}
private Node createLabel(String string, boolean bold) {
Label label = new Label(string);
label.setPadding(new Insets(10));
Font def = Font.getDefault();
label.setFont(Font.font(def.getFamily(), bold ? FontWeight.BOLD : FontWeight.NORMAL, 16));
return label;
}
}
}

View File

@ -0,0 +1,43 @@
package ctbrec.sites.camsoda;
import static ctbrec.sites.camsoda.Camsoda.*;
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 CamsodaTabProvider extends TabProvider {
private Camsoda camsoda;
private Recorder recorder;
public CamsodaTabProvider(Camsoda camsoda, Recorder recorder) {
this.camsoda = camsoda;
this.recorder = recorder;
}
@Override
public List<Tab> getTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>();
tabs.add(createTab("Online", BASE_URI + "/api/v1/browse/online"));
CamsodaFollowedTab followedTab = new CamsodaFollowedTab("Followed", camsoda);
followedTab.setRecorder(recorder);
followedTab.setScene(scene);
tabs.add(followedTab);
tabs.add(new CamsodaShowsTab(camsoda, recorder));
return tabs;
}
private Tab createTab(String title, String url) {
CamsodaUpdateService updateService = new CamsodaUpdateService(url, false, camsoda);
ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, camsoda);
tab.setRecorder(recorder);
return tab;
}
}

View File

@ -0,0 +1,124 @@
package ctbrec.sites.camsoda;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jetty.util.StringUtil;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Model;
import ctbrec.ui.PaginatedScheduledService;
import javafx.concurrent.Task;
import okhttp3.Request;
import okhttp3.Response;
public class CamsodaUpdateService extends PaginatedScheduledService {
private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaUpdateService.class);
private String url;
private boolean loginRequired;
private Camsoda camsoda;
int modelsPerPage = 50;
public CamsodaUpdateService(String url, boolean loginRequired, Camsoda camsoda) {
this.url = url;
this.loginRequired = loginRequired;
this.camsoda = camsoda;
}
@Override
protected Task<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException {
List<CamsodaModel> models = new ArrayList<>();
if(loginRequired && StringUtil.isBlank(ctbrec.Config.getInstance().getSettings().username)) {
return Collections.emptyList();
} else {
String url = CamsodaUpdateService.this.url;
LOG.debug("Fetching page {}", url);
Request request = new Request.Builder().url(url).build();
Response response = camsoda.getHttpClient().execute(request, loginRequired);
if (response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
if(json.has("status") && json.getBoolean("status")) {
JSONArray results = json.getJSONArray("results");
for (int i = 0; i < results.length(); i++) {
JSONObject result = results.getJSONObject(i);
if(result.has("tpl")) {
JSONArray tpl = result.getJSONArray("tpl");
String name = tpl.getString(0);
// int connections = tpl.getInt(2);
String streamName = tpl.getString(5);
String tsize = tpl.getString(6);
String serverPrefix = tpl.getString(7);
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
model.setDescription(tpl.getString(4));
model.setSortOrder(tpl.getFloat(3));
long unixtime = System.currentTimeMillis() / 1000;
String preview = "https://thumbs-orig.camsoda.com/thumbs/"
+ streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime;
model.setPreview(preview);
if(result.has("edge_servers")) {
JSONArray edgeServers = result.getJSONArray("edge_servers");
model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8");
}
models.add(model);
} else {
//LOG.debug("{}", result.toString(2));
String name = result.getString("username");
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
if(result.has("server_prefix")) {
String serverPrefix = result.getString("server_prefix");
String streamName = result.getString("stream_name");
model.setSortOrder(result.getFloat("sort_value"));
models.add(model);
if(result.has("status")) {
model.setOnlineState(result.getString("status"));
}
if(result.has("edge_servers")) {
JSONArray edgeServers = result.getJSONArray("edge_servers");
model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8");
}
if(result.has("tsize")) {
long unixtime = System.currentTimeMillis() / 1000;
String tsize = result.getString("tsize");
String preview = "https://thumbs-orig.camsoda.com/thumbs/"
+ streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime;
model.setPreview(preview);
}
}
}
}
return models.stream()
.sorted((m1,m2) -> (int)(m2.getSortOrder() - m1.getSortOrder()))
.skip( (page-1) * modelsPerPage)
.limit(modelsPerPage)
.collect(Collectors.toList());
} else {
response.close();
return Collections.emptyList();
}
} else {
int code = response.code();
response.close();
throw new IOException("HTTP status " + code);
}
}
}
};
}
}

View File

@ -38,11 +38,6 @@ public class ChaturbateModel extends AbstractModel {
this.site = site;
}
@Override
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
return isOnline(false);
}
@Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
StreamInfo info;

View File

@ -55,7 +55,7 @@ public class MyFreeCams extends AbstractSite {
@Override
public String getAffiliateLink() {
return "";
return BASE_URI + "/?baf=8127165";
}
@Override
@ -93,7 +93,7 @@ public class MyFreeCams extends AbstractSite {
@Override
public String getBuyTokensLink() {
return "https://www.myfreecams.com/php/purchase.php?request=tokens";
return BASE_URI + "/php/purchase.php?request=tokens";
}
@Override
@ -149,7 +149,7 @@ public class MyFreeCams extends AbstractSite {
layout.add(password, 1, 1);
Button createAccount = new Button("Create new Account");
createAccount.setOnAction((e) -> DesktopIntergation.open(BASE_URI + "/php/signup.php?request=register"));
createAccount.setOnAction((e) -> DesktopIntergation.open(getAffiliateLink()));
layout.add(createAccount, 1, 2);
GridPane.setColumnSpan(createAccount, 2);
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));

View File

@ -57,6 +57,7 @@ public class MyFreeCamsClient {
private String ctxenc;
private String chatToken;
private int sessionId;
private long heartBeat;
private EvictingQueue<String> receivedTextHistory = EvictingQueue.create(10000);
@ -135,6 +136,7 @@ public class MyFreeCamsClient {
// TODO find out, what the values in the json message mean, at the moment we hust send 0s, which seems to work, too
// webSocket.send("1 0 0 81 0 %7B%22err%22%3A0%2C%22start%22%3A1540159843072%2C%22stop%22%3A1540159844121%2C%22a%22%3A6392%2C%22time%22%3A1540159844%2C%22key%22%3A%228da80f985c9db390809713dac71df297%22%2C%22cid%22%3A%22c504d684%22%2C%22pid%22%3A1%2C%22site%22%3A%22www%22%7D\n");
webSocket.send("1 0 0 81 0 %7B%22err%22%3A0%2C%22start%22%3A0%2C%22stop%22%3A0%2C%22a%22%3A0%2C%22time%22%3A0%2C%22key%22%3A%22%22%2C%22cid%22%3A%22%22%2C%22pid%22%3A1%2C%22site%22%3A%22www%22%7D\n");
heartBeat = System.currentTimeMillis();
startKeepAlive(webSocket);
} catch (IOException e) {
e.printStackTrace();
@ -165,6 +167,7 @@ public class MyFreeCamsClient {
@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
heartBeat = System.currentTimeMillis();
receivedTextHistory.add(text);
msgBuffer.append(text);
Message message;
@ -469,6 +472,14 @@ public class MyFreeCamsClient {
LOG.trace("--> NULL to keep the connection alive");
try {
ws.send("0 0 0 0 0 -\n");
long millisSinceLastMessage = System.currentTimeMillis() - heartBeat;
if(millisSinceLastMessage > TimeUnit.MINUTES.toMillis(2)) {
LOG.info("No message since 2 mins. Restarting websocket");
ws.close(1000, "");
MyFreeCamsClient.this.ws = null;
}
Thread.sleep(TimeUnit.SECONDS.toMillis(15));
} catch (Exception e) {
e.printStackTrace();
@ -484,7 +495,7 @@ public class MyFreeCamsClient {
lock.lock();
try {
for (SessionState state : sessionStates.values()) {
if(Objects.equals(state.getNm(), model.getName())) {
if(Objects.equals(state.getNm(), model.getName()) || Objects.equals(model.getUid(), state.getUid())) {
model.update(state, getStreamUrl(state));
return;
}

View File

@ -23,6 +23,8 @@ 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.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel;
import ctbrec.recorder.download.StreamSource;
@ -37,7 +39,7 @@ public class MyFreeCamsModel extends AbstractModel {
private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsModel.class);
private int uid;
private int uid = -1; // undefined
private String hlsUrl;
private double camScore;
private int viewerCount;
@ -207,7 +209,17 @@ public class MyFreeCamsModel extends AbstractModel {
this.state = state;
}
@Override
public void setName(String name) {
if(getName() != null && name != null && !getName().equals(name)) {
LOG.debug("Model name changed {} -> {}", getName(), name);
}
super.setName(name);
}
public void update(SessionState state, String streamUrl) {
uid = Integer.parseInt(state.getUid().toString());
setName(state.getNm());
setCamScore(state.getM().getCamscore());
setState(State.of(state.getVs()));
setStreamUrl(streamUrl);
@ -308,4 +320,15 @@ public class MyFreeCamsModel extends AbstractModel {
public Site getSite() {
return site;
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
reader.nextName();
uid = reader.nextInt();
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
writer.name("uid").value(uid);
}
}

View File

@ -28,6 +28,7 @@ import ctbrec.recorder.Recorder;
import ctbrec.recorder.RemoteRecorder;
import ctbrec.sites.Site;
import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.mfc.MyFreeCams;
import javafx.application.Application;
@ -61,6 +62,7 @@ public class CamrecApplication extends Application {
public void start(Stage primaryStage) throws Exception {
sites.add(new Chaturbate());
sites.add(new MyFreeCams());
sites.add(new Camsoda());
sites.add(new Cam4());
loadConfig();
createHttpClient();
@ -72,9 +74,6 @@ public class CamrecApplication extends Application {
try {
site.setRecorder(recorder);
site.init();
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
site.login();
}
} catch(Exception e) {
LOG.error("Error while initializing site {}", site.getName(), e);
}

View File

@ -2,11 +2,12 @@ package ctbrec.ui;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel;
import ctbrec.Model;
@ -20,14 +21,13 @@ import javafx.beans.property.SimpleBooleanProperty;
*/
public class JavaFxModel extends AbstractModel {
private transient BooleanProperty onlineProperty = new SimpleBooleanProperty();
private Model delegate;
public JavaFxModel(Model delegate) {
this.delegate = delegate;
try {
onlineProperty.set(Objects.equals("public", delegate.getOnlineState(true)));
} catch (IOException | ExecutionException e) {}
onlineProperty.set(delegate.isOnline());
} catch (IOException | ExecutionException | InterruptedException e) {}
}
@Override
@ -147,4 +147,14 @@ public class JavaFxModel extends AbstractModel {
public Site getSite() {
return delegate.getSite();
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
delegate.readSiteSpecificData(reader);
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
delegate.writeSiteSpecificData(writer);
}
}

View File

@ -34,7 +34,7 @@ import ctbrec.recorder.Recorder;
import ctbrec.recorder.download.MergedHlsDownload;
import ctbrec.sites.Site;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
@ -47,6 +47,7 @@ import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
@ -57,6 +58,7 @@ import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.stage.FileChooser;
import javafx.util.Callback;
import javafx.util.Duration;
public class RecordingsTab extends Tab implements TabSelectionListener {
@ -99,12 +101,28 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
TableColumn<JavaFxRecording, String> name = new TableColumn<>("Model");
name.setPrefWidth(200);
name.setCellValueFactory(new PropertyValueFactory<JavaFxRecording, String>("modelName"));
TableColumn<JavaFxRecording, String> date = new TableColumn<>("Date");
TableColumn<JavaFxRecording, Instant> date = new TableColumn<>("Date");
date.setCellValueFactory((cdf) -> {
Instant instant = cdf.getValue().getStartDate();
ZonedDateTime time = instant.atZone(ZoneId.systemDefault());
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
return new SimpleStringProperty(dtf.format(time));
return new SimpleObjectProperty<Instant>(instant);
});
date.setCellFactory(new Callback<TableColumn<JavaFxRecording, Instant>, TableCell<JavaFxRecording, Instant>>() {
@Override
public TableCell<JavaFxRecording, Instant> call(TableColumn<JavaFxRecording, Instant> param) {
TableCell<JavaFxRecording, Instant> cell = new TableCell<JavaFxRecording, Instant>() {
@Override
protected void updateItem(Instant instant, boolean empty) {
if(empty || instant == null) {
setText(null);
} else {
ZonedDateTime time = instant.atZone(ZoneId.systemDefault());
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
setText(dtf.format(time));
}
}
};
return cell;
}
});
date.setPrefWidth(200);
TableColumn<JavaFxRecording, String> status = new TableColumn<>("Status");

View File

@ -20,6 +20,7 @@ import javafx.beans.value.ObservableValue;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Accordion;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
@ -64,9 +65,11 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private RadioButton recordRemote;
private ToggleGroup recordLocation;
private ProxySettingsPane proxySettingsPane;
private ComboBox<Integer> maxResolution;
private ComboBox<SplitAfterOption> splitAfter;
private List<Site> sites;
private Label restartLabel;
private Accordion credentialsAccordion = new Accordion();
public SettingsTab(List<Site> sites) {
this.sites = sites;
@ -113,14 +116,16 @@ public class SettingsTab extends Tab implements TabSelectionListener {
//right side
rightSide.getChildren().add(createSiteSelectionPanel());
for (Site site : sites) {
rightSide.getChildren().add(credentialsAccordion);
for (int i = 0; i < sites.size(); i++) {
Site site = sites.get(i);
Node siteConfig = site.getConfigurationGui();
if(siteConfig != null) {
TitledPane pane = new TitledPane(site.getName(), siteConfig);
pane.setCollapsible(false);
rightSide.getChildren().add(pane);
credentialsAccordion.getPanes().add(pane);
}
}
credentialsAccordion.setExpandedPane(credentialsAccordion.getPanes().get(0));
}
private Node createSiteSelectionPanel() {
@ -227,8 +232,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
keyDialog.show();
}
});
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, CHECKBOX_MARGIN, 0, 0));
GridPane.setMargin(secureCommunication, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
GridPane.setMargin(l, new Insets(4, CHECKBOX_MARGIN, 0, 0));
GridPane.setMargin(secureCommunication, new Insets(4, 0, 0, 0));
layout.add(secureCommunication, 1, 3);
TitledPane recordLocation = new TitledPane("Record Location", layout);
@ -245,6 +250,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
GridPane.setFillWidth(recordingsDirectory, true);
GridPane.setHgrow(recordingsDirectory, Priority.ALWAYS);
GridPane.setColumnSpan(recordingsDirectory, 2);
GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(recordingsDirectory, 1, 0);
recordingsDirectoryButton = createRecordingsBrowseButton();
layout.add(recordingsDirectoryButton, 3, 0);
@ -255,19 +261,10 @@ public class SettingsTab extends Tab implements TabSelectionListener {
GridPane.setFillWidth(mediaPlayer, true);
GridPane.setHgrow(mediaPlayer, Priority.ALWAYS);
GridPane.setColumnSpan(mediaPlayer, 2);
GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(mediaPlayer, 1, 1);
layout.add(createMpvBrowseButton(), 3, 1);
Label l = new Label("Allow multiple players");
layout.add(l, 0, 2);
multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer);
multiplePlayers.setOnAction((e) -> Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected());
GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(multiplePlayers, new Insets(3, 0, 0, CHECKBOX_MARGIN));
layout.add(multiplePlayers, 1, 2);
TitledPane locations = new TitledPane("Locations", layout);
locations.setCollapsible(false);
return locations;
@ -275,8 +272,9 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private Node createGeneralPanel() {
GridPane layout = createGridLayout();
int row = 0;
Label l = new Label("Display stream resolution in overview");
layout.add(l, 0, 0);
layout.add(l, 0, row);
loadResolution = new CheckBox();
loadResolution.setSelected(Config.getInstance().getSettings().determineResolution);
loadResolution.setOnAction((e) -> {
@ -287,18 +285,41 @@ public class SettingsTab extends Tab implements TabSelectionListener {
});
//GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
GridPane.setMargin(loadResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(loadResolution, 1, 0);
layout.add(loadResolution, 1, row++);
l = new Label("Allow multiple players");
layout.add(l, 0, row);
multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer);
multiplePlayers.setOnAction((e) -> Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected());
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
layout.add(multiplePlayers, 1, row++);
l = new Label("Manually select stream quality");
layout.add(l, 0, 1);
layout.add(l, 0, row);
chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality);
chooseStreamQuality.setOnAction((e) -> Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected());
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
layout.add(chooseStreamQuality, 1, 1);
layout.add(chooseStreamQuality, 1, row++);
l = new Label("Maximum resolution (0 = unlimited)");
layout.add(l, 0, row);
List<Integer> resolutionOptions = new ArrayList<>();
resolutionOptions.add(1080);
resolutionOptions.add(720);
resolutionOptions.add(600);
resolutionOptions.add(480);
resolutionOptions.add(0);
maxResolution = new ComboBox<>(new ObservableListWrapper<>(resolutionOptions));
setMaxResolutionValue();
maxResolution.setOnAction((e) -> Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem());
layout.add(maxResolution, 1, row++);
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
GridPane.setMargin(maxResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
l = new Label("Split recordings after (minutes)");
layout.add(l, 0, 2);
layout.add(l, 0, row);
List<SplitAfterOption> options = new ArrayList<>();
options.add(new SplitAfterOption("disabled", 0));
options.add(new SplitAfterOption("10 min", 10 * 60));
@ -307,11 +328,12 @@ public class SettingsTab extends Tab implements TabSelectionListener {
options.add(new SplitAfterOption("30 min", 30 * 60));
options.add(new SplitAfterOption("60 min", 60 * 60));
splitAfter = new ComboBox<>(new ObservableListWrapper<>(options));
layout.add(splitAfter, 1, 2);
layout.add(splitAfter, 1, row++);
setSplitAfterValue();
splitAfter.setOnAction((e) -> Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue());
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
GridPane.setMargin(splitAfter, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN));
maxResolution.prefWidthProperty().bind(splitAfter.widthProperty());
TitledPane general = new TitledPane("General", layout);
general.setCollapsible(false);
@ -327,6 +349,15 @@ public class SettingsTab extends Tab implements TabSelectionListener {
}
}
private void setMaxResolutionValue() {
int value = Config.getInstance().getSettings().maximumResolution;
for (Integer option : maxResolution.getItems()) {
if(option == value) {
maxResolution.getSelectionModel().select(option);
}
}
}
void showRestartRequired() {
restartLabel.setVisible(true);
}
@ -346,6 +377,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
recordingsDirectory.setDisable(!local);
recordingsDirectoryButton.setDisable(!local);
splitAfter.setDisable(!local);
maxResolution.setDisable(!local);
}
private ChangeListener<? super Boolean> createRecordingsDirectoryFocusListener() {

View File

@ -1,5 +1,6 @@
package ctbrec.ui;
import java.io.EOFException;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
@ -207,10 +208,16 @@ public class ThumbCell extends StackPane {
LOG.trace("Removing invalid resolution value for {}", model.getName());
model.invalidateCacheEntries();
}
Thread.sleep(500);
} catch (ExecutionException | IOException | InterruptedException e1) {
} catch (IOException | InterruptedException e1) {
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1);
} catch(ExecutionException e) {
if(e.getCause() instanceof EOFException) {
LOG.warn("Couldn't update resolution tag for model {}. Playlist empty", model.getName());
} else {
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e);
}
} finally {
ThumbOverviewTab.resolutionProcessing.remove(model);
}
@ -485,13 +492,13 @@ public class ThumbCell extends StackPane {
nameBackground.setWidth(w);
nameBackground.setHeight(20);
topicBackground.setWidth(w);
topicBackground.setHeight(h-nameBackground.getHeight());
topic.prefHeight(h-25);
topic.maxHeight(h-25);
topicBackground.setHeight(getHeight()-nameBackground.getHeight());
topic.prefHeight(getHeight()-25);
topic.maxHeight(getHeight()-25);
int margin = 4;
topic.maxWidth(w-margin*2);
topic.setWrappingWidth(w-margin*2);
selectionOverlay.setWidth(w);
selectionOverlay.setHeight(h);
selectionOverlay.setHeight(getHeight());
}
}

View File

@ -13,6 +13,7 @@ import ctbrec.sites.Site;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
public class TokenLabel extends Label {
@ -26,10 +27,10 @@ public class TokenLabel extends Label {
CamrecApplication.bus.register(new Object() {
@Subscribe
public void tokensUpdates(Map<String, Object> e) {
if(Objects.equals("tokens", e.get("event"))) {
if (Objects.equals("tokens", e.get("event"))) {
tokens = (int) e.get("amount");
updateText();
} else if(Objects.equals("tokens.sent", e.get("event"))) {
} else if (Objects.equals("tokens.sent", e.get("event"))) {
int _tokens = (int) e.get("amount");
tokens -= _tokens;
updateText();
@ -70,7 +71,10 @@ public class TokenLabel extends Label {
update(tokens);
} catch (InterruptedException | ExecutionException e) {
LOG.error("Couldn't retrieve account balance", e);
Platform.runLater(() -> setText("Tokens: error"));
Platform.runLater(() -> {
setText("Tokens: error");
setTooltip(new Tooltip(e.getMessage()));
});
}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -41,7 +41,7 @@
<logger name="ctbrec.recorder.Chaturbate" level="INFO" />
<logger name="ctbrec.recorder.server.HlsServlet" level="INFO"/>
<logger name="ctbrec.recorder.server.RecorderServlet" level="INFO"/>
<logger name="ctbrec.ui.CookieJarImpl" level="INFO"/>
<logger name="ctbrec.io.CookieJarImpl" level="INFO"/>
<logger name="ctbrec.ui.ThumbOverviewTab" level="DEBUG"/>
<logger name="org.eclipse.jetty" level="INFO" />
<logger name="streamer" level="ERROR" />