Merge branch 'dev'
This commit is contained in:
commit
5de66ce373
|
@ -1,3 +1,10 @@
|
||||||
|
1.7.0
|
||||||
|
========================
|
||||||
|
* Added CamSoda
|
||||||
|
* Added detection of model name changes for MyFreeCams
|
||||||
|
* Added setting to define a maximum resolution
|
||||||
|
* Fixed sorting by date in recordings table
|
||||||
|
|
||||||
1.6.1
|
1.6.1
|
||||||
========================
|
========================
|
||||||
* Fixed UI freeze, which occured for a high number of recorded models
|
* Fixed UI freeze, which occured for a high number of recorded models
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
DIR=$(dirname $0)
|
||||||
|
pushd $DIR
|
||||||
|
JAVA_HOME="$DIR/jre/Contents/Home"
|
||||||
|
JAVA="$JAVA_HOME/bin/java"
|
||||||
|
$JAVA -version
|
||||||
|
$JAVA -cp ${name.final}.jar ctbrec.ui.Launcher
|
||||||
|
popd
|
5
pom.xml
5
pom.xml
|
@ -5,7 +5,7 @@
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>ctbrec</groupId>
|
<groupId>ctbrec</groupId>
|
||||||
<artifactId>ctbrec</artifactId>
|
<artifactId>ctbrec</artifactId>
|
||||||
<version>1.6.1</version>
|
<version>1.7.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
@ -66,6 +66,7 @@
|
||||||
<descriptor>src/assembly/win64-jre.xml</descriptor>
|
<descriptor>src/assembly/win64-jre.xml</descriptor>
|
||||||
<descriptor>src/assembly/win32-jre.xml</descriptor>
|
<descriptor>src/assembly/win32-jre.xml</descriptor>
|
||||||
<descriptor>src/assembly/linux.xml</descriptor>
|
<descriptor>src/assembly/linux.xml</descriptor>
|
||||||
|
<descriptor>src/assembly/macos-jre.xml</descriptor>
|
||||||
</descriptors>
|
</descriptors>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
|
@ -77,7 +78,7 @@
|
||||||
<version>1.7.22</version>
|
<version>1.7.22</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<id>l4j-clui</id>
|
<id>l4j-win</id>
|
||||||
<phase>package</phase>
|
<phase>package</phase>
|
||||||
<goals>
|
<goals>
|
||||||
<goal>launch4j</goal>
|
<goal>launch4j</goal>
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
DIR=$(dirname $0)
|
||||||
|
pushd $DIR
|
||||||
|
JAVA_HOME="$DIR/jre/Contents/Home"
|
||||||
|
JAVA="$JAVA_HOME/bin/java"
|
||||||
|
$JAVA -version
|
||||||
|
$JAVA -cp ${name.final}.jar -Dctbrec.config=server.json ctbrec.recorder.server.HttpServer
|
||||||
|
popd
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<assembly>
|
||||||
|
<id>macos-jre</id>
|
||||||
|
<formats>
|
||||||
|
<format>zip</format>
|
||||||
|
</formats>
|
||||||
|
<includeBaseDirectory>false</includeBaseDirectory>
|
||||||
|
<files>
|
||||||
|
<file>
|
||||||
|
<source>${project.basedir}/ctbrec-macos.sh</source>
|
||||||
|
<outputDirectory>ctbrec</outputDirectory>
|
||||||
|
<filtered>true</filtered>
|
||||||
|
</file>
|
||||||
|
<file>
|
||||||
|
<source>${project.basedir}/server-macos.sh</source>
|
||||||
|
<outputDirectory>ctbrec</outputDirectory>
|
||||||
|
<filtered>true</filtered>
|
||||||
|
</file>
|
||||||
|
<file>
|
||||||
|
<source>${project.build.directory}/${name.final}.jar</source>
|
||||||
|
<outputDirectory>ctbrec</outputDirectory>
|
||||||
|
</file>
|
||||||
|
</files>
|
||||||
|
<fileSets>
|
||||||
|
<fileSet>
|
||||||
|
<directory>jre/jre1.8.0_192_macos</directory>
|
||||||
|
<includes>
|
||||||
|
<include>**/*</include>
|
||||||
|
</includes>
|
||||||
|
<outputDirectory>ctbrec/jre</outputDirectory>
|
||||||
|
<filtered>false</filtered>
|
||||||
|
</fileSet>
|
||||||
|
</fileSets>
|
||||||
|
</assembly>
|
|
@ -2,14 +2,11 @@ package ctbrec;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import com.iheartradio.m3u8.ParseException;
|
import com.squareup.moshi.JsonReader;
|
||||||
import com.iheartradio.m3u8.PlaylistException;
|
import com.squareup.moshi.JsonWriter;
|
||||||
|
|
||||||
import ctbrec.recorder.download.StreamSource;
|
|
||||||
|
|
||||||
public abstract class AbstractModel implements Model {
|
public abstract class AbstractModel implements Model {
|
||||||
|
|
||||||
|
@ -20,6 +17,11 @@ public abstract class AbstractModel implements Model {
|
||||||
private List<String> tags = new ArrayList<>();
|
private List<String> tags = new ArrayList<>();
|
||||||
private int streamUrlIndex = -1;
|
private int streamUrlIndex = -1;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
||||||
|
return isOnline(false);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUrl() {
|
public String getUrl() {
|
||||||
return url;
|
return url;
|
||||||
|
@ -81,16 +83,13 @@ public abstract class AbstractModel implements Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getSegmentPlaylistUrl() throws IOException, ExecutionException, ParseException, PlaylistException {
|
public void readSiteSpecificData(JsonReader reader) throws IOException {
|
||||||
List<StreamSource> streamSources = getStreamSources();
|
// noop default implementation, can be overriden by concrete models
|
||||||
String url = null;
|
}
|
||||||
if(getStreamUrlIndex() >= 0 && getStreamUrlIndex() < streamSources.size()) {
|
|
||||||
url = streamSources.get(getStreamUrlIndex()).getMediaPlaylistUrl();
|
@Override
|
||||||
} else {
|
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
|
||||||
Collections.sort(streamSources);
|
// noop default implementation, can be overriden by concrete models
|
||||||
url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl();
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -6,6 +6,8 @@ import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import com.iheartradio.m3u8.ParseException;
|
import com.iheartradio.m3u8.ParseException;
|
||||||
import com.iheartradio.m3u8.PlaylistException;
|
import com.iheartradio.m3u8.PlaylistException;
|
||||||
|
import com.squareup.moshi.JsonReader;
|
||||||
|
import com.squareup.moshi.JsonWriter;
|
||||||
|
|
||||||
import ctbrec.recorder.download.StreamSource;
|
import ctbrec.recorder.download.StreamSource;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
|
@ -27,7 +29,6 @@ public interface Model {
|
||||||
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException;
|
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException;
|
||||||
public String getOnlineState(boolean failFast) throws IOException, ExecutionException;
|
public String getOnlineState(boolean failFast) throws IOException, ExecutionException;
|
||||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException;
|
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException;
|
||||||
public String getSegmentPlaylistUrl() throws IOException, ExecutionException, ParseException, PlaylistException;
|
|
||||||
public void invalidateCacheEntries();
|
public void invalidateCacheEntries();
|
||||||
public void receiveTip(int tokens) throws IOException;
|
public void receiveTip(int tokens) throws IOException;
|
||||||
public int[] getStreamResolution(boolean failFast) throws ExecutionException;
|
public int[] getStreamResolution(boolean failFast) throws ExecutionException;
|
||||||
|
@ -35,4 +36,6 @@ public interface Model {
|
||||||
public boolean unfollow() throws IOException;
|
public boolean unfollow() throws IOException;
|
||||||
public void setSite(Site site);
|
public void setSite(Site site);
|
||||||
public Site getSite();
|
public Site getSite();
|
||||||
|
public void writeSiteSpecificData(JsonWriter writer) throws IOException;
|
||||||
|
public void readSiteSpecificData(JsonReader reader) throws IOException;
|
||||||
}
|
}
|
|
@ -24,6 +24,8 @@ public class Settings {
|
||||||
public String password = ""; // chaturbate password TODO maybe rename this onetime
|
public String password = ""; // chaturbate password TODO maybe rename this onetime
|
||||||
public String mfcUsername = "";
|
public String mfcUsername = "";
|
||||||
public String mfcPassword = "";
|
public String mfcPassword = "";
|
||||||
|
public String camsodaUsername = "";
|
||||||
|
public String camsodaPassword = "";
|
||||||
public String cam4Username;
|
public String cam4Username;
|
||||||
public String cam4Password;
|
public String cam4Password;
|
||||||
public String lastDownloadDir = "";
|
public String lastDownloadDir = "";
|
||||||
|
@ -32,6 +34,7 @@ public class Settings {
|
||||||
public boolean determineResolution = false;
|
public boolean determineResolution = false;
|
||||||
public boolean requireAuthentication = false;
|
public boolean requireAuthentication = false;
|
||||||
public boolean chooseStreamQuality = false;
|
public boolean chooseStreamQuality = false;
|
||||||
|
public int maximumResolution = 0;
|
||||||
public byte[] key = null;
|
public byte[] key = null;
|
||||||
public ProxyType proxyType = ProxyType.DIRECT;
|
public ProxyType proxyType = ProxyType.DIRECT;
|
||||||
public String proxyHost;
|
public String proxyHost;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package ctbrec.ui;
|
package ctbrec.io;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -34,6 +34,7 @@ public class CookieJarImpl implements CookieJar {
|
||||||
if(newCookie.name().equalsIgnoreCase(name)) {
|
if(newCookie.name().equalsIgnoreCase(name)) {
|
||||||
LOG.debug("Replacing cookie {} {} -> {} [{}]", oldCookie.name(), oldCookie.value(), newCookie.value(), oldCookie.domain());
|
LOG.debug("Replacing cookie {} {} -> {} [{}]", oldCookie.name(), oldCookie.value(), newCookie.value(), oldCookie.domain());
|
||||||
iterator.remove();
|
iterator.remove();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,7 +7,6 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.Settings.ProxyType;
|
import ctbrec.Settings.ProxyType;
|
||||||
import ctbrec.ui.CookieJarImpl;
|
|
||||||
import okhttp3.ConnectionPool;
|
import okhttp3.ConnectionPool;
|
||||||
import okhttp3.Credentials;
|
import okhttp3.Credentials;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
|
|
|
@ -32,45 +32,51 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
||||||
String url = null;
|
String url = null;
|
||||||
String type = null;
|
String type = null;
|
||||||
int streamUrlIndex = -1;
|
int streamUrlIndex = -1;
|
||||||
|
|
||||||
|
Model model = null;
|
||||||
while(reader.hasNext()) {
|
while(reader.hasNext()) {
|
||||||
Token token = reader.peek();
|
try {
|
||||||
if(token == Token.NAME) {
|
Token token = reader.peek();
|
||||||
String key = reader.nextName();
|
if(token == Token.NAME) {
|
||||||
if(key.equals("name")) {
|
String key = reader.nextName();
|
||||||
name = reader.nextString();
|
if(key.equals("name")) {
|
||||||
} else if(key.equals("description")) {
|
name = reader.nextString();
|
||||||
description = reader.nextString();
|
model.setName(name);
|
||||||
} else if(key.equals("url")) {
|
} else if(key.equals("description")) {
|
||||||
url = reader.nextString();
|
description = reader.nextString();
|
||||||
} else if(key.equals("type")) {
|
model.setDescription(description);
|
||||||
type = reader.nextString();
|
} else if(key.equals("url")) {
|
||||||
} else if(key.equals("streamUrlIndex")) {
|
url = reader.nextString();
|
||||||
streamUrlIndex = reader.nextInt();
|
model.setUrl(url);
|
||||||
|
} else if(key.equals("type")) {
|
||||||
|
type = reader.nextString();
|
||||||
|
Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()));
|
||||||
|
model = (Model) modelClass.newInstance();
|
||||||
|
} else if(key.equals("streamUrlIndex")) {
|
||||||
|
streamUrlIndex = reader.nextInt();
|
||||||
|
model.setStreamUrlIndex(streamUrlIndex);
|
||||||
|
} else if(key.equals("siteSpecific")) {
|
||||||
|
reader.beginObject();
|
||||||
|
model.readSiteSpecificData(reader);
|
||||||
|
reader.endObject();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reader.skipValue();
|
||||||
}
|
}
|
||||||
} else {
|
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
|
||||||
reader.skipValue();
|
throw new IOException("Couldn't instantiate model class [" + type + "]", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.endObject();
|
reader.endObject();
|
||||||
|
|
||||||
try {
|
if(sites != null) {
|
||||||
Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()));
|
for (Site site : sites) {
|
||||||
Model model = (Model) modelClass.newInstance();
|
if(site.isSiteForModel(model)) {
|
||||||
model.setName(name);
|
model.setSite(site);
|
||||||
model.setDescription(description);
|
|
||||||
model.setUrl(url);
|
|
||||||
model.setStreamUrlIndex(streamUrlIndex);
|
|
||||||
if(sites != null) {
|
|
||||||
for (Site site : sites) {
|
|
||||||
if(site.isSiteForModel(model)) {
|
|
||||||
model.setSite(site);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return model;
|
|
||||||
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
|
|
||||||
throw new IOException("Couldn't instantiate model class [" + type + "]", e);
|
|
||||||
}
|
}
|
||||||
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -81,6 +87,10 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
||||||
writeValueIfSet(writer, "description", model.getDescription());
|
writeValueIfSet(writer, "description", model.getDescription());
|
||||||
writeValueIfSet(writer, "url", model.getUrl());
|
writeValueIfSet(writer, "url", model.getUrl());
|
||||||
writer.name("streamUrlIndex").value(model.getStreamUrlIndex());
|
writer.name("streamUrlIndex").value(model.getStreamUrlIndex());
|
||||||
|
writer.name("siteSpecific");
|
||||||
|
writer.beginObject();
|
||||||
|
model.writeSiteSpecificData(writer);
|
||||||
|
writer.endObject();
|
||||||
writer.endObject();
|
writer.endObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,16 @@ import java.io.InputStream;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.iheartradio.m3u8.Encoding;
|
import com.iheartradio.m3u8.Encoding;
|
||||||
import com.iheartradio.m3u8.Format;
|
import com.iheartradio.m3u8.Format;
|
||||||
import com.iheartradio.m3u8.ParseException;
|
import com.iheartradio.m3u8.ParseException;
|
||||||
|
@ -20,12 +26,16 @@ import com.iheartradio.m3u8.data.MediaPlaylist;
|
||||||
import com.iheartradio.m3u8.data.Playlist;
|
import com.iheartradio.m3u8.data.Playlist;
|
||||||
import com.iheartradio.m3u8.data.TrackData;
|
import com.iheartradio.m3u8.data.TrackData;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
import ctbrec.io.HttpClient;
|
import ctbrec.io.HttpClient;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
import okhttp3.Response;
|
import okhttp3.Response;
|
||||||
|
|
||||||
public abstract class AbstractHlsDownload implements Download {
|
public abstract class AbstractHlsDownload implements Download {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(AbstractHlsDownload.class);
|
||||||
|
|
||||||
ExecutorService downloadThreadPool = Executors.newFixedThreadPool(5);
|
ExecutorService downloadThreadPool = Executors.newFixedThreadPool(5);
|
||||||
HttpClient client;
|
HttpClient client;
|
||||||
volatile boolean running = false;
|
volatile boolean running = false;
|
||||||
|
@ -69,6 +79,34 @@ public abstract class AbstractHlsDownload implements Download {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||||
|
List<StreamSource> streamSources = model.getStreamSources();
|
||||||
|
String url = null;
|
||||||
|
if(model.getStreamUrlIndex() >= 0 && model.getStreamUrlIndex() < streamSources.size()) {
|
||||||
|
url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl();
|
||||||
|
} else {
|
||||||
|
Collections.sort(streamSources);
|
||||||
|
// filter out stream resolutions, which are too high
|
||||||
|
int maxRes = Config.getInstance().getSettings().maximumResolution;
|
||||||
|
if(maxRes > 0) {
|
||||||
|
for (Iterator<StreamSource> iterator = streamSources.iterator(); iterator.hasNext();) {
|
||||||
|
StreamSource streamSource = iterator.next();
|
||||||
|
if(streamSource.height > 0 && maxRes < streamSource.height) {
|
||||||
|
LOG.trace("Res too high {} > {}", streamSource.height, maxRes);
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(streamSources.isEmpty()) {
|
||||||
|
throw new ExecutionException(new RuntimeException("No stream left in playlist"));
|
||||||
|
} else {
|
||||||
|
url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isAlive() {
|
public boolean isAlive() {
|
||||||
return alive;
|
return alive;
|
||||||
|
|
|
@ -48,7 +48,7 @@ public class HlsDownload extends AbstractHlsDownload {
|
||||||
throw new IOException(model.getName() +"'s room is not public");
|
throw new IOException(model.getName() +"'s room is not public");
|
||||||
}
|
}
|
||||||
|
|
||||||
String segments = model.getSegmentPlaylistUrl();
|
String segments = getSegmentPlaylistUrl(model);
|
||||||
if(segments != null) {
|
if(segments != null) {
|
||||||
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
|
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
Files.createDirectories(downloadDir);
|
Files.createDirectories(downloadDir);
|
||||||
|
|
|
@ -101,7 +101,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
||||||
target = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-00000.ts"));
|
target = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-00000.ts"));
|
||||||
}
|
}
|
||||||
|
|
||||||
String segments = model.getSegmentPlaylistUrl();
|
String segments = getSegmentPlaylistUrl(model);
|
||||||
mergeThread = createMergeThread(target, null, true);
|
mergeThread = createMergeThread(target, null, true);
|
||||||
mergeThread.start();
|
mergeThread.start();
|
||||||
if(segments != null) {
|
if(segments != null) {
|
||||||
|
|
|
@ -21,6 +21,7 @@ import ctbrec.recorder.LocalRecorder;
|
||||||
import ctbrec.recorder.Recorder;
|
import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import ctbrec.sites.cam4.Cam4;
|
import ctbrec.sites.cam4.Cam4;
|
||||||
|
import ctbrec.sites.camsoda.Camsoda;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
import ctbrec.sites.chaturbate.Chaturbate;
|
||||||
import ctbrec.sites.mfc.MyFreeCams;
|
import ctbrec.sites.mfc.MyFreeCams;
|
||||||
|
|
||||||
|
@ -53,7 +54,9 @@ public class HttpServer {
|
||||||
}
|
}
|
||||||
recorder = new LocalRecorder(config);
|
recorder = new LocalRecorder(config);
|
||||||
for (Site site : sites) {
|
for (Site site : sites) {
|
||||||
site.init();
|
if(site.isEnabled()) {
|
||||||
|
site.init();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
startHttpServer();
|
startHttpServer();
|
||||||
}
|
}
|
||||||
|
@ -61,6 +64,7 @@ public class HttpServer {
|
||||||
private void createSites() {
|
private void createSites() {
|
||||||
sites.add(new Chaturbate());
|
sites.add(new Chaturbate());
|
||||||
sites.add(new MyFreeCams());
|
sites.add(new MyFreeCams());
|
||||||
|
sites.add(new Camsoda());
|
||||||
sites.add(new Cam4());
|
sites.add(new Cam4());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package ctbrec.sites.cam4;
|
package ctbrec.sites.cam4;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.CookieHandler;
|
import java.net.CookieHandler;
|
||||||
import java.net.CookieManager;
|
import java.net.CookieManager;
|
||||||
|
@ -13,6 +14,7 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
|
import ctbrec.OS;
|
||||||
import javafx.concurrent.Worker.State;
|
import javafx.concurrent.Worker.State;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.ProgressIndicator;
|
import javafx.scene.control.ProgressIndicator;
|
||||||
|
@ -71,24 +73,29 @@ public class Cam4LoginDialog {
|
||||||
});
|
});
|
||||||
webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> {
|
webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> {
|
||||||
if (newState == State.SUCCEEDED) {
|
if (newState == State.SUCCEEDED) {
|
||||||
String username = Config.getInstance().getSettings().cam4Username;
|
|
||||||
if (username != null && !username.trim().isEmpty()) {
|
|
||||||
webEngine.executeScript("$('input[name=username]').attr('value','" + username + "')");
|
|
||||||
}
|
|
||||||
String password = Config.getInstance().getSettings().cam4Password;
|
|
||||||
if (password != null && !password.trim().isEmpty()) {
|
|
||||||
webEngine.executeScript("$('input[name=password]').attr('value','" + password + "')");
|
|
||||||
}
|
|
||||||
webEngine.executeScript("$('div[class~=navbar]').css('display','none')");
|
|
||||||
webEngine.executeScript("$('div#footer').css('display','none')");
|
|
||||||
webEngine.executeScript("$('div#content').css('padding','0')");
|
|
||||||
veil.setVisible(false);
|
veil.setVisible(false);
|
||||||
p.setVisible(false);
|
p.setVisible(false);
|
||||||
|
try {
|
||||||
|
String username = Config.getInstance().getSettings().cam4Username;
|
||||||
|
if (username != null && !username.trim().isEmpty()) {
|
||||||
|
webEngine.executeScript("$('input[name=username]').attr('value','" + username + "')");
|
||||||
|
}
|
||||||
|
String password = Config.getInstance().getSettings().cam4Password;
|
||||||
|
if (password != null && !password.trim().isEmpty()) {
|
||||||
|
webEngine.executeScript("$('input[name=password]').attr('value','" + password + "')");
|
||||||
|
}
|
||||||
|
webEngine.executeScript("$('div[class~=navbar]').css('display','none')");
|
||||||
|
webEngine.executeScript("$('div#footer').css('display','none')");
|
||||||
|
webEngine.executeScript("$('div#content').css('padding','0')");
|
||||||
|
} catch(Exception e) {
|
||||||
|
LOG.warn("Couldn't auto fill username and password for Cam4", e);
|
||||||
|
}
|
||||||
} else if (newState == State.CANCELLED || newState == State.FAILED) {
|
} else if (newState == State.CANCELLED || newState == State.FAILED) {
|
||||||
veil.setVisible(false);
|
veil.setVisible(false);
|
||||||
p.setVisible(false);
|
p.setVisible(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine"));
|
||||||
webEngine.load(URL);
|
webEngine.load(URL);
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,171 @@
|
||||||
|
package ctbrec.sites.camsoda;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import ctbrec.recorder.Recorder;
|
||||||
|
import ctbrec.sites.AbstractSite;
|
||||||
|
import ctbrec.ui.DesktopIntergation;
|
||||||
|
import ctbrec.ui.SettingsTab;
|
||||||
|
import ctbrec.ui.TabProvider;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.PasswordField;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class Camsoda extends AbstractSite {
|
||||||
|
|
||||||
|
public static final String BASE_URI = "https://www.camsoda.com";
|
||||||
|
private Recorder recorder;
|
||||||
|
private HttpClient httpClient;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "CamSoda";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseUrl() {
|
||||||
|
return BASE_URI;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAffiliateLink() {
|
||||||
|
return BASE_URI;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRecorder(Recorder recorder) {
|
||||||
|
this.recorder = recorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TabProvider getTabProvider() {
|
||||||
|
return new CamsodaTabProvider(this, recorder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Model createModel(String name) {
|
||||||
|
CamsodaModel model = new CamsodaModel();
|
||||||
|
model.setName(name);
|
||||||
|
model.setUrl(getBaseUrl() + "/" + name);
|
||||||
|
model.setSite(this);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getTokenBalance() throws IOException {
|
||||||
|
if (!credentialsAvailable()) {
|
||||||
|
throw new IOException("Account settings not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = Config.getInstance().getSettings().camsodaUsername;
|
||||||
|
String url = BASE_URI + "/api/v1/user/" + username;
|
||||||
|
Request request = new Request.Builder().url(url).build();
|
||||||
|
Response response = getHttpClient().execute(request, true);
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
|
if(json.has("user")) {
|
||||||
|
JSONObject user = json.getJSONObject("user");
|
||||||
|
if(user.has("tokens")) {
|
||||||
|
return user.getInt("tokens");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IOException(response.code() + " " + response.message());
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Tokens not found in response");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBuyTokensLink() {
|
||||||
|
return getBaseUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void login() throws IOException {
|
||||||
|
if(credentialsAvailable()) {
|
||||||
|
getHttpClient().login();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpClient getHttpClient() {
|
||||||
|
if(httpClient == null) {
|
||||||
|
httpClient = new CamsodaHttpClient();
|
||||||
|
}
|
||||||
|
return httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() throws IOException {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
if(httpClient != null) {
|
||||||
|
httpClient.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsTips() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsFollow() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSiteForModel(Model m) {
|
||||||
|
return m instanceof CamsodaModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean credentialsAvailable() {
|
||||||
|
String username = Config.getInstance().getSettings().camsodaUsername;
|
||||||
|
return username != null && !username.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Node getConfigurationGui() {
|
||||||
|
GridPane layout = SettingsTab.createGridLayout();
|
||||||
|
layout.add(new Label("CamSoda User"), 0, 0);
|
||||||
|
TextField username = new TextField(Config.getInstance().getSettings().camsodaUsername);
|
||||||
|
username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().camsodaUsername = username.getText());
|
||||||
|
GridPane.setFillWidth(username, true);
|
||||||
|
GridPane.setHgrow(username, Priority.ALWAYS);
|
||||||
|
GridPane.setColumnSpan(username, 2);
|
||||||
|
layout.add(username, 1, 0);
|
||||||
|
|
||||||
|
layout.add(new Label("CamSoda Password"), 0, 1);
|
||||||
|
PasswordField password = new PasswordField();
|
||||||
|
password.setText(Config.getInstance().getSettings().camsodaPassword);
|
||||||
|
password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().camsodaPassword = password.getText());
|
||||||
|
GridPane.setFillWidth(password, true);
|
||||||
|
GridPane.setHgrow(password, Priority.ALWAYS);
|
||||||
|
GridPane.setColumnSpan(password, 2);
|
||||||
|
layout.add(password, 1, 1);
|
||||||
|
|
||||||
|
Button createAccount = new Button("Create new Account");
|
||||||
|
createAccount.setOnAction((e) -> DesktopIntergation.open(getAffiliateLink()));
|
||||||
|
layout.add(createAccount, 1, 2);
|
||||||
|
GridPane.setColumnSpan(createAccount, 2);
|
||||||
|
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
package ctbrec.sites.camsoda;
|
||||||
|
|
||||||
|
import ctbrec.ui.FollowedTab;
|
||||||
|
import ctbrec.ui.ThumbOverviewTab;
|
||||||
|
import javafx.concurrent.WorkerStateEvent;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.RadioButton;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.input.KeyCode;
|
||||||
|
import javafx.scene.input.KeyEvent;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
|
||||||
|
public class CamsodaFollowedTab extends ThumbOverviewTab implements FollowedTab {
|
||||||
|
private Label status;
|
||||||
|
boolean showOnline = true;
|
||||||
|
|
||||||
|
public CamsodaFollowedTab(String title, Camsoda camsoda) {
|
||||||
|
super(title, new CamsodaFollowedUpdateService(camsoda), camsoda);
|
||||||
|
status = new Label("Logging in...");
|
||||||
|
grid.getChildren().add(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createGui() {
|
||||||
|
super.createGui();
|
||||||
|
addOnlineOfflineSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addOnlineOfflineSelector() {
|
||||||
|
ToggleGroup group = new ToggleGroup();
|
||||||
|
RadioButton online = new RadioButton("online");
|
||||||
|
online.setToggleGroup(group);
|
||||||
|
RadioButton offline = new RadioButton("offline");
|
||||||
|
offline.setToggleGroup(group);
|
||||||
|
pagination.getChildren().add(online);
|
||||||
|
pagination.getChildren().add(offline);
|
||||||
|
HBox.setMargin(online, new Insets(5, 5, 5, 40));
|
||||||
|
HBox.setMargin(offline, new Insets(5, 5, 5, 5));
|
||||||
|
online.setSelected(true);
|
||||||
|
group.selectedToggleProperty().addListener((e) -> {
|
||||||
|
queue.clear();
|
||||||
|
((CamsodaFollowedUpdateService)updateService).showOnline(online.isSelected());
|
||||||
|
updateService.restart();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSuccess() {
|
||||||
|
grid.getChildren().remove(status);
|
||||||
|
super.onSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onFail(WorkerStateEvent event) {
|
||||||
|
String msg = "";
|
||||||
|
if (event.getSource().getException() != null) {
|
||||||
|
msg = ": " + event.getSource().getException().getMessage();
|
||||||
|
}
|
||||||
|
status.setText("Login failed" + msg);
|
||||||
|
super.onFail(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void selected() {
|
||||||
|
status.setText("Logging in...");
|
||||||
|
super.selected();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScene(Scene scene) {
|
||||||
|
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||||
|
if (this.isSelected()) {
|
||||||
|
if (event.getCode() == KeyCode.DELETE) {
|
||||||
|
follow(selectedThumbCells, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package ctbrec.sites.camsoda;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.ui.PaginatedScheduledService;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class CamsodaFollowedUpdateService extends PaginatedScheduledService {
|
||||||
|
private Camsoda camsoda;
|
||||||
|
private boolean showOnline = true;
|
||||||
|
|
||||||
|
public CamsodaFollowedUpdateService(Camsoda camsoda) {
|
||||||
|
this.camsoda = camsoda;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<List<Model>> createTask() {
|
||||||
|
return new Task<List<Model>>() {
|
||||||
|
@Override
|
||||||
|
public List<Model> call() throws IOException {
|
||||||
|
List<Model> models = new ArrayList<>();
|
||||||
|
String url = camsoda.getBaseUrl() + "/api/v1/user/current";
|
||||||
|
Request request = new Request.Builder().url(url).build();
|
||||||
|
Response response = camsoda.getHttpClient().execute(request, true);
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
|
if(json.has("status") && json.getBoolean("status")) {
|
||||||
|
JSONObject user = json.getJSONObject("user");
|
||||||
|
JSONArray following = user.getJSONArray("following");
|
||||||
|
for (int i = 0; i < following.length(); i++) {
|
||||||
|
JSONObject m = following.getJSONObject(i);
|
||||||
|
CamsodaModel model = (CamsodaModel) camsoda.createModel(m.getString("followname"));
|
||||||
|
boolean online = m.getInt("online") == 1;
|
||||||
|
model.setOnlineState(online ? "online" : "offline");
|
||||||
|
model.setPreview("https://md.camsoda.com/thumbs/" + model.getName() + ".jpg");
|
||||||
|
models.add(model);
|
||||||
|
}
|
||||||
|
return models.stream()
|
||||||
|
.filter((m) -> {
|
||||||
|
try {
|
||||||
|
return m.isOnline() == showOnline;
|
||||||
|
} catch (IOException | ExecutionException | InterruptedException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
} else {
|
||||||
|
response.close();
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int code = response.code();
|
||||||
|
response.close();
|
||||||
|
throw new IOException("HTTP status " + code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void showOnline(boolean online) {
|
||||||
|
this.showOnline = online;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,151 @@
|
||||||
|
package ctbrec.sites.camsoda;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.HttpCookie;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.jsoup.nodes.Element;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Config;
|
||||||
|
import ctbrec.io.HttpClient;
|
||||||
|
import ctbrec.sites.cam4.Cam4LoginDialog;
|
||||||
|
import ctbrec.ui.HtmlParser;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import okhttp3.Cookie;
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import okhttp3.HttpUrl;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class CamsodaHttpClient extends HttpClient {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaHttpClient.class);
|
||||||
|
private String csrfToken = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean login() throws IOException {
|
||||||
|
if(loggedIn) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = Camsoda.BASE_URI + "/api/v1/auth/login";
|
||||||
|
FormBody body = new FormBody.Builder()
|
||||||
|
.add("username", Config.getInstance().getSettings().camsodaUsername)
|
||||||
|
.add("password", Config.getInstance().getSettings().camsodaPassword)
|
||||||
|
.build();
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.post(body)
|
||||||
|
.build();
|
||||||
|
Response response = execute(request);
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
JSONObject resp = new JSONObject(response.body().string());
|
||||||
|
if(resp.has("error")) {
|
||||||
|
String error = resp.getString("error");
|
||||||
|
if (Objects.equals(error, "Please confirm that you are not a robot.")) {
|
||||||
|
//return loginWithDialog();
|
||||||
|
throw new IOException("CamSoda requested to solve a captcha. Please try again in a while (maybe 15 min).");
|
||||||
|
} else {
|
||||||
|
throw new IOException(resp.getString("error"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IOException(response.code() + " " + response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private boolean loginWithDialog() throws IOException {
|
||||||
|
BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
|
Runnable showDialog = () -> {
|
||||||
|
// login with javafx WebView
|
||||||
|
CamsodaLoginDialog loginDialog = new CamsodaLoginDialog();
|
||||||
|
|
||||||
|
// transfer cookies from WebView to OkHttp cookie jar
|
||||||
|
transferCookies(loginDialog);
|
||||||
|
|
||||||
|
try {
|
||||||
|
queue.put(true);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOG.error("Error while signaling termination", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if(Platform.isFxApplicationThread()) {
|
||||||
|
showDialog.run();
|
||||||
|
} else {
|
||||||
|
Platform.runLater(showDialog);
|
||||||
|
try {
|
||||||
|
queue.take();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
LOG.error("Error while waiting for login dialog to close", e);
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loggedIn = checkLoginSuccess();
|
||||||
|
return loggedIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check, if the login worked
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private boolean checkLoginSuccess() throws IOException {
|
||||||
|
String url = Camsoda.BASE_URI + "/api/v1/user/current";
|
||||||
|
Request request = new Request.Builder().url(url).build();
|
||||||
|
try(Response response = execute(request)) {
|
||||||
|
if(response.isSuccessful()) {
|
||||||
|
JSONObject resp = new JSONObject(response.body().string());
|
||||||
|
return resp.optBoolean("status");
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void transferCookies(CamsodaLoginDialog loginDialog) {
|
||||||
|
HttpUrl redirectedUrl = HttpUrl.parse(loginDialog.getUrl());
|
||||||
|
List<Cookie> cookies = new ArrayList<>();
|
||||||
|
for (HttpCookie webViewCookie : loginDialog.getCookies()) {
|
||||||
|
Cookie cookie = Cookie.parse(redirectedUrl, webViewCookie.toString());
|
||||||
|
cookies.add(cookie);
|
||||||
|
}
|
||||||
|
cookieJar.saveFromResponse(redirectedUrl, cookies);
|
||||||
|
|
||||||
|
HttpUrl origUrl = HttpUrl.parse(Cam4LoginDialog.URL);
|
||||||
|
cookies = new ArrayList<>();
|
||||||
|
for (HttpCookie webViewCookie : loginDialog.getCookies()) {
|
||||||
|
Cookie cookie = Cookie.parse(origUrl, webViewCookie.toString());
|
||||||
|
cookies.add(cookie);
|
||||||
|
}
|
||||||
|
cookieJar.saveFromResponse(origUrl, cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getCsrfToken() throws IOException {
|
||||||
|
if(csrfToken == null) {
|
||||||
|
String url = Camsoda.BASE_URI;
|
||||||
|
Request request = new Request.Builder().url(url).build();
|
||||||
|
Response resp = execute(request, true);
|
||||||
|
if(resp.isSuccessful()) {
|
||||||
|
Element meta = HtmlParser.getTag(resp.body().string(), "meta[name=\"_token\"]");
|
||||||
|
csrfToken = meta.attr("content");
|
||||||
|
} else {
|
||||||
|
IOException e = new IOException(resp.code() + " " + resp.message());
|
||||||
|
resp.close();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return csrfToken;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package ctbrec.sites.camsoda;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.CookieHandler;
|
||||||
|
import java.net.CookieManager;
|
||||||
|
import java.net.HttpCookie;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import ctbrec.OS;
|
||||||
|
import javafx.concurrent.Worker.State;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.ProgressIndicator;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.layout.Region;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.web.WebEngine;
|
||||||
|
import javafx.scene.web.WebView;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
// FIXME this dialog does not help, because google's recaptcha does not work
|
||||||
|
// with WebView even though it does work in Cam4LoginDialog
|
||||||
|
public class CamsodaLoginDialog {
|
||||||
|
|
||||||
|
public static final String URL = Camsoda.BASE_URI;
|
||||||
|
private List<HttpCookie> cookies = null;
|
||||||
|
private String url;
|
||||||
|
private Region veil;
|
||||||
|
private ProgressIndicator p;
|
||||||
|
|
||||||
|
public CamsodaLoginDialog() {
|
||||||
|
Stage stage = new Stage();
|
||||||
|
stage.setTitle("CamSoda Login");
|
||||||
|
InputStream icon = getClass().getResourceAsStream("/icon.png");
|
||||||
|
stage.getIcons().add(new Image(icon));
|
||||||
|
CookieManager cookieManager = new CookieManager();
|
||||||
|
CookieHandler.setDefault(cookieManager);
|
||||||
|
WebView webView = createWebView(stage);
|
||||||
|
|
||||||
|
veil = new Region();
|
||||||
|
veil.setStyle("-fx-background-color: rgba(1, 1, 1)");
|
||||||
|
p = new ProgressIndicator();
|
||||||
|
p.setMaxSize(140, 140);
|
||||||
|
|
||||||
|
p.setVisible(true);
|
||||||
|
veil.visibleProperty().bind(p.visibleProperty());
|
||||||
|
|
||||||
|
StackPane stackPane = new StackPane();
|
||||||
|
stackPane.getChildren().addAll(webView, veil, p);
|
||||||
|
|
||||||
|
stage.setScene(new Scene(stackPane, 400, 358));
|
||||||
|
stage.showAndWait();
|
||||||
|
cookies = cookieManager.getCookieStore().getCookies();
|
||||||
|
}
|
||||||
|
|
||||||
|
private WebView createWebView(Stage stage) {
|
||||||
|
WebView browser = new WebView();
|
||||||
|
WebEngine webEngine = browser.getEngine();
|
||||||
|
webEngine.setJavaScriptEnabled(true);
|
||||||
|
webEngine.locationProperty().addListener((obs, oldV, newV) -> {
|
||||||
|
// try {
|
||||||
|
// URL _url = new URL(newV);
|
||||||
|
// if (Objects.equals(_url.getPath(), "/")) {
|
||||||
|
// stage.close();
|
||||||
|
// }
|
||||||
|
// } catch (MalformedURLException e) {
|
||||||
|
// LOG.error("Couldn't parse new url {}", newV, e);
|
||||||
|
// }
|
||||||
|
url = newV.toString();
|
||||||
|
System.out.println(newV.toString());
|
||||||
|
});
|
||||||
|
webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> {
|
||||||
|
if (newState == State.SUCCEEDED) {
|
||||||
|
webEngine.executeScript("document.querySelector('a[ng-click=\"signin();\"]').click()");
|
||||||
|
p.setVisible(false);
|
||||||
|
|
||||||
|
// TODO make this work
|
||||||
|
// String username = Config.getInstance().getSettings().camsodaUsername;
|
||||||
|
// if (username != null && !username.trim().isEmpty()) {
|
||||||
|
// webEngine.executeScript("document.querySelector('input[name=\"loginUsername\"]').value = '" + username + "'");
|
||||||
|
// }
|
||||||
|
// String password = Config.getInstance().getSettings().camsodaPassword;
|
||||||
|
// if (password != null && !password.trim().isEmpty()) {
|
||||||
|
// webEngine.executeScript("document.querySelector('input[name=\"loginPassword\"]').value = '" + password + "'");
|
||||||
|
// }
|
||||||
|
} else if (newState == State.CANCELLED || newState == State.FAILED) {
|
||||||
|
p.setVisible(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
webEngine.setUserStyleSheetLocation("data:text/css;base64," + Base64.getEncoder().encodeToString(CUSTOM_STYLE.getBytes()));
|
||||||
|
webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine"));
|
||||||
|
webEngine.load(URL);
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<HttpCookie> getCookies() {
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String CUSTOM_STYLE = ""
|
||||||
|
+ ".ngdialog.ngdialog-theme-custom { padding: 0 !important }"
|
||||||
|
+ ".ngdialog-overlay { background: black !important; }";
|
||||||
|
}
|
|
@ -0,0 +1,270 @@
|
||||||
|
package ctbrec.sites.camsoda;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.google.common.cache.Cache;
|
||||||
|
import com.google.common.cache.CacheBuilder;
|
||||||
|
import com.iheartradio.m3u8.Encoding;
|
||||||
|
import com.iheartradio.m3u8.Format;
|
||||||
|
import com.iheartradio.m3u8.ParseException;
|
||||||
|
import com.iheartradio.m3u8.PlaylistException;
|
||||||
|
import com.iheartradio.m3u8.PlaylistParser;
|
||||||
|
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||||
|
import com.iheartradio.m3u8.data.Playlist;
|
||||||
|
import com.iheartradio.m3u8.data.PlaylistData;
|
||||||
|
import com.iheartradio.m3u8.data.StreamInfo;
|
||||||
|
|
||||||
|
import ctbrec.AbstractModel;
|
||||||
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
import ctbrec.sites.Site;
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class CamsodaModel extends AbstractModel {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaModel.class);
|
||||||
|
private String streamUrl;
|
||||||
|
private Site site;
|
||||||
|
private List<StreamSource> streamSources = null;
|
||||||
|
private String status = "n/a";
|
||||||
|
private float sortOrder = 0;
|
||||||
|
|
||||||
|
private static Cache<String, int[]> streamResolutionCache = CacheBuilder.newBuilder()
|
||||||
|
.initialCapacity(10_000)
|
||||||
|
.maximumSize(10_000)
|
||||||
|
.expireAfterWrite(30, TimeUnit.MINUTES)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public String getStreamUrl() throws IOException {
|
||||||
|
if(streamUrl == null) {
|
||||||
|
// load model
|
||||||
|
loadModel();
|
||||||
|
}
|
||||||
|
return streamUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadModel() throws IOException {
|
||||||
|
String modelUrl = site.getBaseUrl() + "/api/v1/user/" + getName();
|
||||||
|
Request req = new Request.Builder().url(modelUrl).build();
|
||||||
|
Response response = site.getHttpClient().execute(req);
|
||||||
|
try {
|
||||||
|
JSONObject result = new JSONObject(response.body().string());
|
||||||
|
if(result.getBoolean("status")) {
|
||||||
|
JSONObject chat = result.getJSONObject("user").getJSONObject("chat");
|
||||||
|
status = chat.getString("status");
|
||||||
|
if(chat.has("edge_servers")) {
|
||||||
|
String edgeServer = chat.getJSONArray("edge_servers").getString(0);
|
||||||
|
String streamName = chat.getString("stream_name");
|
||||||
|
streamUrl = "https://" + edgeServer + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8";
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new IOException("Result was not ok");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||||
|
if(ignoreCache) {
|
||||||
|
loadModel();
|
||||||
|
}
|
||||||
|
return Objects.equals(status, "online");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||||
|
if(failFast) {
|
||||||
|
return status;
|
||||||
|
} else {
|
||||||
|
if(status.equals("n/a")) {
|
||||||
|
loadModel();
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnlineState(String state) {
|
||||||
|
this.status = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||||
|
String streamUrl = getStreamUrl();
|
||||||
|
if(streamUrl == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
Request req = new Request.Builder().url(streamUrl).build();
|
||||||
|
Response response = site.getHttpClient().execute(req);
|
||||||
|
try {
|
||||||
|
InputStream inputStream = response.body().byteStream();
|
||||||
|
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
|
||||||
|
Playlist playlist = parser.parse();
|
||||||
|
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||||
|
PlaylistData playlistData = master.getPlaylists().get(0);
|
||||||
|
StreamSource streamsource = new StreamSource();
|
||||||
|
streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", playlistData.getUri());
|
||||||
|
if(playlistData.hasStreamInfo()) {
|
||||||
|
StreamInfo info = playlistData.getStreamInfo();
|
||||||
|
streamsource.bandwidth = info.getBandwidth();
|
||||||
|
streamsource.width = info.hasResolution() ? info.getResolution().width : 0;
|
||||||
|
streamsource.height = info.hasResolution() ? info.getResolution().height : 0;
|
||||||
|
} else {
|
||||||
|
streamsource.bandwidth = 0;
|
||||||
|
streamsource.width = 0;
|
||||||
|
streamsource.height = 0;
|
||||||
|
}
|
||||||
|
streamSources = Collections.singletonList(streamsource);
|
||||||
|
} finally {
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
return streamSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateCacheEntries() {
|
||||||
|
streamSources = null;
|
||||||
|
streamResolutionCache.invalidate(getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
|
||||||
|
int[] resolution = streamResolutionCache.getIfPresent(getName());
|
||||||
|
if(resolution != null) {
|
||||||
|
return resolution;
|
||||||
|
} else {
|
||||||
|
if(failFast) {
|
||||||
|
return new int[] {0,0};
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
List<StreamSource> streamSources = getStreamSources();
|
||||||
|
if(streamSources.isEmpty()) {
|
||||||
|
return new int[] {0,0};
|
||||||
|
} else {
|
||||||
|
StreamSource src = streamSources.get(0);
|
||||||
|
resolution = new int[] {src.width, src.height};
|
||||||
|
streamResolutionCache.put(getName(), resolution);
|
||||||
|
return resolution;
|
||||||
|
}
|
||||||
|
} catch (IOException | ParseException | PlaylistException e) {
|
||||||
|
throw new ExecutionException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void receiveTip(int tokens) throws IOException {
|
||||||
|
String csrfToken = ((CamsodaHttpClient)site.getHttpClient()).getCsrfToken();
|
||||||
|
String url = site.getBaseUrl() + "/api/v1/tip/" + getName();
|
||||||
|
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||||
|
LOG.debug("Sending tip {}", url);
|
||||||
|
RequestBody body = new FormBody.Builder()
|
||||||
|
.add("amount", Integer.toString(tokens))
|
||||||
|
.add("comment", "")
|
||||||
|
.build();
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.post(body)
|
||||||
|
.addHeader("Referer", Camsoda.BASE_URI + '/' + getName())
|
||||||
|
.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0")
|
||||||
|
.addHeader("Accept", "application/json, text/plain, */*")
|
||||||
|
.addHeader("Accept-Language", "en")
|
||||||
|
.addHeader("X-CSRF-Token", csrfToken)
|
||||||
|
.build();
|
||||||
|
try(Response response = site.getHttpClient().execute(request, true)) {
|
||||||
|
if(!response.isSuccessful()) {
|
||||||
|
throw new IOException("HTTP status " + response.code() + " " + response.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean follow() throws IOException {
|
||||||
|
String url = Camsoda.BASE_URI + "/api/v1/follow/" + getName();
|
||||||
|
LOG.debug("Sending follow request {}", url);
|
||||||
|
String csrfToken = ((CamsodaHttpClient)site.getHttpClient()).getCsrfToken();
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.post(RequestBody.create(null, ""))
|
||||||
|
.addHeader("Referer", Camsoda.BASE_URI + '/' + getName())
|
||||||
|
.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0")
|
||||||
|
.addHeader("Accept", "application/json, text/plain, */*")
|
||||||
|
.addHeader("Accept-Language", "en")
|
||||||
|
.addHeader("X-CSRF-Token", csrfToken)
|
||||||
|
.build();
|
||||||
|
Response resp = site.getHttpClient().execute(request, true);
|
||||||
|
if (resp.isSuccessful()) {
|
||||||
|
resp.close();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
resp.close();
|
||||||
|
throw new IOException("HTTP status " + resp.code() + " " + resp.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean unfollow() throws IOException {
|
||||||
|
String url = Camsoda.BASE_URI + "/api/v1/unfollow/" + getName();
|
||||||
|
LOG.debug("Sending follow request {}", url);
|
||||||
|
String csrfToken = ((CamsodaHttpClient)site.getHttpClient()).getCsrfToken();
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.post(RequestBody.create(null, ""))
|
||||||
|
.addHeader("Referer", Camsoda.BASE_URI + '/' + getName())
|
||||||
|
.addHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:62.0) Gecko/20100101 Firefox/62.0")
|
||||||
|
.addHeader("Accept", "application/json, text/plain, */*")
|
||||||
|
.addHeader("Accept-Language", "en")
|
||||||
|
.addHeader("X-CSRF-Token", csrfToken)
|
||||||
|
.build();
|
||||||
|
Response resp = site.getHttpClient().execute(request, true);
|
||||||
|
if (resp.isSuccessful()) {
|
||||||
|
resp.close();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
resp.close();
|
||||||
|
throw new IOException("HTTP status " + resp.code() + " " + resp.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSite(Site site) {
|
||||||
|
if(site instanceof Camsoda) {
|
||||||
|
this.site = site;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Site has to be an instance of Camsoda");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Site getSite() {
|
||||||
|
return site;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStreamUrl(String streamUrl) {
|
||||||
|
this.streamUrl = streamUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getSortOrder() {
|
||||||
|
return sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSortOrder(float sortOrder) {
|
||||||
|
this.sortOrder = sortOrder;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,286 @@
|
||||||
|
package ctbrec.sites.camsoda;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.FormatStyle;
|
||||||
|
import java.time.temporal.TemporalAccessor;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.recorder.Recorder;
|
||||||
|
import ctbrec.ui.AutosizeAlert;
|
||||||
|
import ctbrec.ui.DesktopIntergation;
|
||||||
|
import ctbrec.ui.TabSelectionListener;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.scene.Cursor;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ProgressIndicator;
|
||||||
|
import javafx.scene.control.ScrollPane;
|
||||||
|
import javafx.scene.control.Tab;
|
||||||
|
import javafx.scene.control.TitledPane;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.text.Font;
|
||||||
|
import javafx.scene.text.FontWeight;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class CamsodaShowsTab extends Tab implements TabSelectionListener {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaShowsTab.class);
|
||||||
|
|
||||||
|
private Camsoda camsoda;
|
||||||
|
private Recorder recorder;
|
||||||
|
private GridPane showList;
|
||||||
|
private ProgressIndicator progressIndicator;
|
||||||
|
|
||||||
|
public CamsodaShowsTab(Camsoda camsoda, Recorder recorder) {
|
||||||
|
this.camsoda = camsoda;
|
||||||
|
this.recorder = recorder;
|
||||||
|
createGui();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createGui() {
|
||||||
|
showList = new GridPane();
|
||||||
|
showList.setPadding(new Insets(5));
|
||||||
|
showList.setHgap(5);
|
||||||
|
showList.setVgap(5);
|
||||||
|
progressIndicator = new ProgressIndicator();
|
||||||
|
progressIndicator.setPrefSize(100, 100);
|
||||||
|
setContent(progressIndicator);
|
||||||
|
setClosable(false);
|
||||||
|
setText("Shows");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void selected() {
|
||||||
|
Task<List<ShowBox>> task = new Task<List<ShowBox>>() {
|
||||||
|
@Override
|
||||||
|
protected List<ShowBox> call() throws Exception {
|
||||||
|
String url = camsoda.getBaseUrl() + "/api/v1/user/model_shows";
|
||||||
|
Request req = new Request.Builder().url(url).build();
|
||||||
|
Response response = camsoda.getHttpClient().execute(req);
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
|
if (json.optInt("success") == 1) {
|
||||||
|
List<ShowBox> boxes = new ArrayList<>();
|
||||||
|
JSONArray results = json.getJSONArray("results");
|
||||||
|
for (int i = 0; i < results.length(); i++) {
|
||||||
|
JSONObject result = results.getJSONObject(i);
|
||||||
|
String modelUrl = camsoda.getBaseUrl() + result.getString("url");
|
||||||
|
String name = modelUrl.substring(modelUrl.lastIndexOf('/') + 1);
|
||||||
|
Model model = camsoda.createModel(name);
|
||||||
|
ZonedDateTime startTime = parseUtcTime(result.getString("start"));
|
||||||
|
ZonedDateTime endTime = parseUtcTime(result.getString("end"));
|
||||||
|
boxes.add(new ShowBox(model, startTime, endTime));
|
||||||
|
}
|
||||||
|
return boxes;
|
||||||
|
} else {
|
||||||
|
LOG.error("Couldn't load upcoming camsoda shows. Unexpected response: {}", json.toString());
|
||||||
|
showErrorDialog("Oh no!", "Couldn't load upcoming CamSoda shows", "Got an unexpected response from server");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.close();
|
||||||
|
showErrorDialog("Oh no!", "Couldn't load upcoming CamSoda shows", "Got an unexpected response from server");
|
||||||
|
LOG.error("Couldn't load upcoming camsoda shows: {} {}", response.code(), response.message());
|
||||||
|
}
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ZonedDateTime parseUtcTime(String string) {
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
|
||||||
|
TemporalAccessor ta = formatter.parse(string.replace(" UTC", ""));
|
||||||
|
Instant instant = Instant.from(ta);
|
||||||
|
return ZonedDateTime.ofInstant(instant, ZoneId.systemDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void done() {
|
||||||
|
super.done();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
try {
|
||||||
|
List<ShowBox> boxes = get();
|
||||||
|
showList.getChildren().clear();
|
||||||
|
int index = 0;
|
||||||
|
for (ShowBox showBox : boxes) {
|
||||||
|
showList.add(showBox, index % 2, index++ / 2);
|
||||||
|
GridPane.setMargin(showBox, new Insets(20, 20, 0, 20));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Couldn't load upcoming camsoda shows", e);
|
||||||
|
}
|
||||||
|
setContent(new ScrollPane(showList));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
new Thread(task).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deselected() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showErrorDialog(String title, String head, String msg) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||||
|
alert.setTitle(title);
|
||||||
|
alert.setHeaderText(head);
|
||||||
|
alert.setContentText(msg);
|
||||||
|
alert.showAndWait();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ShowBox extends TitledPane {
|
||||||
|
|
||||||
|
BorderPane root = new BorderPane();
|
||||||
|
int thumbSize = 200;
|
||||||
|
|
||||||
|
public ShowBox(Model model, ZonedDateTime startTime, ZonedDateTime endTime) {
|
||||||
|
setText(model.getName());
|
||||||
|
setPrefHeight(268);
|
||||||
|
setContent(root);
|
||||||
|
|
||||||
|
ImageView thumb = new ImageView();
|
||||||
|
thumb.setPreserveRatio(true);
|
||||||
|
thumb.setFitHeight(thumbSize);
|
||||||
|
loadImage(model, thumb);
|
||||||
|
root.setLeft(new ProgressIndicator());
|
||||||
|
BorderPane.setMargin(thumb, new Insets(10, 30, 10, 10));
|
||||||
|
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
|
||||||
|
GridPane grid = new GridPane();
|
||||||
|
|
||||||
|
grid.add(createLabel("Start", true), 0, 0);
|
||||||
|
grid.add(createLabel(formatter.format(startTime), false), 1, 0);
|
||||||
|
grid.add(createLabel("End", true), 0, 1);
|
||||||
|
grid.add(createLabel(formatter.format(endTime), false), 1, 1);
|
||||||
|
Button record = new Button("Record Model");
|
||||||
|
record.setTooltip(new Tooltip(record.getText()));
|
||||||
|
record.setOnAction((evt) -> record(model));
|
||||||
|
grid.add(record, 1, 2);
|
||||||
|
GridPane.setMargin(record, new Insets(10));
|
||||||
|
Button follow = new Button("Follow");
|
||||||
|
follow.setTooltip(new Tooltip(follow.getText()));
|
||||||
|
follow.setOnAction((evt) -> follow(model));
|
||||||
|
grid.add(follow, 1, 3);
|
||||||
|
GridPane.setMargin(follow, new Insets(10));
|
||||||
|
Button openInBrowser = new Button("Open in Browser");
|
||||||
|
openInBrowser.setTooltip(new Tooltip(openInBrowser.getText()));
|
||||||
|
openInBrowser.setOnAction((evt) -> DesktopIntergation.open(model.getUrl()));
|
||||||
|
grid.add(openInBrowser, 1, 4);
|
||||||
|
GridPane.setMargin(openInBrowser, new Insets(10));
|
||||||
|
root.setCenter(grid);
|
||||||
|
loadImage(model, thumb);
|
||||||
|
|
||||||
|
record.prefWidthProperty().bind(openInBrowser.widthProperty());
|
||||||
|
follow.prefWidthProperty().bind(openInBrowser.widthProperty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void follow(Model model) {
|
||||||
|
setCursor(Cursor.WAIT);
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
model.follow();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Couldn't follow model {}", model, e);
|
||||||
|
showErrorDialog("Oh no!", "Couldn't follow model", e.getMessage());
|
||||||
|
} finally {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
setCursor(Cursor.DEFAULT);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void record(Model model) {
|
||||||
|
setCursor(Cursor.WAIT);
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
recorder.startRecording(model);
|
||||||
|
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
|
||||||
|
showErrorDialog("Oh no!", "Couldn't add model to the recorder", "Recorder error: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
setCursor(Cursor.DEFAULT);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadImage(Model model, ImageView thumb) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
String url = camsoda.getBaseUrl() + "/api/v1/user/" + model.getName();
|
||||||
|
Request detailRequest = new Request.Builder().url(url).build();
|
||||||
|
Response resp = camsoda.getHttpClient().execute(detailRequest);
|
||||||
|
if (resp.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(resp.body().string());
|
||||||
|
if (json.optBoolean("status") && json.has("user")) {
|
||||||
|
JSONObject user = json.getJSONObject("user");
|
||||||
|
if (user.has("settings")) {
|
||||||
|
JSONObject settings = user.getJSONObject("settings");
|
||||||
|
String imageUrl;
|
||||||
|
if(Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||||
|
imageUrl = getClass().getResource("/image_not_found.png").toString();
|
||||||
|
} else {
|
||||||
|
if (settings.has("offline_picture")) {
|
||||||
|
imageUrl = settings.getString("offline_picture");
|
||||||
|
} else {
|
||||||
|
imageUrl = "https:" + user.getString("thumb");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
Image img = new Image(imageUrl, 1000, thumbSize, true, true, true);
|
||||||
|
img.progressProperty().addListener(new ChangeListener<Number>() {
|
||||||
|
@Override
|
||||||
|
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
|
||||||
|
if (newValue.doubleValue() == 1.0) {
|
||||||
|
thumb.setImage(img);
|
||||||
|
root.setLeft(thumb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Couldn't load model details", e);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node createLabel(String string, boolean bold) {
|
||||||
|
Label label = new Label(string);
|
||||||
|
label.setPadding(new Insets(10));
|
||||||
|
Font def = Font.getDefault();
|
||||||
|
label.setFont(Font.font(def.getFamily(), bold ? FontWeight.BOLD : FontWeight.NORMAL, 16));
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package ctbrec.sites.camsoda;
|
||||||
|
|
||||||
|
import static ctbrec.sites.camsoda.Camsoda.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import ctbrec.recorder.Recorder;
|
||||||
|
import ctbrec.ui.TabProvider;
|
||||||
|
import ctbrec.ui.ThumbOverviewTab;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Tab;
|
||||||
|
|
||||||
|
public class CamsodaTabProvider extends TabProvider {
|
||||||
|
|
||||||
|
private Camsoda camsoda;
|
||||||
|
private Recorder recorder;
|
||||||
|
|
||||||
|
public CamsodaTabProvider(Camsoda camsoda, Recorder recorder) {
|
||||||
|
this.camsoda = camsoda;
|
||||||
|
this.recorder = recorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Tab> getTabs(Scene scene) {
|
||||||
|
List<Tab> tabs = new ArrayList<>();
|
||||||
|
tabs.add(createTab("Online", BASE_URI + "/api/v1/browse/online"));
|
||||||
|
CamsodaFollowedTab followedTab = new CamsodaFollowedTab("Followed", camsoda);
|
||||||
|
followedTab.setRecorder(recorder);
|
||||||
|
followedTab.setScene(scene);
|
||||||
|
tabs.add(followedTab);
|
||||||
|
tabs.add(new CamsodaShowsTab(camsoda, recorder));
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tab createTab(String title, String url) {
|
||||||
|
CamsodaUpdateService updateService = new CamsodaUpdateService(url, false, camsoda);
|
||||||
|
ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, camsoda);
|
||||||
|
tab.setRecorder(recorder);
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
package ctbrec.sites.camsoda;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.util.StringUtil;
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ctbrec.Model;
|
||||||
|
import ctbrec.ui.PaginatedScheduledService;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
|
||||||
|
public class CamsodaUpdateService extends PaginatedScheduledService {
|
||||||
|
|
||||||
|
private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaUpdateService.class);
|
||||||
|
|
||||||
|
private String url;
|
||||||
|
private boolean loginRequired;
|
||||||
|
private Camsoda camsoda;
|
||||||
|
int modelsPerPage = 50;
|
||||||
|
|
||||||
|
public CamsodaUpdateService(String url, boolean loginRequired, Camsoda camsoda) {
|
||||||
|
this.url = url;
|
||||||
|
this.loginRequired = loginRequired;
|
||||||
|
this.camsoda = camsoda;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<List<Model>> createTask() {
|
||||||
|
return new Task<List<Model>>() {
|
||||||
|
@Override
|
||||||
|
public List<Model> call() throws IOException {
|
||||||
|
List<CamsodaModel> models = new ArrayList<>();
|
||||||
|
if(loginRequired && StringUtil.isBlank(ctbrec.Config.getInstance().getSettings().username)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
} else {
|
||||||
|
String url = CamsodaUpdateService.this.url;
|
||||||
|
LOG.debug("Fetching page {}", url);
|
||||||
|
Request request = new Request.Builder().url(url).build();
|
||||||
|
Response response = camsoda.getHttpClient().execute(request, loginRequired);
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
JSONObject json = new JSONObject(response.body().string());
|
||||||
|
if(json.has("status") && json.getBoolean("status")) {
|
||||||
|
JSONArray results = json.getJSONArray("results");
|
||||||
|
for (int i = 0; i < results.length(); i++) {
|
||||||
|
JSONObject result = results.getJSONObject(i);
|
||||||
|
if(result.has("tpl")) {
|
||||||
|
JSONArray tpl = result.getJSONArray("tpl");
|
||||||
|
String name = tpl.getString(0);
|
||||||
|
// int connections = tpl.getInt(2);
|
||||||
|
String streamName = tpl.getString(5);
|
||||||
|
String tsize = tpl.getString(6);
|
||||||
|
String serverPrefix = tpl.getString(7);
|
||||||
|
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
|
||||||
|
model.setDescription(tpl.getString(4));
|
||||||
|
model.setSortOrder(tpl.getFloat(3));
|
||||||
|
long unixtime = System.currentTimeMillis() / 1000;
|
||||||
|
String preview = "https://thumbs-orig.camsoda.com/thumbs/"
|
||||||
|
+ streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime;
|
||||||
|
model.setPreview(preview);
|
||||||
|
if(result.has("edge_servers")) {
|
||||||
|
JSONArray edgeServers = result.getJSONArray("edge_servers");
|
||||||
|
model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8");
|
||||||
|
}
|
||||||
|
models.add(model);
|
||||||
|
} else {
|
||||||
|
//LOG.debug("{}", result.toString(2));
|
||||||
|
String name = result.getString("username");
|
||||||
|
CamsodaModel model = (CamsodaModel) camsoda.createModel(name);
|
||||||
|
|
||||||
|
|
||||||
|
if(result.has("server_prefix")) {
|
||||||
|
String serverPrefix = result.getString("server_prefix");
|
||||||
|
String streamName = result.getString("stream_name");
|
||||||
|
model.setSortOrder(result.getFloat("sort_value"));
|
||||||
|
models.add(model);
|
||||||
|
if(result.has("status")) {
|
||||||
|
model.setOnlineState(result.getString("status"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(result.has("edge_servers")) {
|
||||||
|
JSONArray edgeServers = result.getJSONArray("edge_servers");
|
||||||
|
model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(result.has("tsize")) {
|
||||||
|
long unixtime = System.currentTimeMillis() / 1000;
|
||||||
|
String tsize = result.getString("tsize");
|
||||||
|
String preview = "https://thumbs-orig.camsoda.com/thumbs/"
|
||||||
|
+ streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime;
|
||||||
|
model.setPreview(preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return models.stream()
|
||||||
|
.sorted((m1,m2) -> (int)(m2.getSortOrder() - m1.getSortOrder()))
|
||||||
|
.skip( (page-1) * modelsPerPage)
|
||||||
|
.limit(modelsPerPage)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} else {
|
||||||
|
response.close();
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int code = response.code();
|
||||||
|
response.close();
|
||||||
|
throw new IOException("HTTP status " + code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -38,11 +38,6 @@ public class ChaturbateModel extends AbstractModel {
|
||||||
this.site = site;
|
this.site = site;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
|
||||||
return isOnline(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||||
StreamInfo info;
|
StreamInfo info;
|
||||||
|
|
|
@ -55,7 +55,7 @@ public class MyFreeCams extends AbstractSite {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getAffiliateLink() {
|
public String getAffiliateLink() {
|
||||||
return "";
|
return BASE_URI + "/?baf=8127165";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -93,7 +93,7 @@ public class MyFreeCams extends AbstractSite {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getBuyTokensLink() {
|
public String getBuyTokensLink() {
|
||||||
return "https://www.myfreecams.com/php/purchase.php?request=tokens";
|
return BASE_URI + "/php/purchase.php?request=tokens";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -149,7 +149,7 @@ public class MyFreeCams extends AbstractSite {
|
||||||
layout.add(password, 1, 1);
|
layout.add(password, 1, 1);
|
||||||
|
|
||||||
Button createAccount = new Button("Create new Account");
|
Button createAccount = new Button("Create new Account");
|
||||||
createAccount.setOnAction((e) -> DesktopIntergation.open(BASE_URI + "/php/signup.php?request=register"));
|
createAccount.setOnAction((e) -> DesktopIntergation.open(getAffiliateLink()));
|
||||||
layout.add(createAccount, 1, 2);
|
layout.add(createAccount, 1, 2);
|
||||||
GridPane.setColumnSpan(createAccount, 2);
|
GridPane.setColumnSpan(createAccount, 2);
|
||||||
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||||
|
|
|
@ -57,6 +57,7 @@ public class MyFreeCamsClient {
|
||||||
private String ctxenc;
|
private String ctxenc;
|
||||||
private String chatToken;
|
private String chatToken;
|
||||||
private int sessionId;
|
private int sessionId;
|
||||||
|
private long heartBeat;
|
||||||
|
|
||||||
private EvictingQueue<String> receivedTextHistory = EvictingQueue.create(10000);
|
private EvictingQueue<String> receivedTextHistory = EvictingQueue.create(10000);
|
||||||
|
|
||||||
|
@ -135,6 +136,7 @@ public class MyFreeCamsClient {
|
||||||
// TODO find out, what the values in the json message mean, at the moment we hust send 0s, which seems to work, too
|
// TODO find out, what the values in the json message mean, at the moment we hust send 0s, which seems to work, too
|
||||||
// webSocket.send("1 0 0 81 0 %7B%22err%22%3A0%2C%22start%22%3A1540159843072%2C%22stop%22%3A1540159844121%2C%22a%22%3A6392%2C%22time%22%3A1540159844%2C%22key%22%3A%228da80f985c9db390809713dac71df297%22%2C%22cid%22%3A%22c504d684%22%2C%22pid%22%3A1%2C%22site%22%3A%22www%22%7D\n");
|
// webSocket.send("1 0 0 81 0 %7B%22err%22%3A0%2C%22start%22%3A1540159843072%2C%22stop%22%3A1540159844121%2C%22a%22%3A6392%2C%22time%22%3A1540159844%2C%22key%22%3A%228da80f985c9db390809713dac71df297%22%2C%22cid%22%3A%22c504d684%22%2C%22pid%22%3A1%2C%22site%22%3A%22www%22%7D\n");
|
||||||
webSocket.send("1 0 0 81 0 %7B%22err%22%3A0%2C%22start%22%3A0%2C%22stop%22%3A0%2C%22a%22%3A0%2C%22time%22%3A0%2C%22key%22%3A%22%22%2C%22cid%22%3A%22%22%2C%22pid%22%3A1%2C%22site%22%3A%22www%22%7D\n");
|
webSocket.send("1 0 0 81 0 %7B%22err%22%3A0%2C%22start%22%3A0%2C%22stop%22%3A0%2C%22a%22%3A0%2C%22time%22%3A0%2C%22key%22%3A%22%22%2C%22cid%22%3A%22%22%2C%22pid%22%3A1%2C%22site%22%3A%22www%22%7D\n");
|
||||||
|
heartBeat = System.currentTimeMillis();
|
||||||
startKeepAlive(webSocket);
|
startKeepAlive(webSocket);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
@ -165,6 +167,7 @@ public class MyFreeCamsClient {
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(WebSocket webSocket, String text) {
|
public void onMessage(WebSocket webSocket, String text) {
|
||||||
super.onMessage(webSocket, text);
|
super.onMessage(webSocket, text);
|
||||||
|
heartBeat = System.currentTimeMillis();
|
||||||
receivedTextHistory.add(text);
|
receivedTextHistory.add(text);
|
||||||
msgBuffer.append(text);
|
msgBuffer.append(text);
|
||||||
Message message;
|
Message message;
|
||||||
|
@ -469,6 +472,14 @@ public class MyFreeCamsClient {
|
||||||
LOG.trace("--> NULL to keep the connection alive");
|
LOG.trace("--> NULL to keep the connection alive");
|
||||||
try {
|
try {
|
||||||
ws.send("0 0 0 0 0 -\n");
|
ws.send("0 0 0 0 0 -\n");
|
||||||
|
|
||||||
|
long millisSinceLastMessage = System.currentTimeMillis() - heartBeat;
|
||||||
|
if(millisSinceLastMessage > TimeUnit.MINUTES.toMillis(2)) {
|
||||||
|
LOG.info("No message since 2 mins. Restarting websocket");
|
||||||
|
ws.close(1000, "");
|
||||||
|
MyFreeCamsClient.this.ws = null;
|
||||||
|
}
|
||||||
|
|
||||||
Thread.sleep(TimeUnit.SECONDS.toMillis(15));
|
Thread.sleep(TimeUnit.SECONDS.toMillis(15));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
@ -484,7 +495,7 @@ public class MyFreeCamsClient {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
for (SessionState state : sessionStates.values()) {
|
for (SessionState state : sessionStates.values()) {
|
||||||
if(Objects.equals(state.getNm(), model.getName())) {
|
if(Objects.equals(state.getNm(), model.getName()) || Objects.equals(model.getUid(), state.getUid())) {
|
||||||
model.update(state, getStreamUrl(state));
|
model.update(state, getStreamUrl(state));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,8 @@ import com.iheartradio.m3u8.PlaylistParser;
|
||||||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||||
import com.iheartradio.m3u8.data.Playlist;
|
import com.iheartradio.m3u8.data.Playlist;
|
||||||
import com.iheartradio.m3u8.data.PlaylistData;
|
import com.iheartradio.m3u8.data.PlaylistData;
|
||||||
|
import com.squareup.moshi.JsonReader;
|
||||||
|
import com.squareup.moshi.JsonWriter;
|
||||||
|
|
||||||
import ctbrec.AbstractModel;
|
import ctbrec.AbstractModel;
|
||||||
import ctbrec.recorder.download.StreamSource;
|
import ctbrec.recorder.download.StreamSource;
|
||||||
|
@ -37,7 +39,7 @@ public class MyFreeCamsModel extends AbstractModel {
|
||||||
|
|
||||||
private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsModel.class);
|
private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsModel.class);
|
||||||
|
|
||||||
private int uid;
|
private int uid = -1; // undefined
|
||||||
private String hlsUrl;
|
private String hlsUrl;
|
||||||
private double camScore;
|
private double camScore;
|
||||||
private int viewerCount;
|
private int viewerCount;
|
||||||
|
@ -207,7 +209,17 @@ public class MyFreeCamsModel extends AbstractModel {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setName(String name) {
|
||||||
|
if(getName() != null && name != null && !getName().equals(name)) {
|
||||||
|
LOG.debug("Model name changed {} -> {}", getName(), name);
|
||||||
|
}
|
||||||
|
super.setName(name);
|
||||||
|
}
|
||||||
|
|
||||||
public void update(SessionState state, String streamUrl) {
|
public void update(SessionState state, String streamUrl) {
|
||||||
|
uid = Integer.parseInt(state.getUid().toString());
|
||||||
|
setName(state.getNm());
|
||||||
setCamScore(state.getM().getCamscore());
|
setCamScore(state.getM().getCamscore());
|
||||||
setState(State.of(state.getVs()));
|
setState(State.of(state.getVs()));
|
||||||
setStreamUrl(streamUrl);
|
setStreamUrl(streamUrl);
|
||||||
|
@ -308,4 +320,15 @@ public class MyFreeCamsModel extends AbstractModel {
|
||||||
public Site getSite() {
|
public Site getSite() {
|
||||||
return site;
|
return site;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readSiteSpecificData(JsonReader reader) throws IOException {
|
||||||
|
reader.nextName();
|
||||||
|
uid = reader.nextInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
|
||||||
|
writer.name("uid").value(uid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.recorder.RemoteRecorder;
|
import ctbrec.recorder.RemoteRecorder;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import ctbrec.sites.cam4.Cam4;
|
import ctbrec.sites.cam4.Cam4;
|
||||||
|
import ctbrec.sites.camsoda.Camsoda;
|
||||||
import ctbrec.sites.chaturbate.Chaturbate;
|
import ctbrec.sites.chaturbate.Chaturbate;
|
||||||
import ctbrec.sites.mfc.MyFreeCams;
|
import ctbrec.sites.mfc.MyFreeCams;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
|
@ -61,6 +62,7 @@ public class CamrecApplication extends Application {
|
||||||
public void start(Stage primaryStage) throws Exception {
|
public void start(Stage primaryStage) throws Exception {
|
||||||
sites.add(new Chaturbate());
|
sites.add(new Chaturbate());
|
||||||
sites.add(new MyFreeCams());
|
sites.add(new MyFreeCams());
|
||||||
|
sites.add(new Camsoda());
|
||||||
sites.add(new Cam4());
|
sites.add(new Cam4());
|
||||||
loadConfig();
|
loadConfig();
|
||||||
createHttpClient();
|
createHttpClient();
|
||||||
|
@ -72,9 +74,6 @@ public class CamrecApplication extends Application {
|
||||||
try {
|
try {
|
||||||
site.setRecorder(recorder);
|
site.setRecorder(recorder);
|
||||||
site.init();
|
site.init();
|
||||||
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
|
||||||
site.login();
|
|
||||||
}
|
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
LOG.error("Error while initializing site {}", site.getName(), e);
|
LOG.error("Error while initializing site {}", site.getName(), e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,12 @@ package ctbrec.ui;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import com.iheartradio.m3u8.ParseException;
|
import com.iheartradio.m3u8.ParseException;
|
||||||
import com.iheartradio.m3u8.PlaylistException;
|
import com.iheartradio.m3u8.PlaylistException;
|
||||||
|
import com.squareup.moshi.JsonReader;
|
||||||
|
import com.squareup.moshi.JsonWriter;
|
||||||
|
|
||||||
import ctbrec.AbstractModel;
|
import ctbrec.AbstractModel;
|
||||||
import ctbrec.Model;
|
import ctbrec.Model;
|
||||||
|
@ -20,14 +21,13 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||||
*/
|
*/
|
||||||
public class JavaFxModel extends AbstractModel {
|
public class JavaFxModel extends AbstractModel {
|
||||||
private transient BooleanProperty onlineProperty = new SimpleBooleanProperty();
|
private transient BooleanProperty onlineProperty = new SimpleBooleanProperty();
|
||||||
|
|
||||||
private Model delegate;
|
private Model delegate;
|
||||||
|
|
||||||
public JavaFxModel(Model delegate) {
|
public JavaFxModel(Model delegate) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
try {
|
try {
|
||||||
onlineProperty.set(Objects.equals("public", delegate.getOnlineState(true)));
|
onlineProperty.set(delegate.isOnline());
|
||||||
} catch (IOException | ExecutionException e) {}
|
} catch (IOException | ExecutionException | InterruptedException e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -147,4 +147,14 @@ public class JavaFxModel extends AbstractModel {
|
||||||
public Site getSite() {
|
public Site getSite() {
|
||||||
return delegate.getSite();
|
return delegate.getSite();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readSiteSpecificData(JsonReader reader) throws IOException {
|
||||||
|
delegate.readSiteSpecificData(reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
|
||||||
|
delegate.writeSiteSpecificData(writer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ import ctbrec.recorder.Recorder;
|
||||||
import ctbrec.recorder.download.MergedHlsDownload;
|
import ctbrec.recorder.download.MergedHlsDownload;
|
||||||
import ctbrec.sites.Site;
|
import ctbrec.sites.Site;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.concurrent.ScheduledService;
|
import javafx.concurrent.ScheduledService;
|
||||||
|
@ -47,6 +47,7 @@ import javafx.scene.control.ContextMenu;
|
||||||
import javafx.scene.control.MenuItem;
|
import javafx.scene.control.MenuItem;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
|
import javafx.scene.control.TableCell;
|
||||||
import javafx.scene.control.TableColumn;
|
import javafx.scene.control.TableColumn;
|
||||||
import javafx.scene.control.TableView;
|
import javafx.scene.control.TableView;
|
||||||
import javafx.scene.control.cell.PropertyValueFactory;
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
|
@ -57,6 +58,7 @@ import javafx.scene.input.MouseEvent;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.FlowPane;
|
import javafx.scene.layout.FlowPane;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
|
import javafx.util.Callback;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
|
||||||
public class RecordingsTab extends Tab implements TabSelectionListener {
|
public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
|
@ -99,12 +101,28 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||||
TableColumn<JavaFxRecording, String> name = new TableColumn<>("Model");
|
TableColumn<JavaFxRecording, String> name = new TableColumn<>("Model");
|
||||||
name.setPrefWidth(200);
|
name.setPrefWidth(200);
|
||||||
name.setCellValueFactory(new PropertyValueFactory<JavaFxRecording, String>("modelName"));
|
name.setCellValueFactory(new PropertyValueFactory<JavaFxRecording, String>("modelName"));
|
||||||
TableColumn<JavaFxRecording, String> date = new TableColumn<>("Date");
|
TableColumn<JavaFxRecording, Instant> date = new TableColumn<>("Date");
|
||||||
date.setCellValueFactory((cdf) -> {
|
date.setCellValueFactory((cdf) -> {
|
||||||
Instant instant = cdf.getValue().getStartDate();
|
Instant instant = cdf.getValue().getStartDate();
|
||||||
ZonedDateTime time = instant.atZone(ZoneId.systemDefault());
|
return new SimpleObjectProperty<Instant>(instant);
|
||||||
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
|
});
|
||||||
return new SimpleStringProperty(dtf.format(time));
|
date.setCellFactory(new Callback<TableColumn<JavaFxRecording, Instant>, TableCell<JavaFxRecording, Instant>>() {
|
||||||
|
@Override
|
||||||
|
public TableCell<JavaFxRecording, Instant> call(TableColumn<JavaFxRecording, Instant> param) {
|
||||||
|
TableCell<JavaFxRecording, Instant> cell = new TableCell<JavaFxRecording, Instant>() {
|
||||||
|
@Override
|
||||||
|
protected void updateItem(Instant instant, boolean empty) {
|
||||||
|
if(empty || instant == null) {
|
||||||
|
setText(null);
|
||||||
|
} else {
|
||||||
|
ZonedDateTime time = instant.atZone(ZoneId.systemDefault());
|
||||||
|
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
|
||||||
|
setText(dtf.format(time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
date.setPrefWidth(200);
|
date.setPrefWidth(200);
|
||||||
TableColumn<JavaFxRecording, String> status = new TableColumn<>("Status");
|
TableColumn<JavaFxRecording, String> status = new TableColumn<>("Status");
|
||||||
|
|
|
@ -20,6 +20,7 @@ import javafx.beans.value.ObservableValue;
|
||||||
import javafx.geometry.HPos;
|
import javafx.geometry.HPos;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Accordion;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.CheckBox;
|
import javafx.scene.control.CheckBox;
|
||||||
|
@ -64,9 +65,11 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
private RadioButton recordRemote;
|
private RadioButton recordRemote;
|
||||||
private ToggleGroup recordLocation;
|
private ToggleGroup recordLocation;
|
||||||
private ProxySettingsPane proxySettingsPane;
|
private ProxySettingsPane proxySettingsPane;
|
||||||
|
private ComboBox<Integer> maxResolution;
|
||||||
private ComboBox<SplitAfterOption> splitAfter;
|
private ComboBox<SplitAfterOption> splitAfter;
|
||||||
private List<Site> sites;
|
private List<Site> sites;
|
||||||
private Label restartLabel;
|
private Label restartLabel;
|
||||||
|
private Accordion credentialsAccordion = new Accordion();
|
||||||
|
|
||||||
public SettingsTab(List<Site> sites) {
|
public SettingsTab(List<Site> sites) {
|
||||||
this.sites = sites;
|
this.sites = sites;
|
||||||
|
@ -113,14 +116,16 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
|
|
||||||
//right side
|
//right side
|
||||||
rightSide.getChildren().add(createSiteSelectionPanel());
|
rightSide.getChildren().add(createSiteSelectionPanel());
|
||||||
for (Site site : sites) {
|
rightSide.getChildren().add(credentialsAccordion);
|
||||||
|
for (int i = 0; i < sites.size(); i++) {
|
||||||
|
Site site = sites.get(i);
|
||||||
Node siteConfig = site.getConfigurationGui();
|
Node siteConfig = site.getConfigurationGui();
|
||||||
if(siteConfig != null) {
|
if(siteConfig != null) {
|
||||||
TitledPane pane = new TitledPane(site.getName(), siteConfig);
|
TitledPane pane = new TitledPane(site.getName(), siteConfig);
|
||||||
pane.setCollapsible(false);
|
credentialsAccordion.getPanes().add(pane);
|
||||||
rightSide.getChildren().add(pane);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
credentialsAccordion.setExpandedPane(credentialsAccordion.getPanes().get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Node createSiteSelectionPanel() {
|
private Node createSiteSelectionPanel() {
|
||||||
|
@ -227,8 +232,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
keyDialog.show();
|
keyDialog.show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, CHECKBOX_MARGIN, 0, 0));
|
GridPane.setMargin(l, new Insets(4, CHECKBOX_MARGIN, 0, 0));
|
||||||
GridPane.setMargin(secureCommunication, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
GridPane.setMargin(secureCommunication, new Insets(4, 0, 0, 0));
|
||||||
layout.add(secureCommunication, 1, 3);
|
layout.add(secureCommunication, 1, 3);
|
||||||
|
|
||||||
TitledPane recordLocation = new TitledPane("Record Location", layout);
|
TitledPane recordLocation = new TitledPane("Record Location", layout);
|
||||||
|
@ -245,6 +250,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
GridPane.setFillWidth(recordingsDirectory, true);
|
GridPane.setFillWidth(recordingsDirectory, true);
|
||||||
GridPane.setHgrow(recordingsDirectory, Priority.ALWAYS);
|
GridPane.setHgrow(recordingsDirectory, Priority.ALWAYS);
|
||||||
GridPane.setColumnSpan(recordingsDirectory, 2);
|
GridPane.setColumnSpan(recordingsDirectory, 2);
|
||||||
|
GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||||
layout.add(recordingsDirectory, 1, 0);
|
layout.add(recordingsDirectory, 1, 0);
|
||||||
recordingsDirectoryButton = createRecordingsBrowseButton();
|
recordingsDirectoryButton = createRecordingsBrowseButton();
|
||||||
layout.add(recordingsDirectoryButton, 3, 0);
|
layout.add(recordingsDirectoryButton, 3, 0);
|
||||||
|
@ -255,19 +261,10 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
GridPane.setFillWidth(mediaPlayer, true);
|
GridPane.setFillWidth(mediaPlayer, true);
|
||||||
GridPane.setHgrow(mediaPlayer, Priority.ALWAYS);
|
GridPane.setHgrow(mediaPlayer, Priority.ALWAYS);
|
||||||
GridPane.setColumnSpan(mediaPlayer, 2);
|
GridPane.setColumnSpan(mediaPlayer, 2);
|
||||||
|
GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||||
layout.add(mediaPlayer, 1, 1);
|
layout.add(mediaPlayer, 1, 1);
|
||||||
layout.add(createMpvBrowseButton(), 3, 1);
|
layout.add(createMpvBrowseButton(), 3, 1);
|
||||||
|
|
||||||
Label l = new Label("Allow multiple players");
|
|
||||||
layout.add(l, 0, 2);
|
|
||||||
multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer);
|
|
||||||
multiplePlayers.setOnAction((e) -> Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected());
|
|
||||||
GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
|
||||||
GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
|
||||||
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
|
|
||||||
GridPane.setMargin(multiplePlayers, new Insets(3, 0, 0, CHECKBOX_MARGIN));
|
|
||||||
layout.add(multiplePlayers, 1, 2);
|
|
||||||
|
|
||||||
TitledPane locations = new TitledPane("Locations", layout);
|
TitledPane locations = new TitledPane("Locations", layout);
|
||||||
locations.setCollapsible(false);
|
locations.setCollapsible(false);
|
||||||
return locations;
|
return locations;
|
||||||
|
@ -275,8 +272,9 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
|
|
||||||
private Node createGeneralPanel() {
|
private Node createGeneralPanel() {
|
||||||
GridPane layout = createGridLayout();
|
GridPane layout = createGridLayout();
|
||||||
|
int row = 0;
|
||||||
Label l = new Label("Display stream resolution in overview");
|
Label l = new Label("Display stream resolution in overview");
|
||||||
layout.add(l, 0, 0);
|
layout.add(l, 0, row);
|
||||||
loadResolution = new CheckBox();
|
loadResolution = new CheckBox();
|
||||||
loadResolution.setSelected(Config.getInstance().getSettings().determineResolution);
|
loadResolution.setSelected(Config.getInstance().getSettings().determineResolution);
|
||||||
loadResolution.setOnAction((e) -> {
|
loadResolution.setOnAction((e) -> {
|
||||||
|
@ -287,18 +285,41 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
});
|
});
|
||||||
//GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
//GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||||
GridPane.setMargin(loadResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
GridPane.setMargin(loadResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||||
layout.add(loadResolution, 1, 0);
|
layout.add(loadResolution, 1, row++);
|
||||||
|
|
||||||
|
l = new Label("Allow multiple players");
|
||||||
|
layout.add(l, 0, row);
|
||||||
|
multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer);
|
||||||
|
multiplePlayers.setOnAction((e) -> Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected());
|
||||||
|
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
|
||||||
|
GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||||
|
layout.add(multiplePlayers, 1, row++);
|
||||||
|
|
||||||
l = new Label("Manually select stream quality");
|
l = new Label("Manually select stream quality");
|
||||||
layout.add(l, 0, 1);
|
layout.add(l, 0, row);
|
||||||
chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality);
|
chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality);
|
||||||
chooseStreamQuality.setOnAction((e) -> Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected());
|
chooseStreamQuality.setOnAction((e) -> Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected());
|
||||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||||
GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||||
layout.add(chooseStreamQuality, 1, 1);
|
layout.add(chooseStreamQuality, 1, row++);
|
||||||
|
|
||||||
|
l = new Label("Maximum resolution (0 = unlimited)");
|
||||||
|
layout.add(l, 0, row);
|
||||||
|
List<Integer> resolutionOptions = new ArrayList<>();
|
||||||
|
resolutionOptions.add(1080);
|
||||||
|
resolutionOptions.add(720);
|
||||||
|
resolutionOptions.add(600);
|
||||||
|
resolutionOptions.add(480);
|
||||||
|
resolutionOptions.add(0);
|
||||||
|
maxResolution = new ComboBox<>(new ObservableListWrapper<>(resolutionOptions));
|
||||||
|
setMaxResolutionValue();
|
||||||
|
maxResolution.setOnAction((e) -> Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem());
|
||||||
|
layout.add(maxResolution, 1, row++);
|
||||||
|
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||||
|
GridPane.setMargin(maxResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||||
|
|
||||||
l = new Label("Split recordings after (minutes)");
|
l = new Label("Split recordings after (minutes)");
|
||||||
layout.add(l, 0, 2);
|
layout.add(l, 0, row);
|
||||||
List<SplitAfterOption> options = new ArrayList<>();
|
List<SplitAfterOption> options = new ArrayList<>();
|
||||||
options.add(new SplitAfterOption("disabled", 0));
|
options.add(new SplitAfterOption("disabled", 0));
|
||||||
options.add(new SplitAfterOption("10 min", 10 * 60));
|
options.add(new SplitAfterOption("10 min", 10 * 60));
|
||||||
|
@ -307,11 +328,12 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
options.add(new SplitAfterOption("30 min", 30 * 60));
|
options.add(new SplitAfterOption("30 min", 30 * 60));
|
||||||
options.add(new SplitAfterOption("60 min", 60 * 60));
|
options.add(new SplitAfterOption("60 min", 60 * 60));
|
||||||
splitAfter = new ComboBox<>(new ObservableListWrapper<>(options));
|
splitAfter = new ComboBox<>(new ObservableListWrapper<>(options));
|
||||||
layout.add(splitAfter, 1, 2);
|
layout.add(splitAfter, 1, row++);
|
||||||
setSplitAfterValue();
|
setSplitAfterValue();
|
||||||
splitAfter.setOnAction((e) -> Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue());
|
splitAfter.setOnAction((e) -> Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue());
|
||||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
|
||||||
GridPane.setMargin(splitAfter, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||||
|
maxResolution.prefWidthProperty().bind(splitAfter.widthProperty());
|
||||||
|
|
||||||
TitledPane general = new TitledPane("General", layout);
|
TitledPane general = new TitledPane("General", layout);
|
||||||
general.setCollapsible(false);
|
general.setCollapsible(false);
|
||||||
|
@ -327,6 +349,15 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setMaxResolutionValue() {
|
||||||
|
int value = Config.getInstance().getSettings().maximumResolution;
|
||||||
|
for (Integer option : maxResolution.getItems()) {
|
||||||
|
if(option == value) {
|
||||||
|
maxResolution.getSelectionModel().select(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void showRestartRequired() {
|
void showRestartRequired() {
|
||||||
restartLabel.setVisible(true);
|
restartLabel.setVisible(true);
|
||||||
}
|
}
|
||||||
|
@ -346,6 +377,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
||||||
recordingsDirectory.setDisable(!local);
|
recordingsDirectory.setDisable(!local);
|
||||||
recordingsDirectoryButton.setDisable(!local);
|
recordingsDirectoryButton.setDisable(!local);
|
||||||
splitAfter.setDisable(!local);
|
splitAfter.setDisable(!local);
|
||||||
|
maxResolution.setDisable(!local);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChangeListener<? super Boolean> createRecordingsDirectoryFocusListener() {
|
private ChangeListener<? super Boolean> createRecordingsDirectoryFocusListener() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package ctbrec.ui;
|
package ctbrec.ui;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -207,10 +208,16 @@ public class ThumbCell extends StackPane {
|
||||||
LOG.trace("Removing invalid resolution value for {}", model.getName());
|
LOG.trace("Removing invalid resolution value for {}", model.getName());
|
||||||
model.invalidateCacheEntries();
|
model.invalidateCacheEntries();
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread.sleep(500);
|
Thread.sleep(500);
|
||||||
} catch (ExecutionException | IOException | InterruptedException e1) {
|
} catch (IOException | InterruptedException e1) {
|
||||||
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1);
|
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1);
|
||||||
|
} catch(ExecutionException e) {
|
||||||
|
if(e.getCause() instanceof EOFException) {
|
||||||
|
LOG.warn("Couldn't update resolution tag for model {}. Playlist empty", model.getName());
|
||||||
|
} else {
|
||||||
|
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
ThumbOverviewTab.resolutionProcessing.remove(model);
|
ThumbOverviewTab.resolutionProcessing.remove(model);
|
||||||
}
|
}
|
||||||
|
@ -485,13 +492,13 @@ public class ThumbCell extends StackPane {
|
||||||
nameBackground.setWidth(w);
|
nameBackground.setWidth(w);
|
||||||
nameBackground.setHeight(20);
|
nameBackground.setHeight(20);
|
||||||
topicBackground.setWidth(w);
|
topicBackground.setWidth(w);
|
||||||
topicBackground.setHeight(h-nameBackground.getHeight());
|
topicBackground.setHeight(getHeight()-nameBackground.getHeight());
|
||||||
topic.prefHeight(h-25);
|
topic.prefHeight(getHeight()-25);
|
||||||
topic.maxHeight(h-25);
|
topic.maxHeight(getHeight()-25);
|
||||||
int margin = 4;
|
int margin = 4;
|
||||||
topic.maxWidth(w-margin*2);
|
topic.maxWidth(w-margin*2);
|
||||||
topic.setWrappingWidth(w-margin*2);
|
topic.setWrappingWidth(w-margin*2);
|
||||||
selectionOverlay.setWidth(w);
|
selectionOverlay.setWidth(w);
|
||||||
selectionOverlay.setHeight(h);
|
selectionOverlay.setHeight(getHeight());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import ctbrec.sites.Site;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
|
|
||||||
public class TokenLabel extends Label {
|
public class TokenLabel extends Label {
|
||||||
|
|
||||||
|
@ -26,10 +27,10 @@ public class TokenLabel extends Label {
|
||||||
CamrecApplication.bus.register(new Object() {
|
CamrecApplication.bus.register(new Object() {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void tokensUpdates(Map<String, Object> e) {
|
public void tokensUpdates(Map<String, Object> e) {
|
||||||
if(Objects.equals("tokens", e.get("event"))) {
|
if (Objects.equals("tokens", e.get("event"))) {
|
||||||
tokens = (int) e.get("amount");
|
tokens = (int) e.get("amount");
|
||||||
updateText();
|
updateText();
|
||||||
} else if(Objects.equals("tokens.sent", e.get("event"))) {
|
} else if (Objects.equals("tokens.sent", e.get("event"))) {
|
||||||
int _tokens = (int) e.get("amount");
|
int _tokens = (int) e.get("amount");
|
||||||
tokens -= _tokens;
|
tokens -= _tokens;
|
||||||
updateText();
|
updateText();
|
||||||
|
@ -70,7 +71,10 @@ public class TokenLabel extends Label {
|
||||||
update(tokens);
|
update(tokens);
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
LOG.error("Couldn't retrieve account balance", e);
|
LOG.error("Couldn't retrieve account balance", e);
|
||||||
Platform.runLater(() -> setText("Tokens: error"));
|
Platform.runLater(() -> {
|
||||||
|
setText("Tokens: error");
|
||||||
|
setTooltip(new Tooltip(e.getMessage()));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
|
@ -41,7 +41,7 @@
|
||||||
<logger name="ctbrec.recorder.Chaturbate" level="INFO" />
|
<logger name="ctbrec.recorder.Chaturbate" level="INFO" />
|
||||||
<logger name="ctbrec.recorder.server.HlsServlet" level="INFO"/>
|
<logger name="ctbrec.recorder.server.HlsServlet" level="INFO"/>
|
||||||
<logger name="ctbrec.recorder.server.RecorderServlet" level="INFO"/>
|
<logger name="ctbrec.recorder.server.RecorderServlet" level="INFO"/>
|
||||||
<logger name="ctbrec.ui.CookieJarImpl" level="INFO"/>
|
<logger name="ctbrec.io.CookieJarImpl" level="INFO"/>
|
||||||
<logger name="ctbrec.ui.ThumbOverviewTab" level="DEBUG"/>
|
<logger name="ctbrec.ui.ThumbOverviewTab" level="DEBUG"/>
|
||||||
<logger name="org.eclipse.jetty" level="INFO" />
|
<logger name="org.eclipse.jetty" level="INFO" />
|
||||||
<logger name="streamer" level="ERROR" />
|
<logger name="streamer" level="ERROR" />
|
||||||
|
|
Loading…
Reference in New Issue