Merge branch 'dev'
This commit is contained in:
commit
2eab3b3bd4
|
@ -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"/>
|
||||
|
|
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -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
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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 = "";
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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\"}";
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package ctbrec.sites;
|
||||
|
||||
import javafx.scene.Parent;
|
||||
|
||||
public interface ConfigUI {
|
||||
public Parent createConfigPanel();
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 = () -> {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 ? " " : "";
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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%"
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue