Merge branch 'dev'

This commit is contained in:
0xboobface 2018-10-06 15:01:12 +02:00
commit 176ad4889a
24 changed files with 719 additions and 314 deletions

View File

@ -1,3 +1,14 @@
1.5.3
========================
* Recording time is now converted to local timezone and formatted nicely
* The state is now displayed in the resolution tag, if the room is not
public (e.g. private, group, offline, away)
* You can now filter for public rooms with the keyword "public", if
the display of resolution is enabled
* Added possibility to switch between online and offline models in the
followed tab
* Added possibility to send tips
1.5.2
========================
* Added possibility to select multiple models in the overview tabs by

BIN
docs/img/token.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -135,7 +135,7 @@
<h2 class="text-center text-uppercase mb-0">Donate</h2>
<hr class="star-light mb-5">
<div class="row">
<div class="col-lg-8 mx-auto text-center">
<div class="col-lg-10 mx-auto text-center">
<p id="download-counter" class="lead text-center" style="display:none"></p>
<p class="lead">
CTB Recorder is free and open source. I'm a student and wrote this software in my spare time.
@ -144,37 +144,51 @@
</div>
</div>
<div class="row text-center">
<div class="col">
<p class="lead">
Buy me a coffee<br/>
<a href="https://www.buymeacoffee.com/0xboobface" target="_blank">
<img src="img/buymeacoffee/buymeacoffee.png" alt="Buy a coffee"/>
</a>
<br/><br/>
</p>
<div class="col-md-2"></div>
<div class="col-md-4">
<p class="lead">
<a href="https://chaturbate.com/in/?track=default&amp;tour=LQps&amp;campaign=55vTi&amp;room=0xb00bface" target="_blank" title="If you buy tokens by using this button, Chaturbate will award me 20% of the tokens' value for sending you over. You get the full tokens and it doesn't cost you any more!">
<img src="img/token.png" alt="Buy Chaturbate tokens"/>
</a><br/>
<input type="button" value="Buy tokens"
onclick="window.open('https://chaturbate.com/in/?track=default&amp;tour=LQps&amp;campaign=55vTi&amp;room=0xb00bface','_blank')"
title="If you buy tokens by using this button, Chaturbate will award me 20% of the tokens' value for sending you over. You get the full tokens and it doesn't cost you any more!">
</p>
</div>
<div class="col-md-4">
<p class="lead">
<a href="https://www.buymeacoffee.com/0xboobface" target="_blank">
<img src="img/buymeacoffee/buymeacoffee.png" alt="Buy a coffee" style="height: 160px; margin: 20px"/>
</a><br/>
<input type="button" value="Buy a coffee"
onclick="window.open('https://www.buymeacoffee.com/0xboobface','_blank')">
</p>
</div>
</div>
<div class="col">
<p class="lead">
Bitcoin<br/>
<img src="https://raw.githubusercontent.com/0xboobface/ctbrec/master/src/main/resources/html/bitcoin-address.png" alt="bitcoin:15sLWZon8diPqAX4UdPQU1DcaPuvZs2GgA" style="width:200px"/><br/><br/>
<input type="text" value="15sLWZon8diPqAX4UdPQU1DcaPuvZs2GgA" onClick="this.select(); document.execCommand('copy');"/>
</p>
<br/><br/>
<div class="row text-center">
<div class="col">
<p class="lead">
Bitcoin<br/>
<img src="https://raw.githubusercontent.com/0xboobface/ctbrec/master/src/main/resources/html/bitcoin-address.png" alt="bitcoin:15sLWZon8diPqAX4UdPQU1DcaPuvZs2GgA" style="width:200px"/><br/><br/>
<input type="text" value="15sLWZon8diPqAX4UdPQU1DcaPuvZs2GgA" onClick="this.select(); document.execCommand('copy');"/>
</p>
</div>
<div class="col">
<p class="lead">
Ethereum<br/>
<img src="https://raw.githubusercontent.com/0xboobface/ctbrec/master/src/main/resources/html/ethereum-address.png" alt="ethereum:0x996041638eEAE7E31f39Ef6e82068d69bA7C090e" style="width:200px"/><br/><br/>
<input type="text" value="0x996041638eEAE7E31f39Ef6e82068d69bA7C090e" onClick="this.select(); document.execCommand('copy');"/>
</p>
</div>
<div class="col">
<p class="lead">
Monero<br/>
<img src="https://raw.githubusercontent.com/0xboobface/ctbrec/master/src/main/resources/html/monero-address.png" alt="monero:448ZQZpzvT4iRNAVBr7CMQBfEbN3H8uAF2BWabtqVRckgTY3GQJkUgydjotEPaGvpzJboUpe39J8rPBkWZaUbrQa31FoSMj" style="width:200px"/><br/><br/>
<input type="text" value="448ZQZpzvT4iRNAVBr7CMQBfEbN3H8uAF2BWabtqVRckgTY3GQJkUgydjotEPaGvpzJboUpe39J8rPBkWZaUbrQa31FoSMj" onClick="this.select(); document.execCommand('copy');"/>
</p>
</div>
</div>
<div class="col">
<p class="lead">
Ethereum<br/>
<img src="https://raw.githubusercontent.com/0xboobface/ctbrec/master/src/main/resources/html/ethereum-address.png" alt="ethereum:0x996041638eEAE7E31f39Ef6e82068d69bA7C090e" style="width:200px"/><br/><br/>
<input type="text" value="0x996041638eEAE7E31f39Ef6e82068d69bA7C090e" onClick="this.select(); document.execCommand('copy');"/>
</p>
</div>
<div class="col">
<p class="lead">
Monero<br/>
<img src="https://raw.githubusercontent.com/0xboobface/ctbrec/master/src/main/resources/html/monero-address.png" alt="monero:448ZQZpzvT4iRNAVBr7CMQBfEbN3H8uAF2BWabtqVRckgTY3GQJkUgydjotEPaGvpzJboUpe39J8rPBkWZaUbrQa31FoSMj" style="width:200px"/><br/><br/>
<input type="text" value="448ZQZpzvT4iRNAVBr7CMQBfEbN3H8uAF2BWabtqVRckgTY3GQJkUgydjotEPaGvpzJboUpe39J8rPBkWZaUbrQa31FoSMj" onClick="this.select(); document.execCommand('copy');"/>
</p>
</div>
</div>
</div>
</section>

View File

@ -1,17 +1,47 @@
package ctbrec;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
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.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.recorder.StreamInfo;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class Model {
private static final transient Logger LOG = LoggerFactory.getLogger(Model.class);
private String url;
private String name;
private String preview;
private String description;
private List<String> tags = new ArrayList<>();
private boolean online = false;
private int streamUrlIndex = -1;
private int streamResolution = -1;
public String getUrl() {
return url;
@ -45,12 +75,19 @@ public class Model {
this.tags = tags;
}
public boolean isOnline() {
return online;
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
return isOnline(false);
}
public void setOnline(boolean online) {
this.online = online;
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
StreamInfo info;
if(ignoreCache) {
info = Chaturbate.INSTANCE.loadStreamInfo(getName());
LOG.trace("Model {} room status: {}", getName(), info.room_status);
} else {
info = Chaturbate.INSTANCE.getStreamInfo(getName());
}
return Objects.equals("public", info.room_status);
}
public String getDescription() {
@ -69,12 +106,46 @@ public class Model {
this.streamUrlIndex = streamUrlIndex;
}
public int getStreamResolution() {
return streamResolution;
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
int[] resolution = Chaturbate.INSTANCE.streamResolutionCache.getIfPresent(getName());
if(resolution != null) {
return Chaturbate.INSTANCE.getResolution(getName());
} else {
return new int[2];
}
}
public void setStreamResolution(int streamResolution) {
this.streamResolution = streamResolution;
public int[] getStreamResolution() throws ExecutionException {
return Chaturbate.INSTANCE.getResolution(getName());
}
/**
* Invalidates the entries in StreamInfo and resolution cache for this model
* and thus causes causes the LoadingCache to update them
*/
public void invalidateCacheEntries() {
Chaturbate.INSTANCE.streamInfoCache.invalidate(getName());
Chaturbate.INSTANCE.streamResolutionCache.invalidate(getName());
}
public String getOnlineState() throws IOException, ExecutionException {
return getOnlineState(false);
}
public String getOnlineState(boolean failFast) throws IOException, ExecutionException {
StreamInfo info = Chaturbate.INSTANCE.streamInfoCache.getIfPresent(getName());
return info != null ? info.room_status : "n/a";
}
public StreamInfo getStreamInfo() throws IOException, ExecutionException {
return Chaturbate.INSTANCE.getStreamInfo(getName());
}
public MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException, ExecutionException {
return Chaturbate.INSTANCE.getMasterPlaylist(getName());
}
public void receiveTip(int tokens) throws IOException {
Chaturbate.INSTANCE.sendTip(getName(), tokens);
}
@Override
@ -110,12 +181,168 @@ public class Model {
@Override
public String toString() {
return name;
return getName();
}
public static void main(String[] args) {
Model model = new Model();
model.name = "A";
model.url = "url";
private static class Chaturbate {
private static final transient Logger LOG = LoggerFactory.getLogger(Chaturbate.class);
public static final Chaturbate INSTANCE = new Chaturbate(HttpClient.getInstance());
private HttpClient client;
private static long lastRequest = System.currentTimeMillis();
private LoadingCache<String, StreamInfo> streamInfoCache = CacheBuilder.newBuilder()
.initialCapacity(10_000)
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(new CacheLoader<String, StreamInfo> () {
@Override
public StreamInfo load(String model) throws Exception {
return loadStreamInfo(model);
}
});
private LoadingCache<String, int[]> streamResolutionCache = CacheBuilder.newBuilder()
.initialCapacity(10_000)
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(new CacheLoader<String, int[]> () {
@Override
public int[] load(String model) throws Exception {
return loadResolution(model);
}
});
public Chaturbate(HttpClient client) {
this.client = client;
}
public void sendTip(String name, int tokens) throws IOException {
RequestBody body = new FormBody.Builder()
.add("csrfmiddlewaretoken", client.getToken())
.add("tip_amount", Integer.toString(tokens))
.add("tip_room_type", "public")
.build();
Request req = new Request.Builder()
.url("https://chaturbate.com/tipping/send_tip/"+name+"/")
.post(body)
.addHeader("Referer", "https://chaturbate.com/"+name+"/")
.addHeader("X-Requested-With", "XMLHttpRequest")
.build();
try(Response response = client.execute(req, true)) {
if(!response.isSuccessful()) {
throw new IOException(response.code() + " " + response.message());
}
}
}
private StreamInfo getStreamInfo(String modelName) throws IOException, ExecutionException {
return streamInfoCache.get(modelName);
}
private StreamInfo loadStreamInfo(String modelName) throws IOException, InterruptedException {
throttleRequests();
RequestBody body = new FormBody.Builder()
.add("room_slug", modelName)
.add("bandwidth", "high")
.build();
Request req = new Request.Builder()
.url("https://chaturbate.com/get_edge_hls_url_ajax/")
.post(body)
.addHeader("X-Requested-With", "XMLHttpRequest")
.build();
Response response = client.execute(req);
try {
if(response.isSuccessful()) {
String content = response.body().string();
LOG.trace("Raw stream info: {}", content);
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<StreamInfo> adapter = moshi.adapter(StreamInfo.class);
StreamInfo streamInfo = adapter.fromJson(content);
streamInfoCache.put(modelName, streamInfo);
return streamInfo;
} else {
int code = response.code();
String message = response.message();
throw new IOException("Server responded with " + code + " - " + message + " headers: [" + response.headers() + "]");
}
} finally {
response.close();
}
}
public int[] getResolution(String modelName) throws ExecutionException {
return streamResolutionCache.get(modelName);
}
private int[] loadResolution(String modelName) throws IOException, ParseException, PlaylistException, ExecutionException, InterruptedException {
int[] res = new int[2];
StreamInfo streamInfo = getStreamInfo(modelName);
if(!streamInfo.url.startsWith("http")) {
return res;
}
EOFException ex = null;
for(int i=0; i<2; i++) {
try {
MasterPlaylist master = getMasterPlaylist(modelName);
for (PlaylistData playlistData : master.getPlaylists()) {
if(playlistData.hasStreamInfo() && playlistData.getStreamInfo().hasResolution()) {
int h = playlistData.getStreamInfo().getResolution().height;
int w = playlistData.getStreamInfo().getResolution().width;
if(w > res[1]) {
res[0] = w;
res[1] = h;
}
}
}
ex = null;
break; // this attempt worked, exit loop
} catch(EOFException e) {
// the cause might be, that the playlist url in streaminfo is outdated,
// so let's remove it from cache and retry in the next iteration
streamInfoCache.invalidate(modelName);
ex = e;
}
}
if(ex != null) {
throw ex;
}
streamResolutionCache.put(modelName, res);
return res;
}
private void throttleRequests() throws InterruptedException {
long now = System.currentTimeMillis();
long diff = now - lastRequest;
if(diff < 500) {
Thread.sleep(diff);
}
lastRequest = now;
}
public MasterPlaylist getMasterPlaylist(String modelName) throws IOException, ParseException, PlaylistException, ExecutionException {
StreamInfo streamInfo = getStreamInfo(modelName);
return getMasterPlaylist(streamInfo);
}
public MasterPlaylist getMasterPlaylist(StreamInfo streamInfo) throws IOException, ParseException, PlaylistException {
LOG.trace("Loading master playlist {}", streamInfo.url);
Request req = new Request.Builder().url(streamInfo.url).build();
Response response = client.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();
return master;
} finally {
response.close();
}
}
}
}

View File

@ -4,7 +4,6 @@ import static ctbrec.ui.CtbrecApplication.BASE_URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
@ -27,7 +26,6 @@ public class ModelParser {
model.setPreview(HtmlParser.getTag(cellHtml, "a img").attr("src"));
model.setUrl(BASE_URI + HtmlParser.getTag(cellHtml, "a").attr("href"));
model.setDescription(HtmlParser.getText(cellHtml, "div.details ul.subject"));
model.setOnline(!Objects.equals("offline", HtmlParser.getText(cellHtml, "div.details li.cams")));
Elements tags = HtmlParser.getTags(cellHtml, "div.details ul.subject li a");
if(tags != null) {
for (Element tag : tags) {

View File

@ -1,101 +0,0 @@
package ctbrec.recorder;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.Encoding;
import com.iheartradio.m3u8.Format;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import com.iheartradio.m3u8.PlaylistParser;
import com.iheartradio.m3u8.data.MasterPlaylist;
import com.iheartradio.m3u8.data.Playlist;
import com.iheartradio.m3u8.data.PlaylistData;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.HttpClient;
import ctbrec.Model;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class Chaturbate {
private static final transient Logger LOG = LoggerFactory.getLogger(Chaturbate.class);
public static StreamInfo getStreamInfo(Model model, HttpClient client) throws IOException {
RequestBody body = new FormBody.Builder()
.add("room_slug", model.getName())
.add("bandwidth", "high")
.build();
Request req = new Request.Builder()
.url("https://chaturbate.com/get_edge_hls_url_ajax/")
.post(body)
.addHeader("X-Requested-With", "XMLHttpRequest")
.build();
Response response = client.execute(req);
try {
if(response.isSuccessful()) {
String content = response.body().string();
LOG.debug("Raw stream info: {}", content);
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<StreamInfo> adapter = moshi.adapter(StreamInfo.class);
StreamInfo streamInfo = adapter.fromJson(content);
model.setOnline(Objects.equals(streamInfo.room_status, "public"));
return streamInfo;
} else {
int code = response.code();
String message = response.message();
throw new IOException("Server responded with " + code + " - " + message + " headers: [" + response.headers() + "]");
}
} finally {
response.close();
}
}
public static int[] getResolution(Model model, HttpClient client) throws IOException, ParseException, PlaylistException {
int[] res = new int[2];
StreamInfo streamInfo = getStreamInfo(model, client);
if(!streamInfo.url.startsWith("http")) {
return res;
}
MasterPlaylist master = getMasterPlaylist(model, client);
for (PlaylistData playlistData : master.getPlaylists()) {
if(playlistData.hasStreamInfo() && playlistData.getStreamInfo().hasResolution()) {
int h = playlistData.getStreamInfo().getResolution().height;
int w = playlistData.getStreamInfo().getResolution().width;
if(w > res[1]) {
res[0] = w;
res[1] = h;
}
}
}
return res;
}
public static MasterPlaylist getMasterPlaylist(Model model, HttpClient client) throws IOException, ParseException, PlaylistException {
StreamInfo streamInfo = getStreamInfo(model, client);
return getMasterPlaylist(streamInfo, client);
}
public static MasterPlaylist getMasterPlaylist(StreamInfo streamInfo, HttpClient client) throws IOException, ParseException, PlaylistException {
LOG.trace("Loading master playlist {}", streamInfo.url);
Request req = new Request.Builder().url(streamInfo.url).build();
Response response = client.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();
return master;
} finally {
response.close();
}
}
}

View File

@ -20,7 +20,6 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -44,6 +43,7 @@ public class LocalRecorder implements Recorder {
private static final transient Logger LOG = LoggerFactory.getLogger(LocalRecorder.class);
private static final boolean IGNORE_CACHE = true;
private List<Model> followedModels = Collections.synchronizedList(new ArrayList<>());
private List<Model> models = Collections.synchronizedList(new ArrayList<>());
private Map<Model, Download> recordingProcesses = Collections.synchronizedMap(new HashMap<>());
@ -60,7 +60,6 @@ public class LocalRecorder implements Recorder {
public LocalRecorder(Config config) {
this.config = config;
config.getSettings().models.stream().forEach((m) -> {
m.setOnline(false);
models.add(m);
});
@ -94,7 +93,6 @@ public class LocalRecorder implements Recorder {
}
models.add(model);
config.getSettings().models.add(model);
onlineMonitor.interrupt();
}
}
@ -193,13 +191,6 @@ public class LocalRecorder implements Recorder {
}
}
private boolean checkIfOnline(Model model) throws IOException {
StreamInfo streamInfo = Chaturbate.getStreamInfo(model, client);
boolean online = Objects.equals(streamInfo.room_status, "public");
model.setOnline(online);
return online;
}
private void tryRestartRecording(Model model) {
if (!recording) {
// recorder is not in recording state
@ -208,7 +199,7 @@ public class LocalRecorder implements Recorder {
try {
boolean modelInRecordingList = isRecording(model);
boolean online = checkIfOnline(model);
boolean online = model.isOnline(IGNORE_CACHE);
if (modelInRecordingList && online) {
LOG.info("Restarting recording for model {}", model);
recordingProcesses.remove(model);
@ -240,7 +231,9 @@ public class LocalRecorder implements Recorder {
LOG.debug("Recording terminated for model {}", m.getName());
iterator.remove();
restart.add(m);
finishRecording(d.getDirectory());
try {
finishRecording(d.getDirectory());
} catch(NullPointerException e) {}//fail silently
}
}
for (Model m : restart) {
@ -354,7 +347,7 @@ public class LocalRecorder implements Recorder {
for (Model model : getModelsRecording()) {
try {
if (!recordingProcesses.containsKey(model)) {
boolean isOnline = checkIfOnline(model);
boolean isOnline = model.isOnline(IGNORE_CACHE);
LOG.trace("Checking online state for {}: {}", model, (isOnline ? "online" : "offline"));
if (isOnline) {
LOG.info("Model {}'s room back to public. Starting recording", model);
@ -363,7 +356,6 @@ public class LocalRecorder implements Recorder {
}
} catch (Exception e) {
LOG.error("Couldn't check if model {} is online", model.getName(), e);
model.setOnline(false);
}
}
@ -497,7 +489,7 @@ public class LocalRecorder implements Recorder {
}
recordings.add(recording);
} catch (Exception e) {
LOG.debug("Ignoring {}", rec.getAbsolutePath());
LOG.debug("Ignoring {} - {}", rec.getAbsolutePath(), e.getMessage());
}
}
}

View File

@ -48,7 +48,7 @@ public class PlaylistGenerator {
LOG.debug("Starting playlist generation for {}", directory);
// get a list of all ts files and sort them by sequence
File[] files = directory.listFiles((f) -> f.getName().endsWith(".ts"));
if(files.length == 0) {
if(files == null || files.length == 0) {
LOG.debug("{} is empty. Not going to generate a playlist", directory);
return null;
}

View File

@ -1,5 +1,6 @@
package ctbrec.recorder.download;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@ -10,6 +11,9 @@ import java.util.List;
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;
@ -27,6 +31,8 @@ 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;
@ -40,9 +46,14 @@ public abstract class AbstractHlsDownload implements Download {
String parseMaster(String url, int streamUrlIndex) throws IOException, ParseException, PlaylistException {
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
Response response = client.execute(request);
String playlistContent = "";
try {
InputStream inputStream = response.body().byteStream();
if(response.code() != 200) {
LOG.debug("HTTP response {}, {}\n{}\n{}", response.code(), response.message(), response.headers(), response.body().string());
throw new IOException("HTTP response " + response.code() + " " + response.message());
}
playlistContent = response.body().string();
InputStream inputStream = new ByteArrayInputStream(playlistContent.getBytes());
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
Playlist playlist = parser.parse();
if(playlist.hasMasterPlaylist()) {
@ -62,6 +73,9 @@ public abstract class AbstractHlsDownload implements Download {
}
}
return null;
} catch(Exception e) {
LOG.debug("Playlist: {}", playlistContent, e);
throw e;
} finally {
response.close();
}

View File

@ -25,7 +25,6 @@ import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config;
import ctbrec.HttpClient;
import ctbrec.Model;
import ctbrec.recorder.Chaturbate;
import ctbrec.recorder.StreamInfo;
import okhttp3.Request;
import okhttp3.Response;
@ -42,21 +41,21 @@ public class HlsDownload extends AbstractHlsDownload {
public void start(Model model, Config config) throws IOException {
try {
running = true;
StreamInfo streamInfo = Chaturbate.getStreamInfo(model, client);
if(!Objects.equals(streamInfo.room_status, "public")) {
throw new IOException(model.getName() +"'s room is not public");
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
String startTime = sdf.format(new Date());
Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getName());
downloadDir = FileSystems.getDefault().getPath(modelDir.toString(), startTime);
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
Files.createDirectories(downloadDir);
StreamInfo streamInfo = model.getStreamInfo();
if(!Objects.equals(streamInfo.room_status, "public")) {
throw new IOException(model.getName() +"'s room is not public");
}
String segments = parseMaster(streamInfo.url, model.getStreamUrlIndex());
if(segments != null) {
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
Files.createDirectories(downloadDir);
}
int lastSegment = 0;
int nextSegment = 0;
while(running) {

View File

@ -20,7 +20,6 @@ import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.LinkedList;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.Callable;
@ -39,7 +38,6 @@ import ctbrec.Config;
import ctbrec.HttpClient;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.recorder.Chaturbate;
import ctbrec.recorder.ProgressListener;
import ctbrec.recorder.StreamInfo;
import okhttp3.Request;
@ -48,6 +46,7 @@ import okhttp3.Response;
public class MergedHlsDownload extends AbstractHlsDownload {
private static final transient Logger LOG = LoggerFactory.getLogger(MergedHlsDownload.class);
private static final boolean IGNORE_CACHE = true;
private BlockingMultiMTSSource multiSource;
private Thread mergeThread;
private Streamer streamer;
@ -64,6 +63,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
public void start(String segmentPlaylistUri, File targetFile, ProgressListener progressListener) throws IOException {
try {
running = true;
downloadDir = targetFile.getParentFile().toPath();
mergeThread = createMergeThread(targetFile, progressListener, false);
mergeThread.start();
downloadSegments(segmentPlaylistUri, false);
@ -84,17 +84,14 @@ public class MergedHlsDownload extends AbstractHlsDownload {
try {
running = true;
startTime = ZonedDateTime.now();
StreamInfo streamInfo = Chaturbate.getStreamInfo(model, client);
if(!Objects.equals(streamInfo.room_status, "public")) {
throw new IOException(model.getName() +"'s room is not public");
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
String startTime = sdf.format(new Date());
Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getName());
downloadDir = FileSystems.getDefault().getPath(modelDir.toString(), startTime);
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
Files.createDirectories(downloadDir);
StreamInfo streamInfo = model.getStreamInfo();
if(!model.isOnline(IGNORE_CACHE)) {
throw new IOException(model.getName() +"'s room is not public");
}
targetFile = Recording.mergedFileFromDirectory(downloadDir.toFile());
@ -103,10 +100,10 @@ public class MergedHlsDownload extends AbstractHlsDownload {
LOG.debug("Splitting recordings every {} seconds", config.getSettings().splitRecordings);
target = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-00000.ts"));
}
mergeThread = createMergeThread(target, null, true);
mergeThread.start();
String segments = parseMaster(streamInfo.url, model.getStreamUrlIndex());
mergeThread = createMergeThread(target, null, true);
mergeThread.start();
if(segments != null) {
downloadSegments(segments, true);
} else {
@ -123,7 +120,9 @@ public class MergedHlsDownload extends AbstractHlsDownload {
throw new IOException("Couldn't download segment", e);
} finally {
alive = false;
streamer.stop();
if(streamer != null) {
streamer.stop();
}
LOG.debug("Download for {} terminated", model);
}
}
@ -250,6 +249,9 @@ public class MergedHlsDownload extends AbstractHlsDownload {
FileChannel channel = null;
try {
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
Files.createDirectories(downloadDir);
}
channel = FileChannel.open(targetFile.toPath(), CREATE, WRITE);
MTSSink sink = ByteChannelSink.builder().setByteChannel(channel).build();
@ -270,11 +272,8 @@ public class MergedHlsDownload extends AbstractHlsDownload {
} catch(Exception e) {
LOG.error("Error while saving stream to file", e);
} finally {
try {
channel.close();
} catch (IOException e) {
LOG.error("Error while closing file {}", targetFile);
}
closeFile(channel);
deleteEmptyRecording(targetFile);
}
});
t.setName("Segment Merger Thread");
@ -282,6 +281,27 @@ public class MergedHlsDownload extends AbstractHlsDownload {
return t;
}
private void deleteEmptyRecording(File targetFile) {
try {
if (targetFile.exists() && targetFile.length() == 0) {
Files.delete(targetFile.toPath());
Files.delete(targetFile.getParentFile().toPath());
}
} catch (IOException e) {
LOG.error("Error while deleting empty recording {}", targetFile);
}
}
private void closeFile(FileChannel channel) {
try {
if (channel != null) {
channel.close();
}
} catch (IOException e) {
LOG.error("Error while closing file channel", e);
}
}
private static class SegmentDownload implements Callable<byte[]> {
private URL url;
private HttpClient client;

View File

@ -40,6 +40,7 @@ public class CtbrecApplication extends Application {
static final transient Logger LOG = LoggerFactory.getLogger(CtbrecApplication.class);
public static final String BASE_URI = "https://chaturbate.com";
public static final String AFFILIATE_LINK = BASE_URI + "/in/?track=default&tour=LQps&campaign=55vTi&room=0xb00bface";
private Config config;
private Recorder recorder;

View File

@ -17,6 +17,7 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
public class DonateTabFx extends Tab {
@ -41,6 +42,17 @@ public class DonateTabFx extends Tab {
header.setPadding(new Insets(20, 0, 0, 0));
container.setTop(header);
ImageView tokenImage = new ImageView(getClass().getResource("/html/token.png").toString());
Button tokenButton = new Button("Buy tokens");
tokenButton.setOnAction((e) -> { DesktopIntergation.open(CtbrecApplication.AFFILIATE_LINK); });
VBox tokenBox = new VBox(5);
tokenBox.setAlignment(Pos.TOP_CENTER);
Label tokenDesc = new Label("If you buy tokens by using this button,\n"
+ "Chaturbate will award me 20% of the tokens' value for sending you over.\n"
+ "You get the full tokens and it doesn't cost you any more!");
tokenDesc.setTextAlignment(TextAlignment.CENTER);
tokenBox.getChildren().addAll(tokenImage, tokenButton, tokenDesc);
ImageView coffeeImage = new ImageView(getClass().getResource("/html/buymeacoffee-fancy.png").toString());
Button coffeeButton = new Button("Buy me a coffee");
coffeeButton.setOnMouseClicked((e) -> { DesktopIntergation.open("https://www.buymeacoffee.com/0xboobface"); });
@ -79,13 +91,18 @@ public class DonateTabFx extends Tab {
moneroBox.setAlignment(Pos.TOP_CENTER);
moneroBox.getChildren().addAll(moneroLabel, moneroAddress, moneroQrCode);
HBox topBox = new HBox(5);
topBox.setAlignment(Pos.CENTER);
topBox.setSpacing(50);
topBox.getChildren().addAll(tokenBox, buyCoffeeBox);
HBox coinBox = new HBox(5);
coinBox.setAlignment(Pos.CENTER);
coinBox.setSpacing(50);
coinBox.getChildren().addAll(bitcoinBox, ethereumBox, moneroBox);
VBox centerBox = new VBox(50);
centerBox.getChildren().addAll(buyCoffeeBox, coinBox);
centerBox.getChildren().addAll(topBox, coinBox);
container.setCenter(centerBox);
}
}

View File

@ -1,20 +1,56 @@
package ctbrec.ui;
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 FollowedTab extends ThumbOverviewTab {
private Label status;
private String onlineUrl;
private String offlineUrl;
public FollowedTab(String title, String url) {
super(title, url, true);
onlineUrl = url;
offlineUrl = url + "offline/";
status = new Label("Logging in...");
grid.getChildren().add(status);
}
@Override
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) -> {
if(online.isSelected()) {
super.url = onlineUrl;
} else {
super.url = offlineUrl;
}
updateService.restart();
});
}
@Override
protected void onSuccess() {
grid.getChildren().remove(status);

View File

@ -1,6 +1,9 @@
package ctbrec.ui;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import ctbrec.Model;
import javafx.beans.property.BooleanProperty;
@ -16,7 +19,9 @@ public class JavaFxModel extends Model {
public JavaFxModel(Model delegate) {
this.delegate = delegate;
setOnline(delegate.isOnline());
try {
onlineProperty.set(Objects.equals("public", delegate.getOnlineState(true)));
} catch (IOException | ExecutionException e) {}
}
@Override
@ -59,17 +64,6 @@ public class JavaFxModel extends Model {
delegate.setTags(tags);
}
@Override
public boolean isOnline() {
return delegate.isOnline();
}
@Override
public void setOnline(boolean online) {
delegate.setOnline(online);
this.onlineProperty.set(online);
}
@Override
public int hashCode() {
return delegate.hashCode();

View File

@ -5,9 +5,13 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@ -50,6 +54,9 @@ import javafx.util.Duration;
public class RecordedModelsTab extends Tab implements TabSelectionListener {
private static final transient Logger LOG = LoggerFactory.getLogger(RecordedModelsTab.class);
static BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
static ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 10, TimeUnit.MINUTES, queue);
private ScheduledService<List<Model>> updateService;
private Recorder recorder;
@ -147,12 +154,19 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
if(models == null) {
return;
}
queue.clear();
for (Model model : models) {
if (!observableModels.contains(model)) {
int index = observableModels.indexOf(model);
if (index == -1) {
observableModels.add(new JavaFxModel(model));
} else {
int index = observableModels.indexOf(model);
observableModels.get(index).setOnline(model.isOnline());
// make sure to update the JavaFX online property, so that the table cell is updated
JavaFxModel javaFxModel = observableModels.get(index);
threadPool.submit(() -> {
try {
javaFxModel.getOnlineProperty().set(javaFxModel.isOnline());
} catch (IOException | ExecutionException | InterruptedException e) {}
});
}
}
for (Iterator<JavaFxModel> iterator = observableModels.iterator(); iterator.hasNext();) {
@ -233,11 +247,20 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
}
private void switchStreamSource(JavaFxModel fxModel) {
if(!fxModel.isOnline()) {
try {
if(!fxModel.isOnline()) {
Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION);
alert.setTitle("Switch resolution");
alert.setHeaderText("Couldn't switch stream resolution");
alert.setContentText("The resolution can only be changed, when the model is online");
alert.showAndWait();
return;
}
} catch (IOException | ExecutionException | InterruptedException e1) {
Alert alert = new AutosizeAlert(Alert.AlertType.INFORMATION);
alert.setTitle("Switch resolution");
alert.setHeaderText("Couldn't switch stream resolution");
alert.setContentText("The resolution can only be changed, when the model is online");
alert.setContentText("An error occured while checking, if the model is online");
alert.showAndWait();
return;
}

View File

@ -9,6 +9,11 @@ import java.io.IOException;
import java.net.URL;
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.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@ -31,6 +36,7 @@ import ctbrec.Recording.STATUS;
import ctbrec.recorder.Recorder;
import ctbrec.recorder.download.MergedHlsDownload;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.ScheduledService;
@ -93,7 +99,12 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
name.setPrefWidth(200);
name.setCellValueFactory(new PropertyValueFactory<JavaFxRecording, String>("modelName"));
TableColumn<JavaFxRecording, String> date = new TableColumn<>("Date");
date.setCellValueFactory(new PropertyValueFactory<JavaFxRecording, String>("startDate"));
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));
});
date.setPrefWidth(200);
TableColumn<JavaFxRecording, String> status = new TableColumn<>("Status");
status.setCellValueFactory((cdf) -> cdf.getValue().getStatusProperty());

View File

@ -187,23 +187,29 @@ public class SettingsTab extends Tab implements TabSelectionListener {
GridPane.setColumnSpan(password, 2);
layout.add(password, 1, 1);
Button createAccount = new Button("Create new Account");
createAccount.setOnAction((e) -> DesktopIntergation.open(CtbrecApplication.AFFILIATE_LINK));
layout.add(createAccount, 1, 2);
GridPane.setColumnSpan(createAccount, 2);
l = new Label("Record all followed models");
layout.add(l, 0, 2);
layout.add(l, 0, 3);
autoRecordFollowed = new CheckBox();
autoRecordFollowed.setSelected(Config.getInstance().getSettings().recordFollowed);
autoRecordFollowed.setOnAction((e) -> {
Config.getInstance().getSettings().recordFollowed = autoRecordFollowed.isSelected();
showRestartRequired();
});
layout.add(autoRecordFollowed, 1, 2);
layout.add(autoRecordFollowed, 1, 3);
Label warning = new Label("Don't do this, if you follow many models. You have been warned ;) !");
warning.setTextFill(Color.RED);
layout.add(warning, 2, 2);
layout.add(warning, 2, 3);
GridPane.setMargin(l, new Insets(3, 0, 0, 0));
GridPane.setMargin(warning, new Insets(3, 0, 0, 0));
GridPane.setMargin(autoRecordFollowed, new Insets(3, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(username, new Insets(0, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(password, new Insets(0, 0, 0, CHECKBOX_MARGIN));
GridPane.setMargin(createAccount, new Insets(0, 0, 0, CHECKBOX_MARGIN));
ctb = new TitledPane("Chaturbate", layout);
ctb.setCollapsible(false);

View File

@ -14,7 +14,6 @@ import com.iheartradio.m3u8.data.PlaylistData;
import ctbrec.HttpClient;
import ctbrec.Model;
import ctbrec.recorder.Chaturbate;
import ctbrec.recorder.StreamInfo;
import ctbrec.recorder.download.StreamSource;
import javafx.concurrent.Task;
@ -27,8 +26,9 @@ public class StreamSourceSelectionDialog {
Task<List<StreamSource>> selectStreamSource = new Task<List<StreamSource>>() {
@Override
protected List<StreamSource> call() throws Exception {
StreamInfo streamInfo = Chaturbate.getStreamInfo(model, client);
MasterPlaylist masterPlaylist = Chaturbate.getMasterPlaylist(streamInfo, client);
model.invalidateCacheEntries();
StreamInfo streamInfo = model.getStreamInfo();
MasterPlaylist masterPlaylist = model.getMasterPlaylist();
List<StreamSource> sources = new ArrayList<>();
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
if (playlist.hasStreamInfo()) {
@ -53,6 +53,7 @@ public class StreamSourceSelectionDialog {
ChoiceDialog<StreamSource> choiceDialog = new ChoiceDialog<StreamSource>(sources.get(sources.size()-1), sources);
choiceDialog.setTitle("Stream Quality");
choiceDialog.setHeaderText("Select your preferred stream quality");
choiceDialog.setResizable(true);
Optional<StreamSource> selectedSource = choiceDialog.showAndWait();
if(selectedSource.isPresent()) {
int index = sources.indexOf(selectedSource.get());

View File

@ -1,21 +1,16 @@
package ctbrec.ui;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config;
import ctbrec.HttpClient;
import ctbrec.Model;
import ctbrec.recorder.Chaturbate;
import ctbrec.recorder.Recorder;
import ctbrec.recorder.StreamInfo;
import javafx.animation.FadeTransition;
@ -38,6 +33,7 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
@ -55,12 +51,11 @@ public class ThumbCell extends StackPane {
public static int width = 180;
private static final Duration ANIMATION_DURATION = new Duration(250);
// this acts like a cache, once the stream resolution for a model has been determined, we don't do it again (until ctbrec is restarted)
private static Map<String, int[]> resolutions = new HashMap<>();
private Model model;
private ImageView iv;
private Rectangle resolutionBackground;
private Paint resolutionOnlineColor = new Color(0.22, 0.8, 0.29, 1);
private Color resolutionOfflineColor = new Color(0.8, 0.28, 0.28, 1);
private Rectangle nameBackground;
private Rectangle topicBackground;
private Rectangle selectionOverlay;
@ -106,7 +101,7 @@ public class ThumbCell extends StackPane {
getChildren().add(topicBackground);
resolutionBackground = new Rectangle(34, 16);
resolutionBackground.setFill(new Color(0.22, 0.8, 0.29, 1));
resolutionBackground.setFill(resolutionOnlineColor );
resolutionBackground.setVisible(false);
resolutionBackground.setArcHeight(5);
resolutionBackground.setArcWidth(resolutionBackground.getArcHeight());
@ -192,65 +187,59 @@ public class ThumbCell extends StackPane {
private void determineResolution() {
if(ThumbOverviewTab.resolutionProcessing.contains(model)) {
LOG.debug("Already fetching resolution for model {}. Queue size {}", model.getName(), ThumbOverviewTab.resolutionProcessing.size());
LOG.trace("Already fetching resolution for model {}. Queue size {}", model.getName(), ThumbOverviewTab.resolutionProcessing.size());
return;
}
ThumbOverviewTab.resolutionProcessing.add(model);
int[] res = resolutions.get(model.getName());
if(res == null) {
ThumbOverviewTab.threadPool.submit(() -> {
try {
Thread.sleep(500); // throttle down, so that we don't do too many requests
int[] resolution = Chaturbate.getResolution(model, client);
resolutions.put(model.getName(), resolution);
if (resolution[1] > 0) {
LOG.trace("Model resolution {} {}x{}", model.getName(), resolution[0], resolution[1]);
LOG.trace("Resolution queue size: {}", ThumbOverviewTab.queue.size());
final int w = resolution[1];
Platform.runLater(() -> {
String _res = Integer.toString(w);
resolutionTag.setText(_res);
resolutionTag.setVisible(true);
resolutionBackground.setVisible(true);
resolutionBackground.setWidth(resolutionTag.getBoundsInLocal().getWidth() + 4);
model.setStreamResolution(w);
});
}
} catch (IOException | ParseException | PlaylistException | InterruptedException e) {
LOG.error("Coulnd't get resolution for model {}", model, e);
} finally {
ThumbOverviewTab.resolutionProcessing.remove(model);
}
});
} else {
ThumbOverviewTab.resolutionProcessing.remove(model);
String _res = Integer.toString(res[1]);
model.setStreamResolution(res[1]);
Platform.runLater(() -> {
resolutionTag.setText(_res);
resolutionTag.setVisible(true);
resolutionBackground.setVisible(true);
resolutionBackground.setWidth(resolutionTag.getBoundsInLocal().getWidth() + 4);
});
ThumbOverviewTab.threadPool.submit(() -> {
try {
ThumbOverviewTab.resolutionProcessing.add(model);
int[] resolution = model.getStreamResolution();
updateResolutionTag(resolution);
// the model is online, but the resolution is 0. probably something went wrong
// when we first requested the stream info, so we remove this invalid value from the "cache"
// so that it is requested again
if(model.isOnline() && res[1] == 0) {
ThumbOverviewTab.threadPool.submit(() -> {
try {
Chaturbate.getStreamInfo(model, client);
if(model.isOnline()) {
LOG.debug("Removing invalid resolution value for {}", model.getName());
resolutions.remove(model.getName());
}
} catch (IOException e) {
LOG.error("Coulnd't get resolution for model {}", model, e);
// the model is online, but the resolution is 0. probably something went wrong
// when we first requested the stream info, so we remove this invalid value from the "cache"
// so that it is requested again
try {
if (model.isOnline() && resolution[1] == 0) {
LOG.debug("Removing invalid resolution value for {}", model.getName());
model.invalidateCacheEntries();
}
});
} catch (IOException | ExecutionException | InterruptedException e) {
LOG.error("Coulnd't get resolution for model {}", model, e);
}
} catch (ExecutionException e1) {
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1);
} catch (IOException e1) {
LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1);
} finally {
ThumbOverviewTab.resolutionProcessing.remove(model);
}
});
}
private void updateResolutionTag(int[] resolution) throws IOException, ExecutionException {
String _res = "n/a";
Paint resolutionBackgroundColor = resolutionOnlineColor;
String state = model.getOnlineState();
if ("public".equals(state)) {
LOG.trace("Model resolution {} {}x{}", model.getName(), resolution[0], resolution[1]);
LOG.trace("Resolution queue size: {}", ThumbOverviewTab.queue.size());
final int w = resolution[1];
_res = w > 0 ? Integer.toString(w) : state;
} else {
_res = model.getOnlineState();
resolutionBackgroundColor = resolutionOfflineColor;
}
final String resText = _res;
final Paint c = resolutionBackgroundColor;
Platform.runLater(() -> {
resolutionTag.setText(resText);
resolutionTag.setVisible(true);
resolutionBackground.setVisible(true);
resolutionBackground.setWidth(resolutionTag.getBoundsInLocal().getWidth() + 4);
resolutionBackground.setFill(c);
});
}
private void setImage(String url) {
@ -288,8 +277,8 @@ public class ThumbCell extends StackPane {
// or maybe not, because the player should automatically switch between resolutions depending on the
// network bandwidth
try {
StreamInfo streamInfo = Chaturbate.getStreamInfo(model, client);
if(streamInfo.room_status.equals("public")) {
if(model.isOnline(true)) {
StreamInfo streamInfo = model.getStreamInfo();
LOG.debug("Playing {}", streamInfo.url);
Player.play(streamInfo.url);
} else {
@ -298,7 +287,7 @@ public class ThumbCell extends StackPane {
alert.setHeaderText("Room is currently not public");
alert.showAndWait();
}
} catch (IOException e1) {
} catch (IOException | ExecutionException | InterruptedException e1) {
LOG.error("Couldn't get stream information for model {}", model, e1);
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
@ -350,8 +339,10 @@ public class ThumbCell extends StackPane {
try {
if(start) {
recorder.startRecording(model);
setRecording(true);
} else {
recorder.stopRecording(model);
setRecording(false);
}
} catch (Exception e1) {
LOG.error("Couldn't start/stop recording", e1);
@ -438,9 +429,7 @@ public class ThumbCell extends StackPane {
//this.model = model;
this.model.setName(model.getName());
this.model.setDescription(model.getDescription());
this.model.setOnline(model.isOnline());
this.model.setPreview(model.getPreview());
this.model.setStreamResolution(model.getStreamResolution());
this.model.setTags(model.getTags());
this.model.setUrl(model.getUrl());
@ -459,8 +448,18 @@ public class ThumbCell extends StackPane {
setRecording(recorder.isRecording(model));
setImage(model.getPreview());
topic.setText(model.getDescription());
//Tooltip t = new Tooltip(model.getDescription());
//Tooltip.install(this, t);
// ThumbOverviewTab.threadPool.submit(() -> {
// StreamInfo streamInfo;
// try {
// streamInfo = Chaturbate.INSTANCE.getStreamInfo(model);
// model.setOnline(streamInfo.room_status.equals("public"));
// model.setOnlineState(streamInfo.room_status);
// } catch (IOException | ExecutionException e) {
// LOG.error("Couldn't retrieve stream information for model {}", model.getName());
// }
// });
if(Config.getInstance().getSettings().determineResolution) {
determineResolution();
} else {

View File

@ -11,6 +11,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
@ -77,6 +78,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
String url;
boolean loginRequired;
HttpClient client = HttpClient.getInstance();
HBox pagination;
int page = 1;
TextField pageInput = new TextField(Integer.toString(page));
Button pagePrev = new Button("");
@ -95,7 +97,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
initializeUpdateService();
}
private void createGui() {
void createGui() {
grid.setPadding(new Insets(5));
grid.setHgap(5);
grid.setVgap(5);
@ -107,12 +109,16 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
gridLock.lock();
try {
filter();
moveActiveRecordingsToFront();
} finally {
gridLock.unlock();
}
});
search.setTooltip(new Tooltip("Filter the models by their name, stream description or #hashtags.\n\n"+""
+ "If the display of stream resolution is enabled, you can even filter by resolution. Try \"1080\" or \">720\""));
Tooltip searchTooltip = new Tooltip("Filter the models by their name, stream description or #hashtags.\n\n"
+ "If the display of stream resolution is enabled, you can even filter for public rooms or by resolution.\n\n"
+ "Try \"1080\" or \">720\" or \"public\"");
search.setTooltip(searchTooltip);
BorderPane.setMargin(search, new Insets(5));
scrollPane.setContent(grid);
@ -120,7 +126,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
scrollPane.setFitToWidth(true);
BorderPane.setMargin(scrollPane, new Insets(5));
HBox pagination = new HBox(5);
pagination = new HBox(5);
pagination.getChildren().add(pagePrev);
pagination.getChildren().add(pageNext);
pagination.getChildren().add(pageInput);
@ -320,6 +326,35 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
clipboard.setContent(content);
});
MenuItem sendTip = new MenuItem("Send Tip");
sendTip.setOnAction((e) -> {
TipDialog tipDialog = new TipDialog(cell.getModel());
tipDialog.showAndWait();
String tipText = tipDialog.getResult();
if(tipText != null) {
if(tipText.matches("[1-9]\\d*")) {
int tokens = Integer.parseInt(tipText);
try {
cell.getModel().receiveTip(tokens);
} catch (IOException e1) {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't send tip");
alert.setContentText("An error occured while sending tip: " + e1.getLocalizedMessage());
alert.showAndWait();
}
} else {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't send tip");
alert.setContentText("You entered an invalid amount of tokens");
alert.showAndWait();
}
}
});
String username = Config.getInstance().getSettings().username;
sendTip.setDisable(username == null || username.trim().isEmpty());
// check, if other cells are selected, too. in that case, we have to disable menu item, which make sense only for
// single selections. but only do that, if the popup has been triggered on a selected cell. otherwise remove the
// selection and show the normal menu
@ -329,6 +364,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
openInPlayer.setDisable(true);
}
copyUrl.setDisable(true);
sendTip.setDisable(true);
} else {
removeSelection();
}
@ -339,7 +375,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
contextMenu.setHideOnEscape(true);
contextMenu.setAutoFix(true);
MenuItem followOrUnFollow = this instanceof FollowedTab ? unfollow : follow;
contextMenu.getItems().addAll(openInPlayer, startStop , followOrUnFollow, copyUrl);
contextMenu.getItems().addAll(openInPlayer, startStop , followOrUnFollow, copyUrl, sendTip);
return contextMenu;
}
@ -479,32 +515,41 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
}
private boolean matches(Model m, String filter) {
String[] tokens = filter.split(" ");
StringBuilder searchTextBuilder = new StringBuilder(m.getName());
searchTextBuilder.append(' ');
for (String tag : m.getTags()) {
searchTextBuilder.append(tag).append(' ');
}
searchTextBuilder.append(m.getStreamResolution());
String searchText = searchTextBuilder.toString().trim();
//LOG.debug("{} -> {}", m.getName(), searchText);
boolean tokensMissing = false;
for (String token : tokens) {
if(token.matches(">\\d+")) {
int res = Integer.parseInt(token.substring(1));
if(m.getStreamResolution() < res) {
tokensMissing = true;
}
} else if(token.matches("<\\d+")) {
int res = Integer.parseInt(token.substring(1));
if(m.getStreamResolution() > res) {
tokensMissing = true;
}
} else if(!searchText.contains(token)) {
tokensMissing = true;
try {
String[] tokens = filter.split(" ");
StringBuilder searchTextBuilder = new StringBuilder(m.getName());
searchTextBuilder.append(' ');
for (String tag : m.getTags()) {
searchTextBuilder.append(tag).append(' ');
}
int[] resolution = m.getStreamResolution(true);
searchTextBuilder.append(resolution[1]);
String searchText = searchTextBuilder.toString().trim();
boolean tokensMissing = false;
for (String token : tokens) {
if(token.matches(">\\d+")) {
int res = Integer.parseInt(token.substring(1));
if(resolution[1] < res) {
tokensMissing = true;
}
} else if(token.matches("<\\d+")) {
int res = Integer.parseInt(token.substring(1));
if(resolution[1] > res) {
tokensMissing = true;
}
} else if(token.equals("public")) {
if(!m.getOnlineState(true).equals(token)) {
tokensMissing = true;
}
} else if(!searchText.contains(token)) {
tokensMissing = true;
}
}
return !tokensMissing;
} catch (NumberFormatException | ExecutionException | IOException e) {
LOG.error("Error while filtering model list", e);
return false;
}
return !tokensMissing;
}
private ScheduledService<List<Model>> createUpdateService() {
@ -550,6 +595,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
@Override
public void selected() {
queue.clear();
if(updateService != null) {
State s = updateService.getState();
if (s != State.SCHEDULED && s != State.RUNNING) {

View File

@ -0,0 +1,97 @@
package ctbrec.ui;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.HttpClient;
import ctbrec.Model;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TextInputDialog;
import okhttp3.Request;
import okhttp3.Response;
public class TipDialog extends TextInputDialog {
private static final transient Logger LOG = LoggerFactory.getLogger(TipDialog.class);
public TipDialog(Model model) {
setTitle("Send Tip");
loadTokenBalance();
setHeaderText("Loading token balance…");
setContentText("Amount of tokens to tip:");
setResizable(true);
getEditor().setDisable(true);
}
private void loadTokenBalance() {
Task<Integer> task = new Task<Integer>() {
@Override
protected Integer call() throws Exception {
String username = Config.getInstance().getSettings().username;
if (username == null || username.trim().isEmpty()) {
throw new IOException("Not logged in");
}
String url = "https://chaturbate.com/p/" + username + "/";
HttpClient client = HttpClient.getInstance();
Request req = new Request.Builder().url(url).build();
Response resp = client.execute(req, true);
if (resp.isSuccessful()) {
String profilePage = resp.body().string();
String tokenText = HtmlParser.getText(profilePage, "span.tokencount");
int tokens = Integer.parseInt(tokenText);
return tokens;
} else {
throw new IOException("HTTP response: " + resp.code() + " - " + resp.message());
}
}
@Override
protected void done() {
try {
int tokens = get();
Platform.runLater(() -> {
if (tokens <= 0) {
String msg = "Do you want to buy tokens now?\n\nIf you agree, Chaturbate will open in a browser. "
+ "The used address is an affiliate link, which supports me, but doesn't cost you anything more.";
Alert buyTokens = new AutosizeAlert(Alert.AlertType.CONFIRMATION, msg, ButtonType.NO, ButtonType.YES);
buyTokens.setTitle("No tokens");
buyTokens.setHeaderText("You don't have any tokens");
buyTokens.showAndWait();
TipDialog.this.close();
if(buyTokens.getResult() == ButtonType.YES) {
DesktopIntergation.open(CtbrecApplication.AFFILIATE_LINK);
}
} else {
getEditor().setDisable(false);
setHeaderText("Current token balance: " + tokens);
}
});
} catch (InterruptedException | ExecutionException e) {
LOG.error("Couldn't retrieve account balance", e);
showErrorDialog(e);
}
}
};
new Thread(task).start();
}
private void showErrorDialog(Throwable throwable) {
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
alert.setTitle("Error");
alert.setHeaderText("Couldn't retrieve token balance");
alert.setContentText("Error while loading your token balance: " + throwable.getLocalizedMessage());
alert.showAndWait();
TipDialog.this.close();
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.