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
|
||||
========================
|
||||
* 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>
|
||||
<groupId>ctbrec</groupId>
|
||||
<artifactId>ctbrec</artifactId>
|
||||
<version>1.6.1</version>
|
||||
<version>1.7.0</version>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
@ -66,6 +66,7 @@
|
|||
<descriptor>src/assembly/win64-jre.xml</descriptor>
|
||||
<descriptor>src/assembly/win32-jre.xml</descriptor>
|
||||
<descriptor>src/assembly/linux.xml</descriptor>
|
||||
<descriptor>src/assembly/macos-jre.xml</descriptor>
|
||||
</descriptors>
|
||||
</configuration>
|
||||
</execution>
|
||||
|
@ -77,7 +78,7 @@
|
|||
<version>1.7.22</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>l4j-clui</id>
|
||||
<id>l4j-win</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>launch4j</goal>
|
||||
|
|
|
@ -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.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
|
||||
public abstract class AbstractModel implements Model {
|
||||
|
||||
|
@ -20,6 +17,11 @@ public abstract class AbstractModel implements Model {
|
|||
private List<String> tags = new ArrayList<>();
|
||||
private int streamUrlIndex = -1;
|
||||
|
||||
@Override
|
||||
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
||||
return isOnline(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return url;
|
||||
|
@ -81,16 +83,13 @@ public abstract class AbstractModel implements Model {
|
|||
}
|
||||
|
||||
@Override
|
||||
public String getSegmentPlaylistUrl() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
List<StreamSource> streamSources = getStreamSources();
|
||||
String url = null;
|
||||
if(getStreamUrlIndex() >= 0 && getStreamUrlIndex() < streamSources.size()) {
|
||||
url = streamSources.get(getStreamUrlIndex()).getMediaPlaylistUrl();
|
||||
} else {
|
||||
Collections.sort(streamSources);
|
||||
url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl();
|
||||
}
|
||||
return url;
|
||||
public void readSiteSpecificData(JsonReader reader) throws IOException {
|
||||
// noop default implementation, can be overriden by concrete models
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
|
||||
// noop default implementation, can be overriden by concrete models
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -6,6 +6,8 @@ import java.util.concurrent.ExecutionException;
|
|||
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import ctbrec.sites.Site;
|
||||
|
@ -27,7 +29,6 @@ public interface Model {
|
|||
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException;
|
||||
public String getOnlineState(boolean failFast) throws IOException, ExecutionException;
|
||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException;
|
||||
public String getSegmentPlaylistUrl() throws IOException, ExecutionException, ParseException, PlaylistException;
|
||||
public void invalidateCacheEntries();
|
||||
public void receiveTip(int tokens) throws IOException;
|
||||
public int[] getStreamResolution(boolean failFast) throws ExecutionException;
|
||||
|
@ -35,4 +36,6 @@ public interface Model {
|
|||
public boolean unfollow() throws IOException;
|
||||
public void setSite(Site site);
|
||||
public Site getSite();
|
||||
public void writeSiteSpecificData(JsonWriter writer) throws IOException;
|
||||
public void readSiteSpecificData(JsonReader reader) throws IOException;
|
||||
}
|
|
@ -24,6 +24,8 @@ public class Settings {
|
|||
public String password = ""; // chaturbate password TODO maybe rename this onetime
|
||||
public String mfcUsername = "";
|
||||
public String mfcPassword = "";
|
||||
public String camsodaUsername = "";
|
||||
public String camsodaPassword = "";
|
||||
public String cam4Username;
|
||||
public String cam4Password;
|
||||
public String lastDownloadDir = "";
|
||||
|
@ -32,6 +34,7 @@ public class Settings {
|
|||
public boolean determineResolution = false;
|
||||
public boolean requireAuthentication = false;
|
||||
public boolean chooseStreamQuality = false;
|
||||
public int maximumResolution = 0;
|
||||
public byte[] key = null;
|
||||
public ProxyType proxyType = ProxyType.DIRECT;
|
||||
public String proxyHost;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package ctbrec.ui;
|
||||
package ctbrec.io;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
@ -34,6 +34,7 @@ public class CookieJarImpl implements CookieJar {
|
|||
if(newCookie.name().equalsIgnoreCase(name)) {
|
||||
LOG.debug("Replacing cookie {} {} -> {} [{}]", oldCookie.name(), oldCookie.value(), newCookie.value(), oldCookie.domain());
|
||||
iterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ import java.util.concurrent.TimeUnit;
|
|||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Settings.ProxyType;
|
||||
import ctbrec.ui.CookieJarImpl;
|
||||
import okhttp3.ConnectionPool;
|
||||
import okhttp3.Credentials;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
|
|
@ -32,45 +32,51 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
|||
String url = null;
|
||||
String type = null;
|
||||
int streamUrlIndex = -1;
|
||||
|
||||
Model model = null;
|
||||
while(reader.hasNext()) {
|
||||
Token token = reader.peek();
|
||||
if(token == Token.NAME) {
|
||||
String key = reader.nextName();
|
||||
if(key.equals("name")) {
|
||||
name = reader.nextString();
|
||||
} else if(key.equals("description")) {
|
||||
description = reader.nextString();
|
||||
} else if(key.equals("url")) {
|
||||
url = reader.nextString();
|
||||
} else if(key.equals("type")) {
|
||||
type = reader.nextString();
|
||||
} else if(key.equals("streamUrlIndex")) {
|
||||
streamUrlIndex = reader.nextInt();
|
||||
try {
|
||||
Token token = reader.peek();
|
||||
if(token == Token.NAME) {
|
||||
String key = reader.nextName();
|
||||
if(key.equals("name")) {
|
||||
name = reader.nextString();
|
||||
model.setName(name);
|
||||
} else if(key.equals("description")) {
|
||||
description = reader.nextString();
|
||||
model.setDescription(description);
|
||||
} else if(key.equals("url")) {
|
||||
url = reader.nextString();
|
||||
model.setUrl(url);
|
||||
} else if(key.equals("type")) {
|
||||
type = reader.nextString();
|
||||
Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()));
|
||||
model = (Model) modelClass.newInstance();
|
||||
} else if(key.equals("streamUrlIndex")) {
|
||||
streamUrlIndex = reader.nextInt();
|
||||
model.setStreamUrlIndex(streamUrlIndex);
|
||||
} else if(key.equals("siteSpecific")) {
|
||||
reader.beginObject();
|
||||
model.readSiteSpecificData(reader);
|
||||
reader.endObject();
|
||||
}
|
||||
} else {
|
||||
reader.skipValue();
|
||||
}
|
||||
} else {
|
||||
reader.skipValue();
|
||||
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
|
||||
throw new IOException("Couldn't instantiate model class [" + type + "]", e);
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
|
||||
try {
|
||||
Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()));
|
||||
Model model = (Model) modelClass.newInstance();
|
||||
model.setName(name);
|
||||
model.setDescription(description);
|
||||
model.setUrl(url);
|
||||
model.setStreamUrlIndex(streamUrlIndex);
|
||||
if(sites != null) {
|
||||
for (Site site : sites) {
|
||||
if(site.isSiteForModel(model)) {
|
||||
model.setSite(site);
|
||||
}
|
||||
if(sites != null) {
|
||||
for (Site site : sites) {
|
||||
if(site.isSiteForModel(model)) {
|
||||
model.setSite(site);
|
||||
}
|
||||
}
|
||||
return model;
|
||||
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
|
||||
throw new IOException("Couldn't instantiate model class [" + type + "]", e);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -81,6 +87,10 @@ public class ModelJsonAdapter extends JsonAdapter<Model> {
|
|||
writeValueIfSet(writer, "description", model.getDescription());
|
||||
writeValueIfSet(writer, "url", model.getUrl());
|
||||
writer.name("streamUrlIndex").value(model.getStreamUrlIndex());
|
||||
writer.name("siteSpecific");
|
||||
writer.beginObject();
|
||||
model.writeSiteSpecificData(writer);
|
||||
writer.endObject();
|
||||
writer.endObject();
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,16 @@ import java.io.InputStream;
|
|||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.iheartradio.m3u8.Encoding;
|
||||
import com.iheartradio.m3u8.Format;
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
|
@ -20,12 +26,16 @@ import com.iheartradio.m3u8.data.MediaPlaylist;
|
|||
import com.iheartradio.m3u8.data.Playlist;
|
||||
import com.iheartradio.m3u8.data.TrackData;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.io.HttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
public abstract class AbstractHlsDownload implements Download {
|
||||
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(AbstractHlsDownload.class);
|
||||
|
||||
ExecutorService downloadThreadPool = Executors.newFixedThreadPool(5);
|
||||
HttpClient client;
|
||||
volatile boolean running = false;
|
||||
|
@ -69,6 +79,34 @@ public abstract class AbstractHlsDownload implements Download {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
String getSegmentPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
List<StreamSource> streamSources = model.getStreamSources();
|
||||
String url = null;
|
||||
if(model.getStreamUrlIndex() >= 0 && model.getStreamUrlIndex() < streamSources.size()) {
|
||||
url = streamSources.get(model.getStreamUrlIndex()).getMediaPlaylistUrl();
|
||||
} else {
|
||||
Collections.sort(streamSources);
|
||||
// filter out stream resolutions, which are too high
|
||||
int maxRes = Config.getInstance().getSettings().maximumResolution;
|
||||
if(maxRes > 0) {
|
||||
for (Iterator<StreamSource> iterator = streamSources.iterator(); iterator.hasNext();) {
|
||||
StreamSource streamSource = iterator.next();
|
||||
if(streamSource.height > 0 && maxRes < streamSource.height) {
|
||||
LOG.trace("Res too high {} > {}", streamSource.height, maxRes);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
if(streamSources.isEmpty()) {
|
||||
throw new ExecutionException(new RuntimeException("No stream left in playlist"));
|
||||
} else {
|
||||
url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl();
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAlive() {
|
||||
return alive;
|
||||
|
|
|
@ -48,7 +48,7 @@ public class HlsDownload extends AbstractHlsDownload {
|
|||
throw new IOException(model.getName() +"'s room is not public");
|
||||
}
|
||||
|
||||
String segments = model.getSegmentPlaylistUrl();
|
||||
String segments = getSegmentPlaylistUrl(model);
|
||||
if(segments != null) {
|
||||
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
|
||||
Files.createDirectories(downloadDir);
|
||||
|
|
|
@ -101,7 +101,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
target = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-00000.ts"));
|
||||
}
|
||||
|
||||
String segments = model.getSegmentPlaylistUrl();
|
||||
String segments = getSegmentPlaylistUrl(model);
|
||||
mergeThread = createMergeThread(target, null, true);
|
||||
mergeThread.start();
|
||||
if(segments != null) {
|
||||
|
|
|
@ -21,6 +21,7 @@ import ctbrec.recorder.LocalRecorder;
|
|||
import ctbrec.recorder.Recorder;
|
||||
import ctbrec.sites.Site;
|
||||
import ctbrec.sites.cam4.Cam4;
|
||||
import ctbrec.sites.camsoda.Camsoda;
|
||||
import ctbrec.sites.chaturbate.Chaturbate;
|
||||
import ctbrec.sites.mfc.MyFreeCams;
|
||||
|
||||
|
@ -53,7 +54,9 @@ public class HttpServer {
|
|||
}
|
||||
recorder = new LocalRecorder(config);
|
||||
for (Site site : sites) {
|
||||
site.init();
|
||||
if(site.isEnabled()) {
|
||||
site.init();
|
||||
}
|
||||
}
|
||||
startHttpServer();
|
||||
}
|
||||
|
@ -61,6 +64,7 @@ public class HttpServer {
|
|||
private void createSites() {
|
||||
sites.add(new Chaturbate());
|
||||
sites.add(new MyFreeCams());
|
||||
sites.add(new Camsoda());
|
||||
sites.add(new Cam4());
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ctbrec.sites.cam4;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
|
@ -13,6 +14,7 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.OS;
|
||||
import javafx.concurrent.Worker.State;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
|
@ -71,24 +73,29 @@ public class Cam4LoginDialog {
|
|||
});
|
||||
webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> {
|
||||
if (newState == State.SUCCEEDED) {
|
||||
String username = Config.getInstance().getSettings().cam4Username;
|
||||
if (username != null && !username.trim().isEmpty()) {
|
||||
webEngine.executeScript("$('input[name=username]').attr('value','" + username + "')");
|
||||
}
|
||||
String password = Config.getInstance().getSettings().cam4Password;
|
||||
if (password != null && !password.trim().isEmpty()) {
|
||||
webEngine.executeScript("$('input[name=password]').attr('value','" + password + "')");
|
||||
}
|
||||
webEngine.executeScript("$('div[class~=navbar]').css('display','none')");
|
||||
webEngine.executeScript("$('div#footer').css('display','none')");
|
||||
webEngine.executeScript("$('div#content').css('padding','0')");
|
||||
veil.setVisible(false);
|
||||
p.setVisible(false);
|
||||
try {
|
||||
String username = Config.getInstance().getSettings().cam4Username;
|
||||
if (username != null && !username.trim().isEmpty()) {
|
||||
webEngine.executeScript("$('input[name=username]').attr('value','" + username + "')");
|
||||
}
|
||||
String password = Config.getInstance().getSettings().cam4Password;
|
||||
if (password != null && !password.trim().isEmpty()) {
|
||||
webEngine.executeScript("$('input[name=password]').attr('value','" + password + "')");
|
||||
}
|
||||
webEngine.executeScript("$('div[class~=navbar]').css('display','none')");
|
||||
webEngine.executeScript("$('div#footer').css('display','none')");
|
||||
webEngine.executeScript("$('div#content').css('padding','0')");
|
||||
} catch(Exception e) {
|
||||
LOG.warn("Couldn't auto fill username and password for Cam4", e);
|
||||
}
|
||||
} else if (newState == State.CANCELLED || newState == State.FAILED) {
|
||||
veil.setVisible(false);
|
||||
p.setVisible(false);
|
||||
}
|
||||
});
|
||||
webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine"));
|
||||
webEngine.load(URL);
|
||||
return browser;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
||||
return isOnline(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||
StreamInfo info;
|
||||
|
|
|
@ -55,7 +55,7 @@ public class MyFreeCams extends AbstractSite {
|
|||
|
||||
@Override
|
||||
public String getAffiliateLink() {
|
||||
return "";
|
||||
return BASE_URI + "/?baf=8127165";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -93,7 +93,7 @@ public class MyFreeCams extends AbstractSite {
|
|||
|
||||
@Override
|
||||
public String getBuyTokensLink() {
|
||||
return "https://www.myfreecams.com/php/purchase.php?request=tokens";
|
||||
return BASE_URI + "/php/purchase.php?request=tokens";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -149,7 +149,7 @@ public class MyFreeCams extends AbstractSite {
|
|||
layout.add(password, 1, 1);
|
||||
|
||||
Button createAccount = new Button("Create new Account");
|
||||
createAccount.setOnAction((e) -> DesktopIntergation.open(BASE_URI + "/php/signup.php?request=register"));
|
||||
createAccount.setOnAction((e) -> DesktopIntergation.open(getAffiliateLink()));
|
||||
layout.add(createAccount, 1, 2);
|
||||
GridPane.setColumnSpan(createAccount, 2);
|
||||
GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN));
|
||||
|
|
|
@ -57,6 +57,7 @@ public class MyFreeCamsClient {
|
|||
private String ctxenc;
|
||||
private String chatToken;
|
||||
private int sessionId;
|
||||
private long heartBeat;
|
||||
|
||||
private EvictingQueue<String> receivedTextHistory = EvictingQueue.create(10000);
|
||||
|
||||
|
@ -135,6 +136,7 @@ public class MyFreeCamsClient {
|
|||
// TODO find out, what the values in the json message mean, at the moment we hust send 0s, which seems to work, too
|
||||
// webSocket.send("1 0 0 81 0 %7B%22err%22%3A0%2C%22start%22%3A1540159843072%2C%22stop%22%3A1540159844121%2C%22a%22%3A6392%2C%22time%22%3A1540159844%2C%22key%22%3A%228da80f985c9db390809713dac71df297%22%2C%22cid%22%3A%22c504d684%22%2C%22pid%22%3A1%2C%22site%22%3A%22www%22%7D\n");
|
||||
webSocket.send("1 0 0 81 0 %7B%22err%22%3A0%2C%22start%22%3A0%2C%22stop%22%3A0%2C%22a%22%3A0%2C%22time%22%3A0%2C%22key%22%3A%22%22%2C%22cid%22%3A%22%22%2C%22pid%22%3A1%2C%22site%22%3A%22www%22%7D\n");
|
||||
heartBeat = System.currentTimeMillis();
|
||||
startKeepAlive(webSocket);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
|
@ -165,6 +167,7 @@ public class MyFreeCamsClient {
|
|||
@Override
|
||||
public void onMessage(WebSocket webSocket, String text) {
|
||||
super.onMessage(webSocket, text);
|
||||
heartBeat = System.currentTimeMillis();
|
||||
receivedTextHistory.add(text);
|
||||
msgBuffer.append(text);
|
||||
Message message;
|
||||
|
@ -469,6 +472,14 @@ public class MyFreeCamsClient {
|
|||
LOG.trace("--> NULL to keep the connection alive");
|
||||
try {
|
||||
ws.send("0 0 0 0 0 -\n");
|
||||
|
||||
long millisSinceLastMessage = System.currentTimeMillis() - heartBeat;
|
||||
if(millisSinceLastMessage > TimeUnit.MINUTES.toMillis(2)) {
|
||||
LOG.info("No message since 2 mins. Restarting websocket");
|
||||
ws.close(1000, "");
|
||||
MyFreeCamsClient.this.ws = null;
|
||||
}
|
||||
|
||||
Thread.sleep(TimeUnit.SECONDS.toMillis(15));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
|
@ -484,7 +495,7 @@ public class MyFreeCamsClient {
|
|||
lock.lock();
|
||||
try {
|
||||
for (SessionState state : sessionStates.values()) {
|
||||
if(Objects.equals(state.getNm(), model.getName())) {
|
||||
if(Objects.equals(state.getNm(), model.getName()) || Objects.equals(model.getUid(), state.getUid())) {
|
||||
model.update(state, getStreamUrl(state));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ import com.iheartradio.m3u8.PlaylistParser;
|
|||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||
import com.iheartradio.m3u8.data.Playlist;
|
||||
import com.iheartradio.m3u8.data.PlaylistData;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
|
||||
import ctbrec.AbstractModel;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
|
@ -37,7 +39,7 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsModel.class);
|
||||
|
||||
private int uid;
|
||||
private int uid = -1; // undefined
|
||||
private String hlsUrl;
|
||||
private double camScore;
|
||||
private int viewerCount;
|
||||
|
@ -207,7 +209,17 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setName(String name) {
|
||||
if(getName() != null && name != null && !getName().equals(name)) {
|
||||
LOG.debug("Model name changed {} -> {}", getName(), name);
|
||||
}
|
||||
super.setName(name);
|
||||
}
|
||||
|
||||
public void update(SessionState state, String streamUrl) {
|
||||
uid = Integer.parseInt(state.getUid().toString());
|
||||
setName(state.getNm());
|
||||
setCamScore(state.getM().getCamscore());
|
||||
setState(State.of(state.getVs()));
|
||||
setStreamUrl(streamUrl);
|
||||
|
@ -308,4 +320,15 @@ public class MyFreeCamsModel extends AbstractModel {
|
|||
public Site getSite() {
|
||||
return site;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readSiteSpecificData(JsonReader reader) throws IOException {
|
||||
reader.nextName();
|
||||
uid = reader.nextInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
|
||||
writer.name("uid").value(uid);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import ctbrec.recorder.Recorder;
|
|||
import ctbrec.recorder.RemoteRecorder;
|
||||
import ctbrec.sites.Site;
|
||||
import ctbrec.sites.cam4.Cam4;
|
||||
import ctbrec.sites.camsoda.Camsoda;
|
||||
import ctbrec.sites.chaturbate.Chaturbate;
|
||||
import ctbrec.sites.mfc.MyFreeCams;
|
||||
import javafx.application.Application;
|
||||
|
@ -61,6 +62,7 @@ public class CamrecApplication extends Application {
|
|||
public void start(Stage primaryStage) throws Exception {
|
||||
sites.add(new Chaturbate());
|
||||
sites.add(new MyFreeCams());
|
||||
sites.add(new Camsoda());
|
||||
sites.add(new Cam4());
|
||||
loadConfig();
|
||||
createHttpClient();
|
||||
|
@ -72,9 +74,6 @@ public class CamrecApplication extends Application {
|
|||
try {
|
||||
site.setRecorder(recorder);
|
||||
site.init();
|
||||
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||
site.login();
|
||||
}
|
||||
} catch(Exception e) {
|
||||
LOG.error("Error while initializing site {}", site.getName(), e);
|
||||
}
|
||||
|
|
|
@ -2,11 +2,12 @@ package ctbrec.ui;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
|
||||
import ctbrec.AbstractModel;
|
||||
import ctbrec.Model;
|
||||
|
@ -20,14 +21,13 @@ import javafx.beans.property.SimpleBooleanProperty;
|
|||
*/
|
||||
public class JavaFxModel extends AbstractModel {
|
||||
private transient BooleanProperty onlineProperty = new SimpleBooleanProperty();
|
||||
|
||||
private Model delegate;
|
||||
|
||||
public JavaFxModel(Model delegate) {
|
||||
this.delegate = delegate;
|
||||
try {
|
||||
onlineProperty.set(Objects.equals("public", delegate.getOnlineState(true)));
|
||||
} catch (IOException | ExecutionException e) {}
|
||||
onlineProperty.set(delegate.isOnline());
|
||||
} catch (IOException | ExecutionException | InterruptedException e) {}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -147,4 +147,14 @@ public class JavaFxModel extends AbstractModel {
|
|||
public Site getSite() {
|
||||
return delegate.getSite();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readSiteSpecificData(JsonReader reader) throws IOException {
|
||||
delegate.readSiteSpecificData(reader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
|
||||
delegate.writeSiteSpecificData(writer);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ import ctbrec.recorder.Recorder;
|
|||
import ctbrec.recorder.download.MergedHlsDownload;
|
||||
import ctbrec.sites.Site;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
|
@ -47,6 +47,7 @@ import javafx.scene.control.ContextMenu;
|
|||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
|
@ -57,6 +58,7 @@ import javafx.scene.input.MouseEvent;
|
|||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class RecordingsTab extends Tab implements TabSelectionListener {
|
||||
|
@ -99,12 +101,28 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
TableColumn<JavaFxRecording, String> name = new TableColumn<>("Model");
|
||||
name.setPrefWidth(200);
|
||||
name.setCellValueFactory(new PropertyValueFactory<JavaFxRecording, String>("modelName"));
|
||||
TableColumn<JavaFxRecording, String> date = new TableColumn<>("Date");
|
||||
TableColumn<JavaFxRecording, Instant> date = new TableColumn<>("Date");
|
||||
date.setCellValueFactory((cdf) -> {
|
||||
Instant instant = cdf.getValue().getStartDate();
|
||||
ZonedDateTime time = instant.atZone(ZoneId.systemDefault());
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
|
||||
return new SimpleStringProperty(dtf.format(time));
|
||||
return new SimpleObjectProperty<Instant>(instant);
|
||||
});
|
||||
date.setCellFactory(new Callback<TableColumn<JavaFxRecording, Instant>, TableCell<JavaFxRecording, Instant>>() {
|
||||
@Override
|
||||
public TableCell<JavaFxRecording, Instant> call(TableColumn<JavaFxRecording, Instant> param) {
|
||||
TableCell<JavaFxRecording, Instant> cell = new TableCell<JavaFxRecording, Instant>() {
|
||||
@Override
|
||||
protected void updateItem(Instant instant, boolean empty) {
|
||||
if(empty || instant == null) {
|
||||
setText(null);
|
||||
} else {
|
||||
ZonedDateTime time = instant.atZone(ZoneId.systemDefault());
|
||||
DateTimeFormatter dtf = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM);
|
||||
setText(dtf.format(time));
|
||||
}
|
||||
}
|
||||
};
|
||||
return cell;
|
||||
}
|
||||
});
|
||||
date.setPrefWidth(200);
|
||||
TableColumn<JavaFxRecording, String> status = new TableColumn<>("Status");
|
||||
|
|
|
@ -20,6 +20,7 @@ import javafx.beans.value.ObservableValue;
|
|||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Accordion;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
|
@ -64,9 +65,11 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
private RadioButton recordRemote;
|
||||
private ToggleGroup recordLocation;
|
||||
private ProxySettingsPane proxySettingsPane;
|
||||
private ComboBox<Integer> maxResolution;
|
||||
private ComboBox<SplitAfterOption> splitAfter;
|
||||
private List<Site> sites;
|
||||
private Label restartLabel;
|
||||
private Accordion credentialsAccordion = new Accordion();
|
||||
|
||||
public SettingsTab(List<Site> sites) {
|
||||
this.sites = sites;
|
||||
|
@ -113,14 +116,16 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
//right side
|
||||
rightSide.getChildren().add(createSiteSelectionPanel());
|
||||
for (Site site : sites) {
|
||||
rightSide.getChildren().add(credentialsAccordion);
|
||||
for (int i = 0; i < sites.size(); i++) {
|
||||
Site site = sites.get(i);
|
||||
Node siteConfig = site.getConfigurationGui();
|
||||
if(siteConfig != null) {
|
||||
TitledPane pane = new TitledPane(site.getName(), siteConfig);
|
||||
pane.setCollapsible(false);
|
||||
rightSide.getChildren().add(pane);
|
||||
credentialsAccordion.getPanes().add(pane);
|
||||
}
|
||||
}
|
||||
credentialsAccordion.setExpandedPane(credentialsAccordion.getPanes().get(0));
|
||||
}
|
||||
|
||||
private Node createSiteSelectionPanel() {
|
||||
|
@ -227,8 +232,8 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
keyDialog.show();
|
||||
}
|
||||
});
|
||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, CHECKBOX_MARGIN, 0, 0));
|
||||
GridPane.setMargin(secureCommunication, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||
GridPane.setMargin(l, new Insets(4, CHECKBOX_MARGIN, 0, 0));
|
||||
GridPane.setMargin(secureCommunication, new Insets(4, 0, 0, 0));
|
||||
layout.add(secureCommunication, 1, 3);
|
||||
|
||||
TitledPane recordLocation = new TitledPane("Record Location", layout);
|
||||
|
@ -245,6 +250,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
GridPane.setFillWidth(recordingsDirectory, true);
|
||||
GridPane.setHgrow(recordingsDirectory, Priority.ALWAYS);
|
||||
GridPane.setColumnSpan(recordingsDirectory, 2);
|
||||
GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(recordingsDirectory, 1, 0);
|
||||
recordingsDirectoryButton = createRecordingsBrowseButton();
|
||||
layout.add(recordingsDirectoryButton, 3, 0);
|
||||
|
@ -255,19 +261,10 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
GridPane.setFillWidth(mediaPlayer, true);
|
||||
GridPane.setHgrow(mediaPlayer, Priority.ALWAYS);
|
||||
GridPane.setColumnSpan(mediaPlayer, 2);
|
||||
GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(mediaPlayer, 1, 1);
|
||||
layout.add(createMpvBrowseButton(), 3, 1);
|
||||
|
||||
Label l = new Label("Allow multiple players");
|
||||
layout.add(l, 0, 2);
|
||||
multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer);
|
||||
multiplePlayers.setOnAction((e) -> Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected());
|
||||
GridPane.setMargin(recordingsDirectory, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
GridPane.setMargin(mediaPlayer, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
|
||||
GridPane.setMargin(multiplePlayers, new Insets(3, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(multiplePlayers, 1, 2);
|
||||
|
||||
TitledPane locations = new TitledPane("Locations", layout);
|
||||
locations.setCollapsible(false);
|
||||
return locations;
|
||||
|
@ -275,8 +272,9 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
private Node createGeneralPanel() {
|
||||
GridPane layout = createGridLayout();
|
||||
int row = 0;
|
||||
Label l = new Label("Display stream resolution in overview");
|
||||
layout.add(l, 0, 0);
|
||||
layout.add(l, 0, row);
|
||||
loadResolution = new CheckBox();
|
||||
loadResolution.setSelected(Config.getInstance().getSettings().determineResolution);
|
||||
loadResolution.setOnAction((e) -> {
|
||||
|
@ -287,18 +285,41 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
});
|
||||
//GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||
GridPane.setMargin(loadResolution, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(loadResolution, 1, 0);
|
||||
layout.add(loadResolution, 1, row++);
|
||||
|
||||
l = new Label("Allow multiple players");
|
||||
layout.add(l, 0, row);
|
||||
multiplePlayers.setSelected(!Config.getInstance().getSettings().singlePlayer);
|
||||
multiplePlayers.setOnAction((e) -> Config.getInstance().getSettings().singlePlayer = !multiplePlayers.isSelected());
|
||||
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
|
||||
GridPane.setMargin(multiplePlayers, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(multiplePlayers, 1, row++);
|
||||
|
||||
l = new Label("Manually select stream quality");
|
||||
layout.add(l, 0, 1);
|
||||
layout.add(l, 0, row);
|
||||
chooseStreamQuality.setSelected(Config.getInstance().getSettings().chooseStreamQuality);
|
||||
chooseStreamQuality.setOnAction((e) -> Config.getInstance().getSettings().chooseStreamQuality = chooseStreamQuality.isSelected());
|
||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||
GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||
layout.add(chooseStreamQuality, 1, 1);
|
||||
layout.add(chooseStreamQuality, 1, row++);
|
||||
|
||||
l = new Label("Maximum resolution (0 = unlimited)");
|
||||
layout.add(l, 0, row);
|
||||
List<Integer> resolutionOptions = new ArrayList<>();
|
||||
resolutionOptions.add(1080);
|
||||
resolutionOptions.add(720);
|
||||
resolutionOptions.add(600);
|
||||
resolutionOptions.add(480);
|
||||
resolutionOptions.add(0);
|
||||
maxResolution = new ComboBox<>(new ObservableListWrapper<>(resolutionOptions));
|
||||
setMaxResolutionValue();
|
||||
maxResolution.setOnAction((e) -> Config.getInstance().getSettings().maximumResolution = maxResolution.getSelectionModel().getSelectedItem());
|
||||
layout.add(maxResolution, 1, row++);
|
||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||
GridPane.setMargin(maxResolution, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||
|
||||
l = new Label("Split recordings after (minutes)");
|
||||
layout.add(l, 0, 2);
|
||||
layout.add(l, 0, row);
|
||||
List<SplitAfterOption> options = new ArrayList<>();
|
||||
options.add(new SplitAfterOption("disabled", 0));
|
||||
options.add(new SplitAfterOption("10 min", 10 * 60));
|
||||
|
@ -307,11 +328,12 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
options.add(new SplitAfterOption("30 min", 30 * 60));
|
||||
options.add(new SplitAfterOption("60 min", 60 * 60));
|
||||
splitAfter = new ComboBox<>(new ObservableListWrapper<>(options));
|
||||
layout.add(splitAfter, 1, 2);
|
||||
layout.add(splitAfter, 1, row++);
|
||||
setSplitAfterValue();
|
||||
splitAfter.setOnAction((e) -> Config.getInstance().getSettings().splitRecordings = splitAfter.getSelectionModel().getSelectedItem().getValue());
|
||||
GridPane.setMargin(l, new Insets(CHECKBOX_MARGIN, 0, 0, 0));
|
||||
GridPane.setMargin(splitAfter, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN));
|
||||
GridPane.setMargin(l, new Insets(0, 0, 0, 0));
|
||||
GridPane.setMargin(splitAfter, new Insets(0, 0, 0, CHECKBOX_MARGIN));
|
||||
maxResolution.prefWidthProperty().bind(splitAfter.widthProperty());
|
||||
|
||||
TitledPane general = new TitledPane("General", layout);
|
||||
general.setCollapsible(false);
|
||||
|
@ -327,6 +349,15 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
}
|
||||
|
||||
private void setMaxResolutionValue() {
|
||||
int value = Config.getInstance().getSettings().maximumResolution;
|
||||
for (Integer option : maxResolution.getItems()) {
|
||||
if(option == value) {
|
||||
maxResolution.getSelectionModel().select(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void showRestartRequired() {
|
||||
restartLabel.setVisible(true);
|
||||
}
|
||||
|
@ -346,6 +377,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
|
|||
recordingsDirectory.setDisable(!local);
|
||||
recordingsDirectoryButton.setDisable(!local);
|
||||
splitAfter.setDisable(!local);
|
||||
maxResolution.setDisable(!local);
|
||||
}
|
||||
|
||||
private ChangeListener<? super Boolean> createRecordingsDirectoryFocusListener() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
@ -207,10 +208,16 @@ public class ThumbCell extends StackPane {
|
|||
LOG.trace("Removing invalid resolution value for {}", model.getName());
|
||||
model.invalidateCacheEntries();
|
||||
}
|
||||
|
||||
|
||||
Thread.sleep(500);
|
||||
} catch (ExecutionException | IOException | InterruptedException e1) {
|
||||
} catch (IOException | InterruptedException e1) {
|
||||
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1);
|
||||
} catch(ExecutionException e) {
|
||||
if(e.getCause() instanceof EOFException) {
|
||||
LOG.warn("Couldn't update resolution tag for model {}. Playlist empty", model.getName());
|
||||
} else {
|
||||
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e);
|
||||
}
|
||||
} finally {
|
||||
ThumbOverviewTab.resolutionProcessing.remove(model);
|
||||
}
|
||||
|
@ -485,13 +492,13 @@ public class ThumbCell extends StackPane {
|
|||
nameBackground.setWidth(w);
|
||||
nameBackground.setHeight(20);
|
||||
topicBackground.setWidth(w);
|
||||
topicBackground.setHeight(h-nameBackground.getHeight());
|
||||
topic.prefHeight(h-25);
|
||||
topic.maxHeight(h-25);
|
||||
topicBackground.setHeight(getHeight()-nameBackground.getHeight());
|
||||
topic.prefHeight(getHeight()-25);
|
||||
topic.maxHeight(getHeight()-25);
|
||||
int margin = 4;
|
||||
topic.maxWidth(w-margin*2);
|
||||
topic.setWrappingWidth(w-margin*2);
|
||||
selectionOverlay.setWidth(w);
|
||||
selectionOverlay.setHeight(h);
|
||||
selectionOverlay.setHeight(getHeight());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import ctbrec.sites.Site;
|
|||
import javafx.application.Platform;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
|
||||
public class TokenLabel extends Label {
|
||||
|
||||
|
@ -26,10 +27,10 @@ public class TokenLabel extends Label {
|
|||
CamrecApplication.bus.register(new Object() {
|
||||
@Subscribe
|
||||
public void tokensUpdates(Map<String, Object> e) {
|
||||
if(Objects.equals("tokens", e.get("event"))) {
|
||||
if (Objects.equals("tokens", e.get("event"))) {
|
||||
tokens = (int) e.get("amount");
|
||||
updateText();
|
||||
} else if(Objects.equals("tokens.sent", e.get("event"))) {
|
||||
} else if (Objects.equals("tokens.sent", e.get("event"))) {
|
||||
int _tokens = (int) e.get("amount");
|
||||
tokens -= _tokens;
|
||||
updateText();
|
||||
|
@ -70,7 +71,10 @@ public class TokenLabel extends Label {
|
|||
update(tokens);
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
LOG.error("Couldn't retrieve account balance", e);
|
||||
Platform.runLater(() -> setText("Tokens: error"));
|
||||
Platform.runLater(() -> {
|
||||
setText("Tokens: error");
|
||||
setTooltip(new Tooltip(e.getMessage()));
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 6.7 KiB |
|
@ -41,7 +41,7 @@
|
|||
<logger name="ctbrec.recorder.Chaturbate" level="INFO" />
|
||||
<logger name="ctbrec.recorder.server.HlsServlet" level="INFO"/>
|
||||
<logger name="ctbrec.recorder.server.RecorderServlet" level="INFO"/>
|
||||
<logger name="ctbrec.ui.CookieJarImpl" level="INFO"/>
|
||||
<logger name="ctbrec.io.CookieJarImpl" level="INFO"/>
|
||||
<logger name="ctbrec.ui.ThumbOverviewTab" level="DEBUG"/>
|
||||
<logger name="org.eclipse.jetty" level="INFO" />
|
||||
<logger name="streamer" level="ERROR" />
|
||||
|
|
Loading…
Reference in New Issue