Merge branch 'dev'
This commit is contained in:
commit
176ad4889a
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -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
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -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&tour=LQps&campaign=55vTi&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&tour=LQps&campaign=55vTi&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>
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
Loading…
Reference in New Issue