Merge branch 'dev'

This commit is contained in:
0xboobface 2018-11-09 18:28:50 +01:00
commit 2eab3b3bd4
66 changed files with 2164 additions and 241 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<classpathentry including="**/*.java" kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
@ -11,7 +11,7 @@
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/oracle-jdk-bin-1.8.0.181"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>

View File

@ -1,3 +1,15 @@
1.8.0
========================
* Added BongaCams
* Added possibility to suspend the recording for a model. The model stays in
the list of recorded models, but the actual recording is suspended
* HTTP sessions are restored on startup. This should reduce the number of
logins needed (especially for Cam4, BongaCams and CamSoda).
* Server can run now run on OpenJRE
* Added JVM parameter to define the configuration directory
(``-Dctbrec.config.dir``)
* Improved memory management for MyFreeCams
1.7.0
========================
* Added CamSoda

View File

@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ctbrec</groupId>
<artifactId>ctbrec</artifactId>
<version>1.7.0</version>
<version>1.8.0</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@ -16,6 +16,10 @@
<outputDirectory>ctbrec</outputDirectory>
<filtered>true</filtered>
</file>
<file>
<source>${project.basedir}/src/main/resources/pp.sh</source>
<outputDirectory>ctbrec</outputDirectory>
</file>
<file>
<source>${project.build.directory}/${name.final}.jar</source>
<outputDirectory>ctbrec</outputDirectory>

View File

@ -16,6 +16,10 @@
<outputDirectory>ctbrec</outputDirectory>
<filtered>true</filtered>
</file>
<file>
<source>${project.basedir}/src/main/resources/pp.sh</source>
<outputDirectory>ctbrec</outputDirectory>
</file>
<file>
<source>${project.build.directory}/${name.final}.jar</source>
<outputDirectory>ctbrec</outputDirectory>

View File

@ -15,6 +15,14 @@
<outputDirectory>ctbrec</outputDirectory>
<filtered>true</filtered>
</file>
<file>
<source>${project.basedir}/src/main/resources/pp.bat</source>
<outputDirectory>ctbrec</outputDirectory>
</file>
<file>
<source>${project.basedir}/src/main/resources/pp.ps1</source>
<outputDirectory>ctbrec</outputDirectory>
</file>
<file>
<source>${project.build.directory}/${name.final}.jar</source>
<outputDirectory>ctbrec</outputDirectory>

View File

@ -15,6 +15,14 @@
<outputDirectory>ctbrec</outputDirectory>
<filtered>true</filtered>
</file>
<file>
<source>${project.basedir}/src/main/resources/pp.bat</source>
<outputDirectory>ctbrec</outputDirectory>
</file>
<file>
<source>${project.basedir}/src/main/resources/pp.ps1</source>
<outputDirectory>ctbrec</outputDirectory>
</file>
<file>
<source>${project.build.directory}/${name.final}.jar</source>
<outputDirectory>ctbrec</outputDirectory>

View File

@ -15,6 +15,14 @@
<outputDirectory>ctbrec</outputDirectory>
<filtered>true</filtered>
</file>
<file>
<source>${project.basedir}/src/main/resources/pp.bat</source>
<outputDirectory>ctbrec</outputDirectory>
</file>
<file>
<source>${project.basedir}/src/main/resources/pp.ps1</source>
<outputDirectory>ctbrec</outputDirectory>
</file>
<file>
<source>${project.build.directory}/${name.final}.jar</source>
<outputDirectory>ctbrec</outputDirectory>

View File

@ -16,6 +16,7 @@ public abstract class AbstractModel implements Model {
private String description;
private List<String> tags = new ArrayList<>();
private int streamUrlIndex = -1;
private boolean suspended = false;
@Override
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
@ -92,6 +93,16 @@ public abstract class AbstractModel implements Model {
// noop default implementation, can be overriden by concrete models
}
@Override
public boolean isSuspended() {
return suspended;
}
@Override
public void setSuspended(boolean suspended) {
this.suspended = suspended;
}
@Override
public int hashCode() {
final int prime = 31;

View File

@ -29,9 +29,16 @@ public class Config {
private Settings settings;
private String filename;
private List<Site> sites;
private File configDir;
private Config(List<Site> sites) throws FileNotFoundException, IOException {
this.sites = sites;
if(System.getProperty("ctbrec.config.dir") != null) {
configDir = new File(System.getProperty("ctbrec.config.dir"));
} else {
configDir = OS.getConfigDir();
}
if(System.getProperty("ctbrec.config") != null) {
filename = System.getProperty("ctbrec.config");
} else {
@ -45,7 +52,6 @@ public class Config {
.add(Model.class, new ModelJsonAdapter(sites))
.build();
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class);
File configDir = OS.getConfigDir();
File configFile = new File(configDir, filename);
LOG.debug("Loading config from {}", configFile.getAbsolutePath());
if(configFile.exists()) {
@ -86,7 +92,6 @@ public class Config {
.build();
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).indent(" ");
String json = adapter.toJson(settings);
File configDir = OS.getConfigDir();
File configFile = new File(configDir, filename);
LOG.debug("Saving config to {}", configFile.getAbsolutePath());
Files.createDirectories(configDir.toPath());
@ -96,4 +101,8 @@ public class Config {
public boolean isServerMode() {
return Objects.equals(System.getProperty("ctbrec.server.mode"), "1");
}
public File getConfigDir() {
return configDir;
}
}

View File

@ -38,4 +38,7 @@ public interface Model {
public Site getSite();
public void writeSiteSpecificData(JsonWriter writer) throws IOException;
public void readSiteSpecificData(JsonReader reader) throws IOException;
public boolean isSuspended();
public void setSuspended(boolean suspended);
}

View File

@ -17,11 +17,15 @@ public class Settings {
public boolean localRecording = true;
public int httpPort = 8080;
public int httpTimeout = 10000;
public String httpUserAgent = "Mozilla/5.0 Gecko/20100101 Firefox/62.0";
public String httpServer = "localhost";
public String recordingsDir = System.getProperty("user.home") + File.separator + "ctbrec";
public String mediaPlayer = "/usr/bin/mpv";
public String postProcessing = "";
public String username = ""; // chaturbate username TODO maybe rename this onetime
public String password = ""; // chaturbate password TODO maybe rename this onetime
public String bongaUsername = "";
public String bongaPassword = "";
public String mfcUsername = "";
public String mfcPassword = "";
public String camsodaUsername = "";

View File

@ -0,0 +1,64 @@
package ctbrec.io;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonReader.Token;
import com.squareup.moshi.JsonWriter;
import ctbrec.io.HttpClient.CookieContainer;
import okhttp3.Cookie;
public class CookieContainerJsonAdapter extends JsonAdapter<CookieContainer> {
private CookieJsonAdapter cookieAdapter = new CookieJsonAdapter();
@Override
public CookieContainer fromJson(JsonReader reader) throws IOException {
CookieContainer cookies = new CookieContainer();
reader.beginArray();
while(reader.hasNext()) {
reader.beginObject();
reader.nextName(); // "domain"
String domain = reader.nextString();
reader.nextName(); // "cookies"
reader.beginArray();
List<Cookie> cookieList = new ArrayList<>();
while(reader.hasNext()) {
Token token = reader.peek();
if(token == Token.END_ARRAY) {
break;
}
Cookie cookie = cookieAdapter.fromJson(reader);
cookieList.add(cookie);
}
reader.endArray();
reader.endObject();
cookies.put(domain, cookieList);
}
reader.endArray();
return cookies;
}
@Override
public void toJson(JsonWriter writer, CookieContainer cookieContainer) throws IOException {
writer.beginArray();
for (Entry<String, List<Cookie>> entry : cookieContainer.entrySet()) {
writer.beginObject();
writer.name("domain").value(entry.getKey());
writer.name("cookies");
writer.beginArray();
for (Cookie cookie : entry.getValue()) {
cookieAdapter.toJson(writer, cookie);
}
writer.endArray();
writer.endObject();
}
writer.endArray();
}
}

View File

@ -4,6 +4,8 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
@ -78,5 +80,16 @@ public class CookieJarImpl implements CookieJar {
return host;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Entry<String, List<Cookie>> entry : cookieStore.entrySet()) {
sb.append(entry.getKey()).append(": ").append(entry.getValue()).append('\n');
}
return sb.toString();
}
public Map<String, List<Cookie>> getCookies() {
return cookieStore;
}
}

View File

@ -0,0 +1,81 @@
package ctbrec.io;
import java.io.IOException;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import okhttp3.Cookie;
import okhttp3.Cookie.Builder;
public class CookieJsonAdapter extends JsonAdapter<Cookie> {
@Override
public Cookie fromJson(JsonReader reader) throws IOException {
reader.beginObject();
Builder builder = new Cookie.Builder();
// domain
reader.nextName();
String domain = reader.nextString();
builder.domain(domain);
// expiresAt
reader.nextName();
builder.expiresAt(reader.nextLong());
// host only
reader.nextName();
if(reader.nextBoolean()) {
builder.hostOnlyDomain(domain);
}
// http only
reader.nextName();
if(reader.nextBoolean()) {
builder.httpOnly();
}
// name
reader.nextName();
builder.name(reader.nextString());
// path
reader.nextName();
builder.path(reader.nextString());
// persistent
reader.nextName();
if(reader.nextBoolean()) {
// noop
}
// secure
reader.nextName();
if(reader.nextBoolean()) {
builder.secure();
}
// value
reader.nextName();
builder.value(reader.nextString());
reader.endObject();
return builder.build();
}
@Override
public void toJson(JsonWriter writer, Cookie cookie) throws IOException {
writer.beginObject();
writer.name("domain").value(cookie.domain());
writer.name("expiresAt").value(cookie.expiresAt());
writer.name("hostOnly").value(cookie.hostOnly());
writer.name("httpOnly").value(cookie.httpOnly());
writer.name("name").value(cookie.name());
writer.name("path").value(cookie.path());
writer.name("persistent").value(cookie.persistent());
writer.name("secure").value(cookie.secure());
writer.name("value").value(cookie.value());
writer.endObject();
}
}

View File

@ -1,13 +1,28 @@
package ctbrec.io;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.Settings.ProxyType;
import okhttp3.ConnectionPool;
import okhttp3.Cookie;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;
import okhttp3.OkHttpClient.Builder;
@ -16,12 +31,16 @@ import okhttp3.Response;
import okhttp3.Route;
public abstract class HttpClient {
private static final transient Logger LOG = LoggerFactory.getLogger(HttpClient.class);
protected OkHttpClient client;
protected CookieJarImpl cookieJar = new CookieJarImpl();
protected boolean loggedIn = false;
protected int loginTries = 0;
private String name;
protected HttpClient() {
protected HttpClient(String name) {
this.name = name;
reconfigure();
}
@ -92,6 +111,7 @@ public abstract class HttpClient {
public void reconfigure() {
loadProxySettings();
loadCookies();
Builder builder = new OkHttpClient.Builder()
.cookieJar(cookieJar)
.connectTimeout(Config.getInstance().getSettings().httpTimeout, TimeUnit.MILLISECONDS)
@ -112,10 +132,61 @@ public abstract class HttpClient {
}
public void shutdown() {
persistCookies();
client.connectionPool().evictAll();
client.dispatcher().executorService().shutdown();
}
private void persistCookies() {
try {
CookieContainer cookies = new CookieContainer();
cookies.putAll(cookieJar.getCookies());
Moshi moshi = new Moshi.Builder()
.add(CookieContainer.class, new CookieContainerJsonAdapter())
.build();
JsonAdapter<CookieContainer> adapter = moshi.adapter(CookieContainer.class).indent(" ");
String json = adapter.toJson(cookies);
File cookieFile = new File(Config.getInstance().getConfigDir(), "cookies-" + name + ".json");
try(FileOutputStream fout = new FileOutputStream(cookieFile)) {
fout.write(json.getBytes("utf-8"));
}
} catch (Exception e) {
LOG.error("Couldn't persist cookies for {}", name, e);
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private void loadCookies() {
try {
File cookieFile = new File(Config.getInstance().getConfigDir(), "cookies-" + name + ".json");
if(!cookieFile.exists()) {
return;
}
byte[] jsonBytes = Files.readAllBytes(cookieFile.toPath());
String json = new String(jsonBytes, "utf-8");
Map<String, List<Cookie>> cookies = cookieJar.getCookies();
Moshi moshi = new Moshi.Builder()
.add(CookieContainer.class, new CookieContainerJsonAdapter())
.build();
JsonAdapter<CookieContainer> adapter = moshi.adapter(CookieContainer.class).indent(" ");
CookieContainer fromJson = adapter.fromJson(json);
Set entries = fromJson.entrySet();
for (Object _entry : entries) {
Entry entry = (Entry) _entry;
cookies.put((String)entry.getKey(), (List<Cookie>)entry.getValue());
}
} catch (Exception e) {
LOG.error("Couldn't load cookies for {}", name, e);
}
}
public static class CookieContainer extends HashMap<String, List<Cookie>> {
}
private okhttp3.Authenticator createHttpProxyAuthenticator(String username, String password) {
return new okhttp3.Authenticator() {
@Override

View File

@ -32,6 +32,7 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
String url = null;
String type = null;
int streamUrlIndex = -1;
boolean suspended = false;
Model model = null;
while(reader.hasNext()) {
@ -55,6 +56,9 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
} else if(key.equals("streamUrlIndex")) {
streamUrlIndex = reader.nextInt();
model.setStreamUrlIndex(streamUrlIndex);
} else if(key.equals("suspended")) {
suspended = reader.nextBoolean();
model.setSuspended(suspended);
} else if(key.equals("siteSpecific")) {
reader.beginObject();
model.readSiteSpecificData(reader);
@ -87,6 +91,7 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
writeValueIfSet(writer, "description", model.getDescription());
writeValueIfSet(writer, "url", model.getUrl());
writer.name("streamUrlIndex").value(model.getStreamUrlIndex());
writer.name("suspended").value(model.isSuspended());
writer.name("siteSpecific");
writer.beginObject();
model.writeSiteSpecificData(writer);

View File

@ -10,6 +10,7 @@ import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
@ -28,7 +29,9 @@ import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.OS;
import ctbrec.Recording;
import ctbrec.io.StreamRedirectThread;
import ctbrec.recorder.PlaylistGenerator.InvalidPlaylistException;
import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.HlsDownload;
@ -46,7 +49,7 @@ public class LocalRecorder implements Recorder {
private Config config;
private ProcessMonitor processMonitor;
private OnlineMonitor onlineMonitor;
private PlaylistGeneratorTrigger playlistGenTrigger;
private PostProcessingTrigger postProcessingTrigger;
private volatile boolean recording = true;
private List<File> deleteInProgress = Collections.synchronizedList(new ArrayList<>());
private RecorderHttpClient client = new RecorderHttpClient();
@ -68,9 +71,9 @@ public class LocalRecorder implements Recorder {
onlineMonitor = new OnlineMonitor();
onlineMonitor.start();
playlistGenTrigger = new PlaylistGeneratorTrigger();
postProcessingTrigger = new PostProcessingTrigger();
if(Config.getInstance().isServerMode()) {
playlistGenTrigger.start();
postProcessingTrigger.start();
}
LOG.debug("Recorder initialized");
@ -112,7 +115,12 @@ public class LocalRecorder implements Recorder {
}
private void startRecordingProcess(Model model) throws IOException {
LOG.debug("Restart recording for model {}", model.getName());
if(model.isSuspended()) {
LOG.info("Recording for model {} is suspended.", model);
return;
}
LOG.debug("Starting recording for model {}", model.getName());
if (recordingProcesses.containsKey(model)) {
LOG.error("A recording for model {} is already running", model);
return;
@ -148,10 +156,51 @@ public class LocalRecorder implements Recorder {
}.start();
}
private void stopRecordingProcess(Model model) throws IOException {
private void stopRecordingProcess(Model model) {
Download download = recordingProcesses.get(model);
download.stop();
recordingProcesses.remove(model);
if(!Config.getInstance().isServerMode()) {
postprocess(download);
}
}
private void postprocess(Download download) {
if(!(download instanceof MergedHlsDownload)) {
throw new IllegalArgumentException("Download should be of type MergedHlsDownload");
}
String postProcessing = Config.getInstance().getSettings().postProcessing;
if (postProcessing != null && !postProcessing.isEmpty()) {
new Thread(() -> {
Runtime rt = Runtime.getRuntime();
try {
MergedHlsDownload d = (MergedHlsDownload) download;
String[] args = new String[] {
postProcessing,
d.getDirectory().getAbsolutePath(),
d.getTargetFile().getAbsolutePath(),
d.getModel().getName(),
d.getModel().getSite().getName(),
Long.toString(download.getStartTime().getEpochSecond())
};
LOG.debug("Running {}", Arrays.toString(args));
Process process = rt.exec(args, OS.getEnvironment(), download.getDirectory());
Thread std = new Thread(new StreamRedirectThread(process.getInputStream(), System.out));
std.setName("Process stdout pipe");
std.setDaemon(true);
std.start();
Thread err = new Thread(new StreamRedirectThread(process.getErrorStream(), System.err));
err.setName("Process stderr pipe");
err.setDaemon(true);
err.start();
process.waitFor();
LOG.debug("Process finished.");
} catch (Exception e) {
LOG.error("Error in process thread", e);
}
}).start();
}
}
@Override
@ -164,6 +213,22 @@ public class LocalRecorder implements Recorder {
}
}
@Override
public boolean isSuspended(Model model) {
lock.lock();
try {
int index = models.indexOf(model);
if(index >= 0) {
Model m = models.get(index);
return m.isSuspended();
} else {
return false;
}
} finally {
lock.unlock();
}
}
@Override
public List<Model> getModelsRecording() {
lock.lock();
@ -181,7 +246,7 @@ public class LocalRecorder implements Recorder {
LOG.debug("Stopping monitor threads");
onlineMonitor.running = false;
processMonitor.running = false;
playlistGenTrigger.running = false;
postProcessingTrigger.running = false;
LOG.debug("Stopping all recording processes");
stopRecordingProcesses();
client.shutdown();
@ -246,10 +311,14 @@ public class LocalRecorder implements Recorder {
LOG.debug("Recording terminated for model {}", m.getName());
iterator.remove();
restart.add(m);
try {
finishRecording(d.getDirectory());
} catch(Exception e) {
LOG.error("Error while finishing recording for model {}", m.getName(), e);
if(config.isServerMode()) {
try {
finishRecording(d.getDirectory());
} catch(Exception e) {
LOG.error("Error while finishing recording for model {}", m.getName(), e);
}
} else {
postprocess(d);
}
}
}
@ -269,17 +338,17 @@ public class LocalRecorder implements Recorder {
}
private void finishRecording(File directory) {
Thread t = new Thread() {
@Override
public void run() {
if(Config.getInstance().isServerMode()) {
if(Config.getInstance().isServerMode()) {
Thread t = new Thread() {
@Override
public void run() {
generatePlaylist(directory);
}
}
};
t.setDaemon(true);
t.setName("Postprocessing" + directory.toString());
t.start();
};
t.setDaemon(true);
t.setName("Post-Processing " + directory.toString());
t.start();
}
}
private void generatePlaylist(File recDir) {
@ -315,7 +384,7 @@ public class LocalRecorder implements Recorder {
while (running) {
for (Model model : getModelsRecording()) {
try {
if (!recordingProcesses.containsKey(model)) {
if (!model.isSuspended() && !recordingProcesses.containsKey(model)) {
boolean isOnline = model.isOnline(IGNORE_CACHE);
LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline"));
if (isOnline) {
@ -339,11 +408,11 @@ public class LocalRecorder implements Recorder {
}
}
private class PlaylistGeneratorTrigger extends Thread {
private class PostProcessingTrigger extends Thread {
private volatile boolean running = false;
public PlaylistGeneratorTrigger() {
setName("PlaylistGeneratorTrigger");
public PostProcessingTrigger() {
setName("PostProcessingTrigger");
setDaemon(true);
}
@ -365,7 +434,7 @@ public class LocalRecorder implements Recorder {
}
if (!recordingProcessFound) {
if (deleteInProgress.contains(recDir)) {
LOG.debug("{} is being deleted. Not going to generate a playlist", recDir);
LOG.debug("{} is being deleted. Not going to start post-processing", recDir);
} else {
finishRecording(recDir);
}
@ -529,4 +598,45 @@ public class LocalRecorder implements Recorder {
stopRecordingProcess(model);
tryRestartRecording(model);
}
@Override
public void suspendRecording(Model model) {
lock.lock();
try {
if (models.contains(model)) {
int index = models.indexOf(model);
models.get(index).setSuspended(true);
model.setSuspended(true);
} else {
LOG.warn("Couldn't suspend model {}. Not found in list", model.getName());
return;
}
} finally {
lock.unlock();
}
Download download = recordingProcesses.get(model);
if(download != null) {
stopRecordingProcess(model);
}
}
@Override
public void resumeRecording(Model model) throws IOException {
lock.lock();
try {
if (models.contains(model)) {
int index = models.indexOf(model);
Model m = models.get(index);
m.setSuspended(false);
startRecordingProcess(m);
model.setSuspended(false);
} else {
LOG.warn("Couldn't resume model {}. Not found in list", model.getName());
return;
}
} finally {
lock.unlock();
}
}
}

View File

@ -28,4 +28,9 @@ public interface Recorder {
public void delete(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException;
public void shutdown();
public void suspendRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException;
public void resumeRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException;
public boolean isSuspended(Model model);
}

View File

@ -88,7 +88,7 @@ public class RemoteRecorder implements Recorder {
if("start".equals(action)) {
models.add(model);
} else {
} else if("stop".equals(action)) {
models.remove(model);
}
} else {
@ -109,6 +109,17 @@ public class RemoteRecorder implements Recorder {
return models != null && models.contains(model);
}
@Override
public boolean isSuspended(Model model) {
int index = models.indexOf(model);
if(index >= 0) {
Model m = models.get(index);
return m.isSuspended();
} else {
return false;
}
}
@Override
public List<Model> getModelsRecording() {
if(lastSync.isBefore(Instant.now().minusSeconds(60))) {
@ -276,4 +287,28 @@ public class RemoteRecorder implements Recorder {
public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
sendRequest("switch", model);
}
@Override
public void suspendRecording(Model model) throws InvalidKeyException, NoSuchAlgorithmException, IllegalStateException, IOException {
sendRequest("suspend", model);
model.setSuspended(true);
// update cached model
int index = models.indexOf(model);
if(index >= 0) {
Model m = models.get(index);
m.setSuspended(true);
}
}
@Override
public void resumeRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException, IllegalStateException {
sendRequest("resume", model);
model.setSuspended(false);
// update cached model
int index = models.indexOf(model);
if(index >= 0) {
Model m = models.get(index);
m.setSuspended(false);
}
}
}

View File

@ -5,6 +5,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Path;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@ -41,6 +42,8 @@ public abstract class AbstractHlsDownload implements Download {
volatile boolean running = false;
volatile boolean alive = true;
Path downloadDir;
Instant startTime;
Model model;
public AbstractHlsDownload(HttpClient client) {
this.client = client;
@ -117,6 +120,16 @@ public abstract class AbstractHlsDownload implements Download {
return downloadDir.toFile();
}
@Override
public Instant getStartTime() {
return startTime;
}
@Override
public Model getModel() {
return model;
}
public static class SegmentPlaylist {
public int seq = 0;
public float totalDuration = 0;

View File

@ -2,6 +2,7 @@ package ctbrec.recorder.download;
import java.io.File;
import java.io.IOException;
import java.time.Instant;
import ctbrec.Config;
import ctbrec.Model;
@ -11,4 +12,6 @@ public interface Download {
public void stop();
public boolean isAlive();
public File getDirectory();
public Model getModel();
public Instant getStartTime();
}

View File

@ -12,6 +12,7 @@ import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.util.Date;
import java.util.concurrent.Callable;
@ -39,6 +40,8 @@ public class HlsDownload extends AbstractHlsDownload {
public void start(Model model, Config config) throws IOException {
try {
running = true;
startTime = Instant.now();
super.model = model;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
String startTime = sdf.format(new Date());
Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getName());

View File

@ -16,6 +16,7 @@ import java.nio.file.Path;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.LinkedList;
@ -58,9 +59,14 @@ public class MergedHlsDownload extends AbstractHlsDownload {
super(client);
}
public File getTargetFile() {
return targetFile;
}
public void start(String segmentPlaylistUri, File targetFile, ProgressListener progressListener) throws IOException {
try {
running = true;
super.startTime = Instant.now();
downloadDir = targetFile.getParentFile().toPath();
mergeThread = createMergeThread(targetFile, progressListener, false);
mergeThread.start();
@ -75,7 +81,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
} finally {
alive = false;
streamer.stop();
LOG.debug("Download for terminated");
LOG.debug("Download terminated for {}", segmentPlaylistUri);
}
}
@ -84,6 +90,8 @@ public class MergedHlsDownload extends AbstractHlsDownload {
this.config = config;
try {
running = true;
super.startTime = Instant.now();
super.model = model;
startTime = ZonedDateTime.now();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
String startTime = sdf.format(new Date());

View File

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

View File

@ -6,6 +6,10 @@ import ctbrec.io.HttpClient;
public class RecorderHttpClient extends HttpClient {
public RecorderHttpClient() {
super("recorder");
}
@Override
public boolean login() throws IOException {
return false;

View File

@ -112,6 +112,18 @@ public class RecorderServlet extends AbstractCtbrecServlet {
response = "{\"status\": \"success\", \"msg\": \"Resolution switched\"}";
resp.getWriter().write(response);
break;
case "suspend":
LOG.debug("Suspend recording for model {} - {}", request.model.getName(), request.model.getUrl());
recorder.suspendRecording(request.model);
response = "{\"status\": \"success\", \"msg\": \"Recording suspended\"}";
resp.getWriter().write(response);
break;
case "resume":
LOG.debug("Resume recording for model {} - {}", request.model.getName(), request.model.getUrl());
recorder.resumeRecording(request.model);
response = "{\"status\": \"success\", \"msg\": \"Recording resumed\"}";
resp.getWriter().write(response);
break;
default:
resp.setStatus(SC_BAD_REQUEST);
response = "{\"status\": \"error\", \"msg\": \"Unknown action\"}";

View File

@ -0,0 +1,7 @@
package ctbrec.sites;
import javafx.scene.Parent;
public interface ConfigUI {
public Parent createConfigPanel();
}

View File

@ -6,7 +6,6 @@ import ctbrec.Model;
import ctbrec.io.HttpClient;
import ctbrec.recorder.Recorder;
import ctbrec.ui.TabProvider;
import javafx.scene.Node;
public interface Site {
public String getName();
@ -24,7 +23,7 @@ public interface Site {
public boolean supportsTips();
public boolean supportsFollow();
public boolean isSiteForModel(Model m);
public Node getConfigurationGui();
public ConfigUI getConfigurationGui();
public boolean credentialsAvailable();
public void setEnabled(boolean enabled);
public boolean isEnabled();

View File

@ -0,0 +1,150 @@
package ctbrec.sites.bonga;
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.sites.ConfigUI;
import ctbrec.ui.TabProvider;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class BongaCams extends AbstractSite {
public static final String BASE_URL = "https://bongacams.com";
private BongaCamsHttpClient httpClient;
private Recorder recorder;
@Override
public String getName() {
return "BongaCams";
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
public String getAffiliateLink() {
return "http://bongacams2.com/track?c=610249";
}
@Override
public void setRecorder(Recorder recorder) {
this.recorder = recorder;
}
@Override
public TabProvider getTabProvider() {
return new BongaCamsTabProvider(recorder, this);
}
@Override
public Model createModel(String name) {
BongaCamsModel model = new BongaCamsModel();
model.setName(name);
model.setUrl(BASE_URL + '/' + name);
model.setDescription("");
model.setSite(this);
return model;
}
@Override
public Integer getTokenBalance() throws IOException {
int userId = ((BongaCamsHttpClient)getHttpClient()).getUserId();
String url = BongaCams.BASE_URL + "/tools/amf.php";
RequestBody body = new FormBody.Builder()
.add("method", "ping")
.add("args[]", Integer.toString(userId))
.build();
Request request = new Request.Builder()
.url(url)
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.addHeader("Accept", "application/json, text/javascript, */*")
.addHeader("Accept-Language", "en")
.addHeader("Referer", BongaCams.BASE_URL)
.addHeader("X-Requested-With", "XMLHttpRequest")
.post(body)
.build();
try(Response response = getHttpClient().execute(request, true)) {
if(response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
if(json.optString("status").equals("online")) {
JSONObject userData = json.getJSONObject("userData");
return userData.getInt("balance");
} else {
throw new IOException("Request was not successful: " + json.toString(2));
}
} else {
throw new IOException(response.code() + " " + response.message());
}
}
}
@Override
public String getBuyTokensLink() {
return getAffiliateLink();
}
@Override
public void login() throws IOException {
getHttpClient().login();
}
@Override
public HttpClient getHttpClient() {
if(httpClient == null) {
httpClient = new BongaCamsHttpClient();
}
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() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isSiteForModel(Model m) {
return m instanceof BongaCamsModel;
}
@Override
public ConfigUI getConfigurationGui() {
return new BongaCamsConfigUI(this);
}
@Override
public boolean credentialsAvailable() {
String username = Config.getInstance().getSettings().bongaUsername;
return username != null && !username.trim().isEmpty();
}
}

View File

@ -0,0 +1,54 @@
package ctbrec.sites.bonga;
import ctbrec.Config;
import ctbrec.sites.ConfigUI;
import ctbrec.ui.DesktopIntergation;
import ctbrec.ui.SettingsTab;
import javafx.geometry.Insets;
import javafx.scene.Parent;
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 BongaCamsConfigUI implements ConfigUI {
private BongaCams bongaCams;
public BongaCamsConfigUI(BongaCams bongaCams) {
this.bongaCams = bongaCams;
}
@Override
public Parent createConfigPanel() {
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("BongaCams User"), 0, 0);
TextField username = new TextField(Config.getInstance().getSettings().bongaUsername);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().bongaUsername = username.getText());
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(new Label("BongaCams Password"), 0, 1);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().bongaPassword);
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().bongaPassword = 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(bongaCams.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,243 @@
package ctbrec.sites.bonga;
import java.io.IOException;
import java.net.HttpCookie;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import javafx.application.Platform;
import okhttp3.Cookie;
import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class BongaCamsHttpClient extends HttpClient {
private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsHttpClient.class);
private int userId = 0;
public BongaCamsHttpClient() {
super("bongacams");
addSortByPopularCookie();
}
/**
* Adds a cookie, which defines the sort order for returned model lists
*/
private void addSortByPopularCookie() {
Cookie sortByCookie = new Cookie.Builder()
.domain("bongacams.com")
.name("bcmlsf9")
.value("%7B%22limit%22%3A20%2C%22c_limit%22%3A10%2C%22th_type%22%3A%22live%22%2C%22sorting%22%3A%22popular%22%2C%22display%22%3A%22auto%22%7D")
.build();
Map<String, List<Cookie>> cookies = cookieJar.getCookies();
for (Entry<String, List<Cookie>> entry : cookies.entrySet()) {
List<Cookie> cookieList = entry.getValue();
for (Iterator<Cookie> iterator = cookieList.iterator(); iterator.hasNext();) {
Cookie cookie = iterator.next();
if(cookie.name().equals("bcmlsf9")) {
iterator.remove();
}
}
entry.getValue().add(sortByCookie);
}
}
@Override
public synchronized boolean login() throws IOException {
if(loggedIn) {
return true;
}
boolean cookiesWorked = checkLoginSuccess();
if(cookiesWorked) {
loggedIn = true;
LOG.debug("Logged in with cookies");
return true;
}
BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>();
Runnable showDialog = () -> {
// login with javafx WebView
BongaCamsLoginDialog loginDialog = new BongaCamsLoginDialog();
// 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();
if(loggedIn) {
LOG.info("Logged in. User ID is {}", userId);
} else {
LOG.info("Login failed");
}
return loggedIn;
}
/**
* Check, if the login worked by requesting roomdata and looking
* @throws IOException
*/
private boolean checkLoginSuccess() throws IOException {
String modelName = getAnyModelName();
// we request the roomData of a random model, because it contains
// user data, if the user is logged in, which we can use to verify, that the login worked
String url = BongaCams.BASE_URL + "/tools/amf.php";
RequestBody body = new FormBody.Builder()
.add("method", "getRoomData")
.add("args[]", modelName)
.add("args[]", "false")
//.add("method", "ping") // TODO alternative request, but
//.add("args[]", <userId>) // where to get the userId
.build();
Request request = new Request.Builder()
.url(url)
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.addHeader("Accept", "application/json, text/javascript, */*")
.addHeader("Accept-Language", "en")
.addHeader("Referer", BongaCams.BASE_URL)
.addHeader("X-Requested-With", "XMLHttpRequest")
.post(body)
.build();
try(Response response = execute(request)) {
if(response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
if(json.optString("status").equals("success")) {
JSONObject userData = json.getJSONObject("userData");
userId = userData.optInt("userId");
return userId > 0;
} else {
throw new IOException("Request was not successful: " + json.toString(2));
}
} else {
throw new IOException(response.code() + " " + response.message());
}
}
}
/**
* Fetches the list of online models and returns the name of the first model
*/
private String getAnyModelName() throws IOException {
Request request = new Request.Builder()
.url(BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=female&online_only=true&is_mobile=true&offset=0")
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.addHeader("Accept", "application/json, text/javascript, */*")
.addHeader("Accept-Language", "en")
.addHeader("Referer", BongaCams.BASE_URL)
.addHeader("X-Requested-With", "XMLHttpRequest")
.build();
try(Response response = execute(request)) {
if (response.isSuccessful()) {
String content = response.body().string();
JSONObject json = new JSONObject(content);
if(json.optString("status").equals("success")) {
JSONArray _models = json.getJSONArray("models");
JSONObject m = _models.getJSONObject(0);
String name = m.getString("username");
return name;
} else {
throw new IOException("Request was not successful: " + content);
}
} else {
throw new IOException(response.code() + ' ' + response.message());
}
}
}
private void transferCookies(BongaCamsLoginDialog 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(BongaCamsLoginDialog.URL);
cookies = new ArrayList<>();
for (HttpCookie webViewCookie : loginDialog.getCookies()) {
Cookie cookie = Cookie.parse(origUrl, webViewCookie.toString());
cookies.add(cookie);
}
cookieJar.saveFromResponse(origUrl, cookies);
}
// @Override
// public boolean login() throws IOException {
// String url = BongaCams.BASE_URL + "/login";
// String dateTime = new SimpleDateFormat("d.MM.yyyy', 'HH:mm:ss").format(new Date());
// RequestBody body = new FormBody.Builder()
// .add("security_log_additional_info","{\"language\":\"en\",\"cookieEnabled\":true,\"javaEnabled\":false,\"flashVersion\":\"31.0.0\",\"dateTime\":\""+dateTime+"\",\"ips\":[\"192.168.0.1\"]}")
// .add("log_in[username]", Config.getInstance().getSettings().bongaUsername)
// .add("log_in[password]", Config.getInstance().getSettings().bongaPassword)
// .add("log_in[remember]", "1")
// .add("log_in[bfpt]", "")
// .add("header_form", "1")
// .build();
// Request request = new Request.Builder()
// .url(url)
// .post(body)
// .addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
// .addHeader("Accept","application/json")
// .addHeader("Accept-Language", "en")
// .addHeader("Referer", BongaCams.BASE_URL)
// .addHeader("X-Requested-With", "XMLHttpRequest")
// .build();
// try(Response response = execute(request)) {
// if(response.isSuccessful()) {
// JSONObject json = new JSONObject(response.body().string());
// if(json.optString("status").equals("success")) {
// return true;
// } else {
// LOG.debug("Login response: {}", json.toString(2));
// Platform.runLater(() -> new BongaCamsLoginDialog());
// throw new IOException("Login not successful");
// }
// } else {
// throw new IOException(response.code() + " " + response.message());
// }
// }
// }
public int getUserId() throws IOException {
if(userId == 0) {
login();
}
return userId;
}
}

View File

@ -0,0 +1,119 @@
package ctbrec.sites.bonga;
import java.io.File;
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 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;
public class BongaCamsLoginDialog {
private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsLoginDialog.class);
public static final String URL = BongaCams.BASE_URL + "/login";
private List<HttpCookie> cookies = null;
private String url;
private Region veil;
private ProgressIndicator p;
public BongaCamsLoginDialog() {
Stage stage = new Stage();
stage.setTitle("BongaCams 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, 640, 480));
stage.showAndWait();
cookies = cookieManager.getCookieStore().getCookies();
}
private WebView createWebView(Stage stage) {
WebView browser = new WebView();
WebEngine webEngine = browser.getEngine();
webEngine.setJavaScriptEnabled(true);
webEngine.setUserAgent(Config.getInstance().getSettings().httpUserAgent);
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) {
veil.setVisible(false);
p.setVisible(false);
//System.out.println("############# " + webEngine.getLocation());
//System.out.println(webEngine.getDocument().getDocumentElement().getTextContent());
try {
String username = Config.getInstance().getSettings().bongaUsername;
if (username != null && !username.trim().isEmpty()) {
webEngine.executeScript("$('input[name=\"log_in[username]\"]').attr('value','" + username + "')");
}
String password = Config.getInstance().getSettings().bongaPassword;
if (password != null && !password.trim().isEmpty()) {
webEngine.executeScript("$('input[name=\"log_in[password]\"]').attr('value','" + password + "')");
}
webEngine.executeScript("$('div[class~=\"fancybox-overlay\"]').css('display','none')");
webEngine.executeScript("$('div#header').css('display','none')");
webEngine.executeScript("$('div.footer').css('display','none')");
webEngine.executeScript("$('div.footer_copy').css('display','none')");
webEngine.executeScript("$('div[class~=\"banner_top_index\"]').css('display','none')");
webEngine.executeScript("$('td.menu_container').css('display','none')");
} catch(Exception e) {
LOG.warn("Couldn't auto fill username and password for BongaCams", 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;
}
public List<HttpCookie> getCookies() {
// for (HttpCookie httpCookie : cookies) {
// LOG.debug("Cookie: {}", httpCookie);
// }
return cookies;
}
public String getUrl() {
return url;
}
}

View File

@ -0,0 +1,223 @@
package ctbrec.sites.bonga;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.iheartradio.m3u8.data.StreamInfo;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.recorder.download.StreamSource;
import ctbrec.sites.Site;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class BongaCamsModel extends AbstractModel {
private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsModel.class);
private BongaCams site;
private int userId;
private String onlineState = "n/a";
private boolean online = false;
private List<StreamSource> streamSources = new ArrayList<>();
private int[] resolution;
@Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
return online;
}
public void setOnline(boolean online) {
this.online = online;
}
@Override
public String getOnlineState(boolean failFast) throws IOException, ExecutionException {
return onlineState;
}
public void setOnlineState(String onlineState) {
this.onlineState = onlineState;
}
@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();
for (PlaylistData playlistData : master.getPlaylists()) {
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.add(streamsource);
}
} finally {
response.close();
}
return streamSources;
}
private String getStreamUrl() throws IOException {
String url = BongaCams.BASE_URL + "/tools/amf.php";
RequestBody body = new FormBody.Builder()
.add("method", "getRoomData")
.add("args[]", getName())
.add("args[]", "false")
.build();
Request request = new Request.Builder()
.url(url)
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.addHeader("Accept", "application/json, text/javascript, */*")
.addHeader("Accept-Language", "en")
.addHeader("Referer", BongaCams.BASE_URL)
.addHeader("X-Requested-With", "XMLHttpRequest")
.post(body)
.build();
try(Response response = site.getHttpClient().execute(request)) {
if(response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
if(json.optString("status").equals("success")) {
JSONObject localData = json.getJSONObject("localData");
String server = localData.getString("videoServerUrl");
return "https:" + server + "/hls/stream_" + getName() + "/playlist.m3u8";
} else {
throw new IOException("Request was not successful: " + json.toString(2));
}
} else {
throw new IOException(response.code() + " " + response.message());
}
}
}
@Override
public void invalidateCacheEntries() {
resolution = null;
}
@Override
public void receiveTip(int tokens) throws IOException {
String url = BongaCams.BASE_URL + "/chat-ajax-amf-service?" + System.currentTimeMillis();
int userId = ((BongaCamsHttpClient)site.getHttpClient()).getUserId();
RequestBody body = new FormBody.Builder()
.add("method", "tipModel")
.add("args[]", getName())
.add("args[]", Integer.toString(tokens))
.add("args[]", Integer.toString(userId))
.add("args[3]", "")
.build();
Request request = new Request.Builder()
.url(url)
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.addHeader("Accept", "application/json, text/javascript, */*")
.addHeader("Accept-Language", "en")
.addHeader("Referer", BongaCams.BASE_URL + '/' + getName())
.addHeader("X-Requested-With", "XMLHttpRequest")
.post(body)
.build();
try(Response response = site.getHttpClient().execute(request, true)) {
if(response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
if(!json.optString("status").equals("success")) {
LOG.error("Sending tip failed {}", json.toString(2));
throw new IOException("Sending tip failed");
}
} else {
throw new IOException(response.code() + ' ' + response.message());
}
}
}
@Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
if(resolution == null) {
if(failFast) {
return new int[2];
}
try {
if(!isOnline()) {
return new int[2];
}
List<StreamSource> streamSources = getStreamSources();
Collections.sort(streamSources);
StreamSource best = streamSources.get(streamSources.size()-1);
resolution = new int[] {best.width, best.height};
} catch (ExecutionException | IOException | ParseException | PlaylistException | InterruptedException e) {
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
}
return resolution;
} else {
return resolution;
}
}
@Override
public boolean follow() throws IOException {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean unfollow() throws IOException {
// TODO Auto-generated method stub
return false;
}
@Override
public void setSite(Site site) {
if(site instanceof BongaCams) {
this.site = (BongaCams) site;
} else {
throw new IllegalArgumentException("Site has to be an instance of BongaCams");
}
}
@Override
public Site getSite() {
return site;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
}

View File

@ -0,0 +1,66 @@
package ctbrec.sites.bonga;
import java.util.ArrayList;
import java.util.List;
import ctbrec.recorder.Recorder;
import ctbrec.ui.PaginatedScheduledService;
import ctbrec.ui.TabProvider;
import ctbrec.ui.ThumbOverviewTab;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
public class BongaCamsTabProvider extends TabProvider {
private BongaCams bongaCams;
private Recorder recorder;
public BongaCamsTabProvider(Recorder recorder, BongaCams bongaCams) {
this.recorder = recorder;
this.bongaCams = bongaCams;
}
@Override
public List<Tab> getTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>();
// female
String url = BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=female&online_only=true&is_mobile=true&offset=";
BongaCamsUpdateService updateService = new BongaCamsUpdateService(bongaCams, url);
tabs.add(createTab("Female", updateService));
// male
url = BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=male&online_only=true&is_mobile=true&offset=";
updateService = new BongaCamsUpdateService(bongaCams, url);
tabs.add(createTab("Male", updateService));
// couples
url = BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=couples&online_only=true&is_mobile=true&offset=";
updateService = new BongaCamsUpdateService(bongaCams, url);
tabs.add(createTab("Couples", updateService));
// trans
url = BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=transsexual&online_only=true&is_mobile=true&offset=";
updateService = new BongaCamsUpdateService(bongaCams, url);
tabs.add(createTab("Transsexual", updateService));
// new
url = BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=new-models&online_only=true&is_mobile=true&offset=";
updateService = new BongaCamsUpdateService(bongaCams, url);
tabs.add(createTab("New", updateService));
// friends
url = BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=friends&online_only=true&offset=";
updateService = new BongaCamsUpdateService(bongaCams, url);
tabs.add(createTab("Friends", updateService));
return tabs;
}
private Tab createTab(String title, PaginatedScheduledService updateService) {
ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, bongaCams);
tab.setRecorder(recorder);
return tab;
}
}

View File

@ -0,0 +1,83 @@
package ctbrec.sites.bonga;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.ui.PaginatedScheduledService;
import javafx.concurrent.Task;
import okhttp3.Request;
import okhttp3.Response;
public class BongaCamsUpdateService extends PaginatedScheduledService {
private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsUpdateService.class);
private BongaCams bongaCams;
private String url;
public BongaCamsUpdateService(BongaCams bongaCams, String url) {
this.bongaCams = bongaCams;
this.url = url;
}
@Override
protected Task<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException {
String _url = url + ((page-1) * 36);
LOG.debug("Fetching page {}", _url);
Request request = new Request.Builder()
.url(_url)
.addHeader("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.addHeader("Accept", "application/json, text/javascript, */*")
.addHeader("Accept-Language", "en")
.addHeader("Referer", bongaCams.getBaseUrl())
.addHeader("X-Requested-With", "XMLHttpRequest")
.build();
Response response = bongaCams.getHttpClient().execute(request);
if (response.isSuccessful()) {
String content = response.body().string();
List<Model> models = new ArrayList<>();
JSONObject json = new JSONObject(content);
if(json.optString("status").equals("success")) {
JSONArray _models = json.getJSONArray("models");
for (int i = 0; i < _models.length(); i++) {
JSONObject m = _models.getJSONObject(i);
String name = m.getString("username");
BongaCamsModel model = (BongaCamsModel) bongaCams.createModel(name);
model.setUserId(m.getInt("user_id"));
boolean away = m.optBoolean("is_away");
boolean online = m.optBoolean("online") && !away;
model.setOnline(online);
if(online) {
if(away) {
model.setOnlineState("away");
} else {
model.setOnlineState(m.getString("room"));
}
} else {
model.setOnlineState("offline");
}
model.setPreview("https:" + m.getString("thumb_image"));
models.add(model);
}
}
return models;
} else {
int code = response.code();
response.close();
throw new IOException("HTTP status " + code);
}
}
};
}
}

View File

@ -9,17 +9,8 @@ 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.sites.ConfigUI;
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 {
@ -124,32 +115,7 @@ public class Cam4 extends AbstractSite {
}
@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;
public ConfigUI getConfigurationGui() {
return new Cam4ConfigUI();
}
}

View File

@ -0,0 +1,48 @@
package ctbrec.sites.cam4;
import ctbrec.Config;
import ctbrec.sites.ConfigUI;
import ctbrec.ui.DesktopIntergation;
import ctbrec.ui.SettingsTab;
import javafx.geometry.Insets;
import javafx.scene.Parent;
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 Cam4ConfigUI implements ConfigUI {
@Override
public Parent createConfigPanel() {
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

@ -23,12 +23,23 @@ public class Cam4HttpClient extends HttpClient {
private static final transient Logger LOG = LoggerFactory.getLogger(Cam4HttpClient.class);
public Cam4HttpClient() {
super("cam4");
}
@Override
public synchronized boolean login() throws IOException {
if(loggedIn) {
return true;
}
boolean cookiesWorked = checkLoginSuccess();
if(cookiesWorked) {
loggedIn = true;
LOG.debug("Logged in with cookies");
return true;
}
BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>();
Runnable showDialog = () -> {

View File

@ -60,6 +60,7 @@ public class Cam4LoginDialog {
WebView browser = new WebView();
WebEngine webEngine = browser.getEngine();
webEngine.setJavaScriptEnabled(true);
webEngine.setUserAgent(Config.getInstance().getSettings().httpUserAgent);
webEngine.locationProperty().addListener((obs, oldV, newV) -> {
try {
URL _url = new URL(newV);

View File

@ -9,17 +9,8 @@ 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.sites.ConfigUI;
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;
@ -44,6 +35,11 @@ public class Camsoda extends AbstractSite {
return BASE_URI;
}
@Override
public String getBuyTokensLink() {
return BASE_URI;
}
@Override
public void setRecorder(Recorder recorder) {
this.recorder = recorder;
@ -87,11 +83,6 @@ public class Camsoda extends AbstractSite {
throw new RuntimeException("Tokens not found in response");
}
@Override
public String getBuyTokensLink() {
return getBaseUrl();
}
@Override
public void login() throws IOException {
if(credentialsAvailable()) {
@ -140,32 +131,7 @@ public class Camsoda extends AbstractSite {
}
@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;
public ConfigUI getConfigurationGui() {
return new CamsodaConfigUI(this);
}
}

View File

@ -0,0 +1,54 @@
package ctbrec.sites.camsoda;
import ctbrec.Config;
import ctbrec.sites.ConfigUI;
import ctbrec.ui.DesktopIntergation;
import ctbrec.ui.SettingsTab;
import javafx.geometry.Insets;
import javafx.scene.Parent;
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 CamsodaConfigUI implements ConfigUI {
private Camsoda camsoda;
public CamsodaConfigUI(Camsoda camsoda) {
this.camsoda = camsoda;
}
@Override
public Parent createConfigPanel() {
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(camsoda.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

@ -29,12 +29,23 @@ public class CamsodaHttpClient extends HttpClient {
private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaHttpClient.class);
private String csrfToken = null;
public CamsodaHttpClient() {
super("camsoda");
}
@Override
public boolean login() throws IOException {
if(loggedIn) {
return true;
}
// persisted cookies might let us log in
if(checkLoginSuccess()) {
loggedIn = true;
LOG.debug("Logged in with cookies");
return true;
}
String url = Camsoda.BASE_URI + "/api/v1/auth/login";
FormBody body = new FormBody.Builder()
.add("username", Config.getInstance().getSettings().camsodaUsername)

View File

@ -25,6 +25,7 @@ import com.iheartradio.m3u8.data.PlaylistData;
import com.iheartradio.m3u8.data.StreamInfo;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.recorder.download.StreamSource;
import ctbrec.sites.Site;
import okhttp3.FormBody;
@ -181,7 +182,7 @@ public class CamsodaModel extends AbstractModel {
.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("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.addHeader("Accept", "application/json, text/plain, */*")
.addHeader("Accept-Language", "en")
.addHeader("X-CSRF-Token", csrfToken)
@ -203,7 +204,7 @@ public class CamsodaModel extends AbstractModel {
.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("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.addHeader("Accept", "application/json, text/plain, */*")
.addHeader("Accept-Language", "en")
.addHeader("X-CSRF-Token", csrfToken)
@ -227,7 +228,7 @@ public class CamsodaModel extends AbstractModel {
.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("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.addHeader("Accept", "application/json, text/plain, */*")
.addHeader("Accept-Language", "en")
.addHeader("X-CSRF-Token", csrfToken)

View File

@ -195,8 +195,8 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener {
root.setCenter(grid);
loadImage(model, thumb);
record.prefWidthProperty().bind(openInBrowser.widthProperty());
follow.prefWidthProperty().bind(openInBrowser.widthProperty());
record.minWidthProperty().bind(openInBrowser.widthProperty());
follow.minWidthProperty().bind(openInBrowser.widthProperty());
}
private void follow(Model model) {

View File

@ -29,18 +29,9 @@ import ctbrec.Model;
import ctbrec.io.HttpClient;
import ctbrec.recorder.Recorder;
import ctbrec.sites.AbstractSite;
import ctbrec.ui.DesktopIntergation;
import ctbrec.sites.ConfigUI;
import ctbrec.ui.HtmlParser;
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.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
@ -316,33 +307,8 @@ public class Chaturbate extends AbstractSite {
}
@Override
public Node getConfigurationGui() {
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("Chaturbate User"), 0, 0);
TextField username = new TextField(Config.getInstance().getSettings().username);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().username = username.getText());
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(new Label("Chaturbate Password"), 0, 1);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().password);
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().password = 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(Chaturbate.REGISTRATION_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;
public ConfigUI getConfigurationGui() {
return new ChaturbateConfigUi();
}
@Override

View File

@ -0,0 +1,48 @@
package ctbrec.sites.chaturbate;
import ctbrec.Config;
import ctbrec.sites.ConfigUI;
import ctbrec.ui.DesktopIntergation;
import ctbrec.ui.SettingsTab;
import javafx.geometry.Insets;
import javafx.scene.Parent;
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 ChaturbateConfigUi implements ConfigUI {
@Override
public Parent createConfigPanel() {
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("Chaturbate User"), 0, 0);
TextField username = new TextField(Config.getInstance().getSettings().username);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().username = username.getText());
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(new Label("Chaturbate Password"), 0, 1);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().password);
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().password = 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(Chaturbate.REGISTRATION_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

@ -20,6 +20,10 @@ public class ChaturbateHttpClient extends HttpClient {
private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateHttpClient.class);
protected String token;
public ChaturbateHttpClient() {
super("chaturbate");
}
private void extractCsrfToken(Request request) {
try {
Cookie csrfToken = cookieJar.getCookie(request.url(), "csrftoken");
@ -38,6 +42,16 @@ public class ChaturbateHttpClient extends HttpClient {
@Override
public boolean login() throws IOException {
if(loggedIn) {
return true;
}
if(checkLogin()) {
loggedIn = true;
LOG.debug("Logged in with cookies");
return true;
}
try {
Request login = new Request.Builder()
.url(Chaturbate.BASE_URI + "/auth/login/")
@ -82,6 +96,24 @@ public class ChaturbateHttpClient extends HttpClient {
return loggedIn;
}
private boolean checkLogin() throws IOException {
String url = "https://chaturbate.com/p/" + Config.getInstance().getSettings().username + "/";
Request req = new Request.Builder().url(url).build();
Response resp = execute(req);
if (resp.isSuccessful()) {
String profilePage = resp.body().string();
try {
HtmlParser.getText(profilePage, "span.tokencount");
return true;
} catch(Exception e) {
LOG.debug("Token tag not found. Login failed");
return false;
}
} else {
throw new IOException("HTTP response: " + resp.code() + " - " + resp.message());
}
}
@Override
public Response execute(Request req, boolean requiresLogin) throws IOException {
Response resp = super.execute(req, requiresLogin);

View File

@ -17,6 +17,7 @@ import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.PlaylistData;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.recorder.download.StreamSource;
import ctbrec.sites.Site;
import okhttp3.Request;
@ -147,7 +148,7 @@ public class ChaturbateModel extends AbstractModel {
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.5")
.header("Referer", getUrl())
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:59.0) Gecko/20100101 Firefox/59.0")
.header("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.header("X-CSRFToken", ((ChaturbateHttpClient)site.getHttpClient()).getToken())
.header("X-Requested-With", "XMLHttpRequest")
.build();

View File

@ -8,18 +8,9 @@ import ctbrec.Config;
import ctbrec.Model;
import ctbrec.recorder.Recorder;
import ctbrec.sites.AbstractSite;
import ctbrec.ui.DesktopIntergation;
import ctbrec.sites.ConfigUI;
import ctbrec.ui.HtmlParser;
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;
@ -129,33 +120,8 @@ public class MyFreeCams extends AbstractSite {
}
@Override
public Node getConfigurationGui() {
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("MyFreeCams User"), 0, 0);
TextField username = new TextField(Config.getInstance().getSettings().mfcUsername);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().mfcUsername = username.getText());
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(new Label("MyFreeCams Password"), 0, 1);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().mfcPassword);
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().mfcPassword = 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;
public ConfigUI getConfigurationGui() {
return new MyFreeCamsConfigUI(this);
}
@Override

View File

@ -7,9 +7,7 @@ import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -23,6 +21,8 @@ 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.google.common.collect.EvictingQueue;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
@ -45,8 +45,8 @@ public class MyFreeCamsClient {
private Moshi moshi;
private volatile boolean running = false;
private Map<Integer, SessionState> sessionStates = new HashMap<>();
private Map<Integer, MyFreeCamsModel> models = new HashMap<>();
private Cache<Integer, SessionState> sessionStates = CacheBuilder.newBuilder().maximumSize(4000).build();
private Cache<Integer, MyFreeCamsModel> models = CacheBuilder.newBuilder().maximumSize(4000).build();
private Lock lock = new ReentrantLock();
private ExecutorService executor = Executors.newSingleThreadExecutor();
private ServerConfig serverConfig;
@ -59,7 +59,7 @@ public class MyFreeCamsClient {
private int sessionId;
private long heartBeat;
private EvictingQueue<String> receivedTextHistory = EvictingQueue.create(10000);
private EvictingQueue<String> receivedTextHistory = EvictingQueue.create(100);
private MyFreeCamsClient() {
moshi = new Moshi.Builder().build();
@ -118,7 +118,7 @@ public class MyFreeCamsClient {
lock.lock();
try {
LOG.trace("Models: {}", models.size());
return new ArrayList<>(this.models.values());
return new ArrayList<>(this.models.asMap().values());
} finally {
lock.unlock();
}
@ -208,7 +208,7 @@ public class MyFreeCamsClient {
JSONObject json = new JSONObject(message.getMessage());
String[] names = JSONObject.getNames(json);
Integer uid = Integer.parseInt(names[0]);
SessionState sessionState = sessionStates.get(uid);
SessionState sessionState = sessionStates.getIfPresent(uid);
if (sessionState != null) {
JSONArray tags = json.getJSONArray(names[0]);
for (Object obj : tags) {
@ -358,7 +358,7 @@ public class MyFreeCamsClient {
if (newState.getUid() <= 0) {
return;
}
SessionState storedState = sessionStates.get(newState.getUid());
SessionState storedState = sessionStates.getIfPresent(newState.getUid());
if (storedState != null) {
storedState.merge(newState);
updateModel(storedState);
@ -384,7 +384,7 @@ public class MyFreeCamsClient {
return;
}
MyFreeCamsModel model = models.get(state.getUid());
MyFreeCamsModel model = models.getIfPresent(state.getUid());
if(model == null) {
model = mfc.createModel(state.getNm());
model.setUid(state.getUid());
@ -494,7 +494,7 @@ public class MyFreeCamsClient {
public void update(MyFreeCamsModel model) {
lock.lock();
try {
for (SessionState state : sessionStates.values()) {
for (SessionState state : sessionStates.asMap().values()) {
if(Objects.equals(state.getNm(), model.getName()) || Objects.equals(model.getUid(), state.getUid())) {
model.update(state, getStreamUrl(state));
return;
@ -532,7 +532,7 @@ public class MyFreeCamsClient {
}
public MyFreeCamsModel getModel(int uid) {
return models.get(uid);
return models.getIfPresent(uid);
}
public void execute(Runnable r) {
@ -540,7 +540,7 @@ public class MyFreeCamsClient {
}
public void getSessionState(ctbrec.Model model) {
for (SessionState state : sessionStates.values()) {
for (SessionState state : sessionStates.asMap().values()) {
if(Objects.equals(state.getNm(), model.getName())) {
JsonAdapter<SessionState> adapter = moshi.adapter(SessionState.class).indent(" ");
System.out.println(adapter.toJson(state));

View File

@ -0,0 +1,54 @@
package ctbrec.sites.mfc;
import ctbrec.Config;
import ctbrec.sites.ConfigUI;
import ctbrec.ui.DesktopIntergation;
import ctbrec.ui.SettingsTab;
import javafx.geometry.Insets;
import javafx.scene.Parent;
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 MyFreeCamsConfigUI implements ConfigUI {
private MyFreeCams myFreeCams;
public MyFreeCamsConfigUI(MyFreeCams myFreeCams) {
this.myFreeCams = myFreeCams;
}
@Override
public Parent createConfigPanel() {
GridPane layout = SettingsTab.createGridLayout();
layout.add(new Label("MyFreeCams User"), 0, 0);
TextField username = new TextField(Config.getInstance().getSettings().mfcUsername);
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().mfcUsername = username.getText());
GridPane.setFillWidth(username, true);
GridPane.setHgrow(username, Priority.ALWAYS);
GridPane.setColumnSpan(username, 2);
layout.add(username, 1, 0);
layout.add(new Label("MyFreeCams Password"), 0, 1);
PasswordField password = new PasswordField();
password.setText(Config.getInstance().getSettings().mfcPassword);
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().mfcPassword = 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(myFreeCams.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

@ -5,11 +5,13 @@ import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import ctbrec.ui.HtmlParser;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.FormBody;
@ -24,12 +26,22 @@ public class MyFreeCamsHttpClient extends HttpClient {
private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsHttpClient.class);
public MyFreeCamsHttpClient() {
super("myfreecams");
}
@Override
public boolean login() throws IOException {
if(loggedIn) {
return true;
}
if(checkLogin()) {
loggedIn = true;
LOG.debug("Logged in with cookies");
return true;
}
String username = Config.getInstance().getSettings().mfcUsername;
String password = Config.getInstance().getSettings().mfcPassword;
RequestBody body = new FormBody.Builder()
@ -61,6 +73,25 @@ public class MyFreeCamsHttpClient extends HttpClient {
}
}
private boolean checkLogin() throws IOException {
Request req = new Request.Builder().url(MyFreeCams.BASE_URI + "/php/account.php?request=status").build();
Response resp = execute(req);
if(resp.isSuccessful()) {
String content = resp.body().string();
try {
Elements tags = HtmlParser.getTags(content, "div.content > p > b");
tags.get(2).text();
return true;
} catch(Exception e) {
LOG.debug("Token tag not found. Login failed");
return false;
}
} else {
resp.close();
throw new IOException(resp.code() + " " + resp.message());
}
}
public WebSocket newWebSocket(Request req, WebSocketListener webSocketListener) {
return client.newWebSocket(req, webSocketListener);
}

View File

@ -213,6 +213,7 @@ public class MyFreeCamsModel extends AbstractModel {
public void setName(String name) {
if(getName() != null && name != null && !getName().equals(name)) {
LOG.debug("Model name changed {} -> {}", getName(), name);
setUrl("https://profiles.myfreecams.com/" + name);
}
super.setName(name);
}

View File

@ -37,8 +37,12 @@ public class X {
if(x == null) {
return;
}
fcext.merge(x.fcext);
share.merge(x.share);
if (fcext != null) {
fcext.merge(x.fcext);
}
if (share != null) {
share.merge(x.share);
}
additionalProperties.putAll(x.additionalProperties);
}

View File

@ -27,6 +27,7 @@ import ctbrec.recorder.LocalRecorder;
import ctbrec.recorder.Recorder;
import ctbrec.recorder.RemoteRecorder;
import ctbrec.sites.Site;
import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate;
@ -60,10 +61,11 @@ public class CamrecApplication extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
sites.add(new BongaCams());
sites.add(new Cam4());
sites.add(new Camsoda());
sites.add(new Chaturbate());
sites.add(new MyFreeCams());
sites.add(new Camsoda());
sites.add(new Cam4());
loadConfig();
createHttpClient();
bus = new AsyncEventBus(Executors.newSingleThreadExecutor());
@ -198,7 +200,7 @@ public class CamrecApplication extends Application {
}
private void createHttpClient() {
httpClient = new HttpClient() {
httpClient = new HttpClient("camrec") {
@Override
public boolean login() throws IOException {
return false;

View File

@ -9,7 +9,6 @@ import com.iheartradio.m3u8.PlaylistException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel;
import ctbrec.Model;
import ctbrec.recorder.download.StreamSource;
import ctbrec.sites.Site;
@ -19,15 +18,13 @@ import javafx.beans.property.SimpleBooleanProperty;
/**
* Just a wrapper for Model, which augments it with JavaFX value binding properties, so that UI widgets get updated proeprly
*/
public class JavaFxModel extends AbstractModel {
public class JavaFxModel implements Model {
private transient BooleanProperty onlineProperty = new SimpleBooleanProperty();
private transient BooleanProperty pausedProperty = new SimpleBooleanProperty();
private Model delegate;
public JavaFxModel(Model delegate) {
this.delegate = delegate;
try {
onlineProperty.set(delegate.isOnline());
} catch (IOException | ExecutionException | InterruptedException e) {}
}
@Override
@ -89,6 +86,10 @@ public class JavaFxModel extends AbstractModel {
return onlineProperty;
}
public BooleanProperty getPausedProperty() {
return pausedProperty;
}
Model getDelegate() {
return delegate;
}
@ -157,4 +158,35 @@ public class JavaFxModel extends AbstractModel {
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
delegate.writeSiteSpecificData(writer);
}
@Override
public String getDescription() {
return delegate.getDescription();
}
@Override
public void setDescription(String description) {
delegate.setDescription(description);
}
@Override
public int getStreamUrlIndex() {
return delegate.getStreamUrlIndex();
}
@Override
public void setStreamUrlIndex(int streamUrlIndex) {
delegate.setStreamUrlIndex(streamUrlIndex);
}
@Override
public boolean isSuspended() {
return delegate.isSuspended();
}
@Override
public void setSuspended(boolean suspended) {
delegate.setSuspended(suspended);
pausedProperty.set(suspended);
}
}

View File

@ -0,0 +1,18 @@
package ctbrec.ui;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
public class PauseIndicator extends HBox {
public PauseIndicator(Color c, int size) {
spacingProperty().setValue(size*1/5);
Rectangle left = new Rectangle(size*2/5, size);
left.setFill(c);
Rectangle right = new Rectangle(size*2/5, size);
right.setFill(c);
getChildren().add(left);
getChildren().add(right);
}
}

View File

@ -67,7 +67,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
ScrollPane scrollPane = new ScrollPane();
TableView<JavaFxModel> table = new TableView<JavaFxModel>();
ObservableList<JavaFxModel> observableModels = FXCollections.observableArrayList();
ContextMenu popup = createContextMenu();
ContextMenu popup;
Label modelLabel = new Label("Model");
TextField model = new TextField();
@ -104,11 +104,17 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
online.setCellValueFactory((cdf) -> cdf.getValue().getOnlineProperty());
online.setCellFactory(CheckBoxTableCell.forTableColumn(online));
online.setPrefWidth(60);
table.getColumns().addAll(name, url, online);
TableColumn<JavaFxModel, Boolean> paused = new TableColumn<>("Paused");
paused.setCellValueFactory((cdf) -> cdf.getValue().getPausedProperty());
paused.setCellFactory(CheckBoxTableCell.forTableColumn(paused));
paused.setPrefWidth(60);
table.getColumns().addAll(name, url, online, paused);
table.setItems(observableModels);
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
popup = createContextMenu();
popup.show(table, event.getScreenX(), event.getScreenY());
if(popup != null) {
popup.show(table, event.getScreenX(), event.getScreenY());
}
event.consume();
});
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
@ -186,17 +192,20 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
queue.clear();
for (Model model : models) {
int index = observableModels.indexOf(model);
final JavaFxModel javaFxModel;
if (index == -1) {
observableModels.add(new JavaFxModel(model));
javaFxModel = new JavaFxModel(model);
observableModels.add(javaFxModel);
} else {
// make sure to update the JavaFX online property, so that the table cell is updated
JavaFxModel javaFxModel = observableModels.get(index);
threadPool.submit(() -> {
try {
javaFxModel.getOnlineProperty().set(javaFxModel.isOnline());
} catch (IOException | ExecutionException | InterruptedException e) {}
});
javaFxModel = observableModels.get(index);
}
threadPool.submit(() -> {
try {
javaFxModel.getOnlineProperty().set(javaFxModel.isOnline());
javaFxModel.setSuspended(model.isSuspended());
} catch (IOException | ExecutionException | InterruptedException e) {}
});
}
for (Iterator<JavaFxModel> iterator = observableModels.iterator(); iterator.hasNext();) {
Model model = iterator.next();
@ -204,7 +213,6 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
iterator.remove();
}
}
});
updateService.setOnFailed((event) -> {
LOG.info("Couldn't get list of models from recorder", event.getSource().getException());
@ -253,26 +261,37 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}
private ContextMenu createContextMenu() {
MenuItem stop = new MenuItem("Stop Recording");
JavaFxModel selectedModel = table.getSelectionModel().getSelectedItem();
if(selectedModel == null) {
return null;
}
MenuItem stop = new MenuItem("Remove Model");
stop.setOnAction((e) -> stopAction());
MenuItem copyUrl = new MenuItem("Copy URL");
copyUrl.setOnAction((e) -> {
Model selected = table.getSelectionModel().getSelectedItem();
Model selected = selectedModel;
final Clipboard clipboard = Clipboard.getSystemClipboard();
final ClipboardContent content = new ClipboardContent();
content.putString(selected.getUrl());
clipboard.setContent(content);
});
MenuItem pauseRecording = new MenuItem("Pause Recording");
pauseRecording.setOnAction((e) -> pauseRecording());
MenuItem resumeRecording = new MenuItem("Resume Recording");
resumeRecording.setOnAction((e) -> resumeRecording());
MenuItem openInBrowser = new MenuItem("Open in Browser");
openInBrowser.setOnAction((e) -> DesktopIntergation.open(table.getSelectionModel().getSelectedItem().getUrl()));
openInBrowser.setOnAction((e) -> DesktopIntergation.open(selectedModel.getUrl()));
MenuItem openInPlayer = new MenuItem("Open in Player");
openInPlayer.setOnAction((e) -> Player.play(table.getSelectionModel().getSelectedItem().getUrl()));
openInPlayer.setOnAction((e) -> Player.play(selectedModel.getUrl()));
MenuItem switchStreamSource = new MenuItem("Switch resolution");
switchStreamSource.setOnAction((e) -> switchStreamSource(table.getSelectionModel().getSelectedItem()));
switchStreamSource.setOnAction((e) -> switchStreamSource(selectedModel));
return new ContextMenu(stop, copyUrl, openInBrowser, switchStreamSource);
ContextMenu menu = new ContextMenu(stop);
menu.getItems().add(selectedModel.isSuspended() ? resumeRecording : pauseRecording);
menu.getItems().addAll(copyUrl, openInBrowser, switchStreamSource);
return menu;
}
private void switchStreamSource(JavaFxModel fxModel) {
@ -345,4 +364,60 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}.start();
}
};
private void pauseRecording() {
JavaFxModel model = table.getSelectionModel().getSelectedItem();
Model delegate = table.getSelectionModel().getSelectedItem().getDelegate();
if (delegate != null) {
table.setCursor(Cursor.WAIT);
new Thread() {
@Override
public void run() {
try {
recorder.suspendRecording(delegate);
Platform.runLater(() -> model.setSuspended(true));
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Couldn't pause recording", e1);
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't pause recording");
alert.setContentText("Error while pausing the recording: " + e1.getLocalizedMessage());
alert.showAndWait();
});
} finally {
table.setCursor(Cursor.DEFAULT);
}
}
}.start();
}
};
private void resumeRecording() {
JavaFxModel model = table.getSelectionModel().getSelectedItem();
Model delegate = table.getSelectionModel().getSelectedItem().getDelegate();
if (delegate != null) {
table.setCursor(Cursor.WAIT);
new Thread() {
@Override
public void run() {
try {
recorder.resumeRecording(delegate);
Platform.runLater(() -> model.setSuspended(false));
} catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e1) {
LOG.error("Couldn't resume recording", e1);
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't resume recording");
alert.setContentText("Error while resuming the recording: " + e1.getLocalizedMessage());
alert.showAndWait();
});
} finally {
table.setCursor(Cursor.DEFAULT);
}
}
}.start();
}
};
}

View File

@ -139,9 +139,11 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
table.setItems(observableRecordings);
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
Recording recording = table.getSelectionModel().getSelectedItem();
popup = createContextMenu(recording);
if(!popup.getItems().isEmpty()) {
popup.show(table, event.getScreenX(), event.getScreenY());
if(recording != null) {
popup = createContextMenu(recording);
if(!popup.getItems().isEmpty()) {
popup.show(table, event.getScreenX(), event.getScreenY());
}
}
event.consume();
});

View File

@ -14,6 +14,7 @@ import com.sun.javafx.collections.ObservableListWrapper;
import ctbrec.Config;
import ctbrec.Hmac;
import ctbrec.Settings;
import ctbrec.sites.ConfigUI;
import ctbrec.sites.Site;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@ -27,6 +28,7 @@ import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
import javafx.scene.control.TextInputDialog;
@ -54,7 +56,9 @@ public class SettingsTab extends Tab implements TabSelectionListener {
public static final int CHECKBOX_MARGIN = 6;
private TextField recordingsDirectory;
private Button recordingsDirectoryButton;
private Button postProcessingDirectoryButton;
private TextField mediaPlayer;
private TextField postProcessing;
private TextField server;
private TextField port;
private CheckBox loadResolution;
@ -88,7 +92,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
ColumnConstraints cc = new ColumnConstraints();
cc.setPercentWidth(50);
mainLayout.getColumnConstraints().setAll(cc, cc);
setContent(mainLayout);
setContent(new ScrollPane(mainLayout));
VBox leftSide = new VBox(15);
VBox rightSide = new VBox(15);
GridPane.setHgrow(leftSide, Priority.ALWAYS);
@ -119,9 +123,9 @@ public class SettingsTab extends Tab implements TabSelectionListener {
rightSide.getChildren().add(credentialsAccordion);
for (int i = 0; i < sites.size(); i++) {
Site site = sites.get(i);
Node siteConfig = site.getConfigurationGui();
ConfigUI siteConfig = site.getConfigurationGui();
if(siteConfig != null) {
TitledPane pane = new TitledPane(site.getName(), siteConfig);
TitledPane pane = new TitledPane(site.getName(), siteConfig.createConfigPanel());
credentialsAccordion.getPanes().add(pane);
}
}
@ -265,6 +269,17 @@ public class SettingsTab extends Tab implements TabSelectionListener {
layout.add(mediaPlayer, 1, 1);
layout.add(createMpvBrowseButton(), 3, 1);
layout.add(new Label("Post-Processing"), 0, 2);
postProcessing = new TextField(Config.getInstance().getSettings().postProcessing);
postProcessing.focusedProperty().addListener(createPostProcessingFocusListener());
GridPane.setFillWidth(postProcessing, true);
GridPane.setHgrow(postProcessing, Priority.ALWAYS);
GridPane.setColumnSpan(postProcessing, 2);
GridPane.setMargin(postProcessing, new Insets(0, 0, 0, CHECKBOX_MARGIN));
layout.add(postProcessing, 1, 2);
postProcessingDirectoryButton = createPostProcessingBrowseButton();
layout.add(postProcessingDirectoryButton, 3, 2);
TitledPane locations = new TitledPane("Locations", layout);
locations.setCollapsible(false);
return locations;
@ -378,6 +393,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
recordingsDirectoryButton.setDisable(!local);
splitAfter.setDisable(!local);
maxResolution.setDisable(!local);
postProcessing.setDisable(!local);
postProcessingDirectoryButton.setDisable(!local);
}
private ChangeListener<? super Boolean> createRecordingsDirectoryFocusListener() {
@ -412,6 +429,22 @@ public class SettingsTab extends Tab implements TabSelectionListener {
};
}
private ChangeListener<? super Boolean> createPostProcessingFocusListener() {
return new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> arg0, Boolean oldPropertyValue, Boolean newPropertyValue) {
if (newPropertyValue) {
postProcessing.setBorder(Border.EMPTY);
postProcessing.setTooltip(null);
} else {
String input = postProcessing.getText();
File program = new File(input);
setPostProcessing(program);
}
}
};
}
private void setMpv(File program) {
String msg = validateProgram(program);
if (msg != null) {
@ -422,6 +455,16 @@ public class SettingsTab extends Tab implements TabSelectionListener {
}
}
private void setPostProcessing(File program) {
String msg = validateProgram(program);
if (msg != null) {
postProcessing.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.DASHED, new CornerRadii(2), new BorderWidths(2))));
postProcessing.setTooltip(new Tooltip(msg));
} else {
Config.getInstance().getSettings().postProcessing = postProcessing.getText();
}
}
private String validateProgram(File program) {
if (program == null || !program.exists()) {
return "File does not exist";
@ -468,6 +511,27 @@ public class SettingsTab extends Tab implements TabSelectionListener {
return button;
}
private Button createPostProcessingBrowseButton() {
Button button = new Button("Select");
button.setOnAction((e) -> {
FileChooser chooser = new FileChooser();
File program = chooser.showOpenDialog(null);
if(program != null) {
try {
postProcessing.setText(program.getCanonicalPath());
} catch (IOException e1) {
LOG.error("Couldn't determine path", e1);
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Whoopsie");
alert.setContentText("Couldn't determine path");
alert.showAndWait();
}
setPostProcessing(program);
}
});
return button;
}
private void setRecordingsDir(File dir) {
if (dir != null && dir.isDirectory()) {
try {

View File

@ -64,6 +64,7 @@ public class ThumbCell extends StackPane {
private Text resolutionTag;
private Recorder recorder;
private Circle recordingIndicator;
private PauseIndicator pausedIndicator;
private int index = 0;
ContextMenu popup;
private final Color colorNormal = Color.BLACK;
@ -81,6 +82,7 @@ public class ThumbCell extends StackPane {
this.model = model;
this.recorder = recorder;
recording = recorder.isRecording(model);
model.setSuspended(recorder.isSuspended(model));
this.setStyle("-fx-background-color: lightgray");
iv = new ImageView();
@ -118,7 +120,10 @@ public class ThumbCell extends StackPane {
StackPane.setAlignment(name, Pos.BOTTOM_CENTER);
getChildren().add(name);
topic = new Text(model.getDescription());
topic = new Text();
String txt = recording ? " " : "";
txt += model.getDescription();
topic.setText(txt);
topic.setFill(Color.WHITE);
topic.setFont(new Font("Sansserif", 13));
@ -142,6 +147,12 @@ public class ThumbCell extends StackPane {
StackPane.setAlignment(recordingIndicator, Pos.TOP_LEFT);
getChildren().add(recordingIndicator);
pausedIndicator = new PauseIndicator(colorRecording, 16);
pausedIndicator.setVisible(false);
StackPane.setMargin(pausedIndicator, new Insets(3));
StackPane.setAlignment(pausedIndicator, Pos.TOP_LEFT);
getChildren().add(pausedIndicator);
selectionOverlay = new Rectangle();
selectionOverlay.setOpacity(0);
StackPane.setAlignment(selectionOverlay, Pos.TOP_LEFT);
@ -208,13 +219,15 @@ 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) {
if(e.getCause() instanceof EOFException) {
LOG.warn("Couldn't update resolution tag for model {}. Playlist empty", model.getName());
} else if(e.getCause() instanceof ParseException) {
LOG.warn("Couldn't update resolution tag for model {} - {}", model.getName(), e.getMessage());
} else {
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e);
}
@ -321,7 +334,14 @@ public class ThumbCell extends StackPane {
Color c = mouseHovering ? colorHighlight : colorNormal;
nameBackground.setFill(c);
}
recordingIndicator.setVisible(recording);
if(recording) {
recordingIndicator.setVisible(!model.isSuspended());
pausedIndicator.setVisible(model.isSuspended());
} else {
recordingIndicator.setVisible(false);
pausedIndicator.setVisible(false);
}
}
void startStopAction(boolean start) {
@ -347,6 +367,31 @@ public class ThumbCell extends StackPane {
}
}
void pauseResumeAction(boolean pause) {
setCursor(Cursor.WAIT);
new Thread(() -> {
try {
if(pause) {
recorder.suspendRecording(model);
} else {
recorder.resumeRecording(model);
}
setRecording(recording);
} catch (Exception e1) {
LOG.error("Couldn't pause/resume recording", e1);
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't pause/resume recording");
alert.setContentText("I/O error while pausing/resuming the recording: " + e1.getLocalizedMessage());
alert.showAndWait();
});
} finally {
setCursor(Cursor.DEFAULT);
}
}).start();
}
private void _startStopAction(Model model, boolean start) {
new Thread(() -> {
try {
@ -426,6 +471,7 @@ public class ThumbCell extends StackPane {
this.model.setPreview(model.getPreview());
this.model.setTags(model.getTags());
this.model.setUrl(model.getUrl());
this.model.setSuspended(model.isSuspended());
update();
}
@ -438,6 +484,7 @@ public class ThumbCell extends StackPane {
}
private void update() {
model.setSuspended(recorder.isSuspended(model));
setRecording(recorder.isRecording(model));
setImage(model.getPreview());
String txt = recording ? " " : "";

View File

@ -324,6 +324,12 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
stop.setOnAction((e) -> startStopAction(getSelectedThumbCells(cell), false));
MenuItem startStop = recorder.isRecording(cell.getModel()) ? stop : start;
MenuItem pause = new MenuItem("Pause Recording");
pause.setOnAction((e) -> pauseResumeAction(getSelectedThumbCells(cell), true));
MenuItem resume = new MenuItem("Resume Recording");
resume.setOnAction((e) -> pauseResumeAction(getSelectedThumbCells(cell), false));
MenuItem pauseResume = recorder.isSuspended(cell.getModel()) ? resume : pause;
MenuItem follow = new MenuItem("Follow");
follow.setOnAction((e) -> follow(getSelectedThumbCells(cell), true));
MenuItem unfollow = new MenuItem("Unfollow");
@ -389,6 +395,9 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
contextMenu.setHideOnEscape(true);
contextMenu.setAutoFix(true);
contextMenu.getItems().addAll(openInPlayer, startStop);
if(recorder.isRecording(cell.getModel())) {
contextMenu.getItems().add(pauseResume);
}
if(site.supportsFollow()) {
MenuItem followOrUnFollow = (this instanceof FollowedTab) ? unfollow : follow;
followOrUnFollow.setDisable(!site.credentialsAvailable());
@ -431,6 +440,12 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
}
}
private void pauseResumeAction(List<ThumbCell> selection, boolean pause) {
for (ThumbCell thumbCell : selection) {
thumbCell.pauseResumeAction(pause);
}
}
private void startPlayer(List<ThumbCell> selection) {
for (ThumbCell thumbCell : selection) {
thumbCell.startPlayer();

18
src/main/resources/pp.bat Normal file
View File

@ -0,0 +1,18 @@
REM This is an post-processing example script
REM This script is just a wrapper to call the actual powershell script.
REM But you can do something completly different here, too.
REM
REM If you want to use powershell, make sure, that your system allows the execution of powershell scripts:
REM 1. Open cmd.exe as administrator (Click on start, type cmd.exe, right-click on it and select "Run as administrator")
REM 2. Execute powershell
REM 3. Execute Set-ExecutionPolicy Unrestricted
@echo off
set directory=%1
set file=%2
set model=%3
set site=%4
set unixtime=%5
powershell -F C:\Users\henrik\Desktop\ctbrec\pp.ps1 -dir "%directory%" -file "%file%" -model "%model%" -site "%site%" -time "%unixtime%"

17
src/main/resources/pp.ps1 Normal file
View File

@ -0,0 +1,17 @@
# parse command line parameters
param (
[Parameter(Mandatory=$true)][string]$dir,
[Parameter(Mandatory=$true)][string]$file,
[Parameter(Mandatory=$true)][string]$model,
[Parameter(Mandatory=$true)][string]$site,
[Parameter(Mandatory=$true)][string]$time
)
# convert unixtime into a date object
$epoch = get-date "1/1/1970"
$date = $epoch.AddSeconds($time)
# print out a theoretical new file name, you could use "rename" here, to rename the file
# or move it somewhere or ...
$newname = "$($model)_$($site)_$($date.toString("yyyyMMdd-HHmm")).ts"
ren $file $newname

28
src/main/resources/pp.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash
# $1 directory (absolute path)
# $2 file (absolute path)
# $3 model name
# $4 site name
# $5 unixtime
# get the filename without path
FILE=`basename $2`
# format unixtime to human readable
TIME=$(date --date="@$5" +%d.%m.%Y_%H:%M)
# define filename of end result
MP4=$(echo "$1/$4_$3_$TIME.mp4")
# remux ts to mp4
ffmpeg -i $2 -c:v copy -c:a copy -f mp4 $MP4
# move mp4 to target directory
mv $MP4 /tmp
# delete the original .ts file
rm $2
# delete the directory of the recording
rm -r $1