forked from j62/ctbrec
Add interface Model to abstract from different implementations
Model is the common interface for the implementations of all sites. At the moment only ChaturbateModel exists.
This commit is contained in:
parent
534e9905eb
commit
43c29758c4
|
@ -0,0 +1,132 @@
|
|||
package ctbrec;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
|
||||
public abstract class AbstractModel implements Model {
|
||||
|
||||
private String url;
|
||||
private String name;
|
||||
private String preview;
|
||||
private String description;
|
||||
private List<String> tags = new ArrayList<>();
|
||||
private int streamUrlIndex = -1;
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPreview() {
|
||||
return preview;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPreview(String preview) {
|
||||
this.preview = preview;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTags(List<String> tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStreamUrlIndex() {
|
||||
return streamUrlIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStreamUrlIndex(int streamUrlIndex) {
|
||||
this.streamUrlIndex = streamUrlIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSegmentPlaylistUrl() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
List<StreamSource> streamSources = getStreamSources();
|
||||
String url = null;
|
||||
if(getStreamUrlIndex() >= 0 && getStreamUrlIndex() < streamSources.size()) {
|
||||
url = streamSources.get(getStreamUrlIndex()).getMediaPlaylistUrl();
|
||||
} else {
|
||||
Collections.sort(streamSources);
|
||||
url = streamSources.get(streamSources.size()-1).getMediaPlaylistUrl();
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((getName() == null) ? 0 : getName().hashCode());
|
||||
result = prime * result + ((getUrl() == null) ? 0 : getUrl().hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (!(obj instanceof Model))
|
||||
return false;
|
||||
Model other = (Model) obj;
|
||||
if (getName() == null) {
|
||||
if (other.getName() != null)
|
||||
return false;
|
||||
} else if (!getName().equals(other.getName()))
|
||||
return false;
|
||||
if (getUrl() == null) {
|
||||
if (other.getUrl() != null)
|
||||
return false;
|
||||
} else if (!getUrl().equals(other.getUrl()))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,285 @@
|
|||
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 ctbrec.recorder.download.StreamSource;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
public class ChaturbateModel extends AbstractModel {
|
||||
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateModel.class);
|
||||
|
||||
@Override
|
||||
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
||||
return isOnline(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
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 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 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
invalidateCacheEntries();
|
||||
StreamInfo streamInfo = getStreamInfo();
|
||||
MasterPlaylist masterPlaylist = getMasterPlaylist();
|
||||
List<StreamSource> sources = new ArrayList<>();
|
||||
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||
src.height = playlist.getStreamInfo().getResolution().height;
|
||||
String masterUrl = streamInfo.url;
|
||||
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||
String segmentUri = baseUrl + playlist.getUri();
|
||||
src.mediaPlaylistUrl = segmentUri;
|
||||
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||
sources.add(src);
|
||||
}
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -39,7 +39,9 @@ public class Config {
|
|||
}
|
||||
|
||||
private void load() throws FileNotFoundException, IOException {
|
||||
Moshi moshi = new Moshi.Builder().build();
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Model.class, new ModelAdapter())
|
||||
.build();
|
||||
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class);
|
||||
File configDir = OS.getConfigDir();
|
||||
File configFile = new File(configDir, filename);
|
||||
|
@ -73,7 +75,9 @@ public class Config {
|
|||
}
|
||||
|
||||
public void save() throws IOException {
|
||||
Moshi moshi = new Moshi.Builder().build();
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Model.class, new ModelAdapter())
|
||||
.build();
|
||||
JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).indent(" ");
|
||||
String json = adapter.toJson(settings);
|
||||
File configDir = OS.getConfigDir();
|
||||
|
|
|
@ -1,350 +1,31 @@
|
|||
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;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
|
||||
public class Model {
|
||||
public interface Model {
|
||||
public String getUrl();
|
||||
public void setUrl(String url);
|
||||
public String getName();
|
||||
public void setName(String name);
|
||||
public String getPreview();
|
||||
public void setPreview(String preview);
|
||||
public List<String> getTags();
|
||||
public void setTags(List<String> tags);
|
||||
public String getDescription();
|
||||
public void setDescription(String description);
|
||||
public int getStreamUrlIndex();
|
||||
public void setStreamUrlIndex(int streamUrlIndex);
|
||||
public boolean isOnline() throws IOException, ExecutionException, InterruptedException;
|
||||
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException;
|
||||
public String getOnlineState(boolean failFast) throws IOException, ExecutionException;
|
||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException;
|
||||
public String getSegmentPlaylistUrl() throws IOException, ExecutionException, ParseException, PlaylistException;
|
||||
|
||||
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 int streamUrlIndex = -1;
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getPreview() {
|
||||
return preview;
|
||||
}
|
||||
|
||||
public void setPreview(String preview) {
|
||||
this.preview = preview;
|
||||
}
|
||||
|
||||
public List<String> getTags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
public void setTags(List<String> tags) {
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
||||
return isOnline(false);
|
||||
}
|
||||
|
||||
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() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getStreamUrlIndex() {
|
||||
return streamUrlIndex;
|
||||
}
|
||||
|
||||
public void setStreamUrlIndex(int streamUrlIndex) {
|
||||
this.streamUrlIndex = streamUrlIndex;
|
||||
}
|
||||
|
||||
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 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
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((getName() == null) ? 0 : getName().hashCode());
|
||||
result = prime * result + ((getUrl() == null) ? 0 : getUrl().hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (!(obj instanceof Model))
|
||||
return false;
|
||||
Model other = (Model) obj;
|
||||
if (getName() == null) {
|
||||
if (other.getName() != null)
|
||||
return false;
|
||||
} else if (!getName().equals(other.getName()))
|
||||
return false;
|
||||
if (getUrl() == null) {
|
||||
if (other.getUrl() != null)
|
||||
return false;
|
||||
} else if (!getUrl().equals(other.getUrl()))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package ctbrec;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.JsonReader;
|
||||
import com.squareup.moshi.JsonReader.Token;
|
||||
import com.squareup.moshi.JsonWriter;
|
||||
|
||||
public class ModelAdapter extends JsonAdapter<Model> {
|
||||
|
||||
@Override
|
||||
public Model fromJson(JsonReader reader) throws IOException {
|
||||
reader.beginObject();
|
||||
String name = null;
|
||||
String description = null;
|
||||
String url = null;
|
||||
String type = null;
|
||||
while(reader.hasNext()) {
|
||||
Token token = reader.peek();
|
||||
if(token == Token.NAME) {
|
||||
String key = reader.nextName();
|
||||
if(key.equals("name")) {
|
||||
name = reader.nextString();
|
||||
} else if(key.equals("description")) {
|
||||
description = reader.nextString();
|
||||
} else if(key.equals("url")) {
|
||||
url = reader.nextString();
|
||||
} else if(key.equals("type")) {
|
||||
type = reader.nextString();
|
||||
}
|
||||
} else {
|
||||
reader.skipValue();
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
|
||||
try {
|
||||
Class<?> modelClass = Class.forName(Optional.ofNullable(type).orElse(ChaturbateModel.class.getName()));
|
||||
Model model = (Model) modelClass.newInstance();
|
||||
model.setName(name);
|
||||
model.setDescription(description);
|
||||
model.setUrl(url);
|
||||
return model;
|
||||
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
|
||||
throw new IOException("Couldn't instantiate mode class [" + type + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toJson(JsonWriter writer, Model model) throws IOException {
|
||||
writer.beginObject();
|
||||
writer.name("type").value(model.getClass().getName());
|
||||
writeValueIfSet(writer, "name", model.getName());
|
||||
writeValueIfSet(writer, "description", model.getDescription());
|
||||
writeValueIfSet(writer, "url", model.getUrl());
|
||||
writer.endObject();
|
||||
}
|
||||
|
||||
private void writeValueIfSet(JsonWriter writer, String name, String value) throws IOException {
|
||||
if(value != null) {
|
||||
writer.name(name).value(value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -15,13 +15,13 @@ import ctbrec.ui.HtmlParser;
|
|||
public class ModelParser {
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(ModelParser.class);
|
||||
|
||||
public static List<Model> parseModels(String html) {
|
||||
List<Model> models = new ArrayList<>();
|
||||
public static List<ChaturbateModel> parseModels(String html) {
|
||||
List<ChaturbateModel> models = new ArrayList<>();
|
||||
Elements cells = HtmlParser.getTags(html, "ul.list > li");
|
||||
for (Element cell : cells) {
|
||||
String cellHtml = cell.html();
|
||||
try {
|
||||
Model model = new Model();
|
||||
ChaturbateModel model = new ChaturbateModel();
|
||||
model.setName(HtmlParser.getText(cellHtml, "div.title > a").trim());
|
||||
model.setPreview(HtmlParser.getTag(cellHtml, "a img").attr("src"));
|
||||
model.setUrl(BASE_URI + HtmlParser.getTag(cellHtml, "a").attr("href"));
|
||||
|
|
|
@ -23,6 +23,7 @@ public class Settings {
|
|||
public String username = "";
|
||||
public String password = "";
|
||||
public String lastDownloadDir = "";
|
||||
|
||||
public List<Model> models = new ArrayList<Model>();
|
||||
public boolean determineResolution = false;
|
||||
public boolean requireAuthentication = false;
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
|
|||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
|
||||
import ctbrec.ChaturbateModel;
|
||||
import ctbrec.Config;
|
||||
import ctbrec.HttpClient;
|
||||
import ctbrec.Model;
|
||||
|
@ -222,7 +223,7 @@ public class LocalRecorder implements Recorder {
|
|||
public void run() {
|
||||
running = true;
|
||||
while (running) {
|
||||
List<Model> restart = new ArrayList<Model>();
|
||||
List<Model> restart = new ArrayList<>();
|
||||
for (Iterator<Entry<Model, Download>> iterator = recordingProcesses.entrySet().iterator(); iterator.hasNext();) {
|
||||
Entry<Model, Download> entry = iterator.next();
|
||||
Model m = entry.getKey();
|
||||
|
@ -269,10 +270,10 @@ public class LocalRecorder implements Recorder {
|
|||
Request request = new Request.Builder().url(url).build();
|
||||
Response response = client.execute(request, true);
|
||||
if (response.isSuccessful()) {
|
||||
List<Model> followed = ModelParser.parseModels(response.body().string());
|
||||
List<ChaturbateModel> followed = ModelParser.parseModels(response.body().string());
|
||||
response.close();
|
||||
followedModels.clear();
|
||||
for (Model model : followed) {
|
||||
for (ChaturbateModel model : followed) {
|
||||
if (!followedModels.contains(model) && !models.contains(model)) {
|
||||
LOG.info("Model {} added", model);
|
||||
followedModels.add(model);
|
||||
|
|
|
@ -19,6 +19,7 @@ import ctbrec.Hmac;
|
|||
import ctbrec.HttpClient;
|
||||
import ctbrec.InstantJsonAdapter;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelAdapter;
|
||||
import ctbrec.Recording;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.Request;
|
||||
|
@ -33,6 +34,7 @@ public class RemoteRecorder implements Recorder {
|
|||
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
|
||||
private Moshi moshi = new Moshi.Builder()
|
||||
.add(Instant.class, new InstantJsonAdapter())
|
||||
.add(Model.class, new ModelAdapter())
|
||||
.build();
|
||||
private JsonAdapter<ModelListResponse> modelListResponseAdapter = moshi.adapter(ModelListResponse.class);
|
||||
private JsonAdapter<RecordingListResponse> recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package ctbrec.recorder.download;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -11,18 +10,13 @@ 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;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
import com.iheartradio.m3u8.PlaylistParser;
|
||||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||
import com.iheartradio.m3u8.data.MediaPlaylist;
|
||||
import com.iheartradio.m3u8.data.Playlist;
|
||||
import com.iheartradio.m3u8.data.PlaylistData;
|
||||
import com.iheartradio.m3u8.data.TrackData;
|
||||
|
||||
import ctbrec.HttpClient;
|
||||
|
@ -31,8 +25,6 @@ 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;
|
||||
|
@ -43,44 +35,6 @@ public abstract class AbstractHlsDownload implements Download {
|
|||
this.client = client;
|
||||
}
|
||||
|
||||
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 {
|
||||
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()) {
|
||||
MasterPlaylist master = playlist.getMasterPlaylist();
|
||||
PlaylistData bestQuality = null;
|
||||
if(streamUrlIndex >= 0 && streamUrlIndex < master.getPlaylists().size()) {
|
||||
bestQuality = master.getPlaylists().get(streamUrlIndex);
|
||||
} else {
|
||||
bestQuality = master.getPlaylists().get(master.getPlaylists().size()-1);
|
||||
}
|
||||
String uri = bestQuality.getUri();
|
||||
if(!uri.startsWith("http")) {
|
||||
String masterUrl = url;
|
||||
String baseUri = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||
String segmentUri = baseUri + uri;
|
||||
return segmentUri;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch(Exception e) {
|
||||
LOG.debug("Playlist: {}", playlistContent, e);
|
||||
throw e;
|
||||
} finally {
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
|
||||
SegmentPlaylist getNextSegments(String segments) throws IOException, ParseException, PlaylistException {
|
||||
URL segmentsUrl = new URL(segments);
|
||||
Request request = new Request.Builder().url(segmentsUrl).addHeader("connection", "keep-alive").build();
|
||||
|
|
|
@ -25,7 +25,6 @@ import com.iheartradio.m3u8.PlaylistException;
|
|||
import ctbrec.Config;
|
||||
import ctbrec.HttpClient;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.recorder.StreamInfo;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
|
@ -46,12 +45,11 @@ public class HlsDownload extends AbstractHlsDownload {
|
|||
Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getName());
|
||||
downloadDir = FileSystems.getDefault().getPath(modelDir.toString(), startTime);
|
||||
|
||||
StreamInfo streamInfo = model.getStreamInfo();
|
||||
if(!Objects.equals(streamInfo.room_status, "public")) {
|
||||
if(!Objects.equals(model.getOnlineState(false), "public")) {
|
||||
throw new IOException(model.getName() +"'s room is not public");
|
||||
}
|
||||
|
||||
String segments = parseMaster(streamInfo.url, model.getStreamUrlIndex());
|
||||
String segments = model.getSegmentPlaylistUrl();
|
||||
if(segments != null) {
|
||||
if (!Files.exists(downloadDir, LinkOption.NOFOLLOW_LINKS)) {
|
||||
Files.createDirectories(downloadDir);
|
||||
|
|
|
@ -39,7 +39,6 @@ import ctbrec.HttpClient;
|
|||
import ctbrec.Model;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.ProgressListener;
|
||||
import ctbrec.recorder.StreamInfo;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
|
@ -89,7 +88,6 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
Path modelDir = FileSystems.getDefault().getPath(config.getSettings().recordingsDir, model.getName());
|
||||
downloadDir = FileSystems.getDefault().getPath(modelDir.toString(), startTime);
|
||||
|
||||
StreamInfo streamInfo = model.getStreamInfo();
|
||||
if(!model.isOnline(IGNORE_CACHE)) {
|
||||
throw new IOException(model.getName() +"'s room is not public");
|
||||
}
|
||||
|
@ -101,7 +99,7 @@ public class MergedHlsDownload extends AbstractHlsDownload {
|
|||
target = new File(targetFile.getAbsolutePath().replaceAll("\\.ts", "-00000.ts"));
|
||||
}
|
||||
|
||||
String segments = parseMaster(streamInfo.url, model.getStreamUrlIndex());
|
||||
String segments = model.getSegmentPlaylistUrl();
|
||||
mergeThread = createMergeThread(target, null, true);
|
||||
mergeThread.start();
|
||||
if(segments != null) {
|
||||
|
|
|
@ -2,7 +2,7 @@ package ctbrec.recorder.download;
|
|||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
public class StreamSource {
|
||||
public class StreamSource implements Comparable<StreamSource> {
|
||||
public int bandwidth;
|
||||
public int height;
|
||||
public String mediaPlaylistUrl;
|
||||
|
@ -37,4 +37,18 @@ public class StreamSource {
|
|||
float mbit = bandwidth / 1024.0f / 1024.0f;
|
||||
return height + "p (" + df.format(mbit) + " Mbit/s)";
|
||||
}
|
||||
|
||||
/**
|
||||
* First compares the sources by height, if the heights are the same
|
||||
* it compares the bandwidth values
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(StreamSource o) {
|
||||
int heightDiff = height - o.height;
|
||||
if(heightDiff != 0) {
|
||||
return heightDiff;
|
||||
} else {
|
||||
return bandwidth - o.bandwidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,10 @@ import org.slf4j.LoggerFactory;
|
|||
import com.squareup.moshi.JsonAdapter;
|
||||
import com.squareup.moshi.Moshi;
|
||||
|
||||
import ctbrec.ChaturbateModel;
|
||||
import ctbrec.InstantJsonAdapter;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ModelAdapter;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.recorder.Recorder;
|
||||
|
||||
|
@ -53,6 +55,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
|||
LOG.debug("Request: {}", json);
|
||||
Moshi moshi = new Moshi.Builder()
|
||||
.add(Instant.class, new InstantJsonAdapter())
|
||||
.add(Model.class, new ModelAdapter())
|
||||
.build();
|
||||
JsonAdapter<Request> requestAdapter = moshi.adapter(Request.class);
|
||||
Request request = requestAdapter.fromJson(json);
|
||||
|
@ -130,7 +133,7 @@ public class RecorderServlet extends AbstractCtbrecServlet {
|
|||
|
||||
private static class Request {
|
||||
public String action;
|
||||
public Model model;
|
||||
public ChaturbateModel model;
|
||||
public String recording;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -260,6 +260,7 @@ public class CtbrecApplication extends Application {
|
|||
try {
|
||||
Config.init();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Couldn't load settings", e);
|
||||
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Whoopsie");
|
||||
alert.setContentText("Couldn't load settings.");
|
||||
|
|
|
@ -5,14 +5,19 @@ import java.util.List;
|
|||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import com.iheartradio.m3u8.ParseException;
|
||||
import com.iheartradio.m3u8.PlaylistException;
|
||||
|
||||
import ctbrec.AbstractModel;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
||||
/**
|
||||
* Just a wrapper for Model, which augments it with JavaFX value binding properties, so that UI widgets get updated proeprly
|
||||
*/
|
||||
public class JavaFxModel extends Model {
|
||||
public class JavaFxModel extends AbstractModel {
|
||||
private transient BooleanProperty onlineProperty = new SimpleBooleanProperty();
|
||||
|
||||
private Model delegate;
|
||||
|
@ -86,4 +91,24 @@ public class JavaFxModel extends Model {
|
|||
Model getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnline() throws IOException, ExecutionException, InterruptedException {
|
||||
return delegate.isOnline();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
||||
return delegate.isOnline(ignoreCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOnlineState(boolean failFast) throws IOException, ExecutionException {
|
||||
return delegate.getOnlineState(failFast);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
||||
return delegate.getStreamSources();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import java.util.function.Function;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.ChaturbateModel;
|
||||
import ctbrec.HttpClient;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.recorder.Recorder;
|
||||
|
@ -125,7 +126,7 @@ public class RecordedModelsTab extends Tab implements TabSelectionListener {
|
|||
model.setPrefWidth(300);
|
||||
BorderPane.setMargin(addModelBox, new Insets(5));
|
||||
addModelButton.setOnAction((e) -> {
|
||||
Model m = new Model();
|
||||
ChaturbateModel m = new ChaturbateModel();
|
||||
m.setName(model.getText());
|
||||
m.setUrl("https://chaturbate.com/" + m.getName() + "/");
|
||||
try {
|
||||
|
|
|
@ -30,7 +30,7 @@ import com.iheartradio.m3u8.PlaylistException;
|
|||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.HttpClient;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ChaturbateModel;
|
||||
import ctbrec.Recording;
|
||||
import ctbrec.Recording.STATUS;
|
||||
import ctbrec.recorder.Recorder;
|
||||
|
@ -246,7 +246,7 @@ public class RecordingsTab extends Tab implements TabSelectionListener {
|
|||
|
||||
MenuItem stopRecording = new MenuItem("Stop recording");
|
||||
stopRecording.setOnAction((e) -> {
|
||||
Model m = new Model();
|
||||
ChaturbateModel m = new ChaturbateModel();
|
||||
m.setName(recording.getModelName());
|
||||
m.setUrl(CtbrecApplication.BASE_URI + '/' + recording.getModelName() + '/');
|
||||
try {
|
||||
|
|
|
@ -1,49 +1,22 @@
|
|||
package ctbrec.ui;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.iheartradio.m3u8.data.MasterPlaylist;
|
||||
import com.iheartradio.m3u8.data.PlaylistData;
|
||||
|
||||
import ctbrec.HttpClient;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.recorder.StreamInfo;
|
||||
import ctbrec.recorder.download.StreamSource;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.scene.control.ChoiceDialog;
|
||||
|
||||
public class StreamSourceSelectionDialog {
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(StreamSourceSelectionDialog.class);
|
||||
|
||||
public static void show(Model model, HttpClient client, Function<Model,Void> onSuccess, Function<Throwable, Void> onFail) {
|
||||
Task<List<StreamSource>> selectStreamSource = new Task<List<StreamSource>>() {
|
||||
@Override
|
||||
protected List<StreamSource> call() throws Exception {
|
||||
model.invalidateCacheEntries();
|
||||
StreamInfo streamInfo = model.getStreamInfo();
|
||||
MasterPlaylist masterPlaylist = model.getMasterPlaylist();
|
||||
List<StreamSource> sources = new ArrayList<>();
|
||||
for (PlaylistData playlist : masterPlaylist.getPlaylists()) {
|
||||
if (playlist.hasStreamInfo()) {
|
||||
StreamSource src = new StreamSource();
|
||||
src.bandwidth = playlist.getStreamInfo().getBandwidth();
|
||||
src.height = playlist.getStreamInfo().getResolution().height;
|
||||
String masterUrl = streamInfo.url;
|
||||
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
||||
String segmentUri = baseUrl + playlist.getUri();
|
||||
src.mediaPlaylistUrl = segmentUri;
|
||||
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
|
||||
sources.add(src);
|
||||
}
|
||||
}
|
||||
return sources;
|
||||
return model.getStreamSources();
|
||||
}
|
||||
};
|
||||
selectStreamSource.setOnSucceeded((e) -> {
|
||||
|
|
|
@ -8,6 +8,7 @@ import java.util.function.Function;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import ctbrec.ChaturbateModel;
|
||||
import ctbrec.Config;
|
||||
import ctbrec.HttpClient;
|
||||
import ctbrec.Model;
|
||||
|
@ -50,7 +51,7 @@ public class ThumbCell extends StackPane {
|
|||
public static int width = 180;
|
||||
private static final Duration ANIMATION_DURATION = new Duration(250);
|
||||
|
||||
private Model model;
|
||||
private ChaturbateModel model;
|
||||
private ImageView iv;
|
||||
private Rectangle resolutionBackground;
|
||||
private final Paint resolutionOnlineColor = new Color(0.22, 0.8, 0.29, 1);
|
||||
|
@ -76,7 +77,7 @@ public class ThumbCell extends StackPane {
|
|||
private boolean mouseHovering = false;
|
||||
private boolean recording = false;
|
||||
|
||||
public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder, HttpClient client) {
|
||||
public ThumbCell(ThumbOverviewTab parent, ChaturbateModel model, Recorder recorder, HttpClient client) {
|
||||
this.thumbCellList = parent.grid.getChildren();
|
||||
this.model = model;
|
||||
this.recorder = recorder;
|
||||
|
@ -421,7 +422,7 @@ public class ThumbCell extends StackPane {
|
|||
}.start();
|
||||
}
|
||||
|
||||
public Model getModel() {
|
||||
public ChaturbateModel getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import com.sun.javafx.collections.ObservableListWrapper;
|
|||
|
||||
import ctbrec.Config;
|
||||
import ctbrec.HttpClient;
|
||||
import ctbrec.Model;
|
||||
import ctbrec.ChaturbateModel;
|
||||
import ctbrec.ModelParser;
|
||||
import ctbrec.recorder.Recorder;
|
||||
import javafx.collections.ObservableList;
|
||||
|
@ -65,11 +65,11 @@ import okhttp3.Response;
|
|||
public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
||||
private static final transient Logger LOG = LoggerFactory.getLogger(ThumbOverviewTab.class);
|
||||
|
||||
static Set<Model> resolutionProcessing = Collections.synchronizedSet(new HashSet<>());
|
||||
static Set<ChaturbateModel> resolutionProcessing = Collections.synchronizedSet(new HashSet<>());
|
||||
static BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
|
||||
static ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 10, TimeUnit.MINUTES, queue);
|
||||
|
||||
ScheduledService<List<Model>> updateService;
|
||||
ScheduledService<List<ChaturbateModel>> updateService;
|
||||
Recorder recorder;
|
||||
List<ThumbCell> filteredThumbCells = Collections.synchronizedList(new ArrayList<>());
|
||||
List<ThumbCell> selectedThumbCells = Collections.synchronizedList(new ArrayList<>());
|
||||
|
@ -223,7 +223,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
gridLock.lock();
|
||||
try {
|
||||
List<Model> models = updateService.getValue();
|
||||
List<ChaturbateModel> models = updateService.getValue();
|
||||
ObservableList<Node> nodes = grid.getChildren();
|
||||
|
||||
// first remove models, which are not in the updated list
|
||||
|
@ -238,7 +238,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
|
||||
List<ThumbCell> positionChangedOrNew = new ArrayList<>();
|
||||
int index = 0;
|
||||
for (Model model : models) {
|
||||
for (ChaturbateModel model : models) {
|
||||
boolean found = false;
|
||||
for (Iterator<Node> iterator = nodes.iterator(); iterator.hasNext();) {
|
||||
Node node = iterator.next();
|
||||
|
@ -278,7 +278,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
|
||||
}
|
||||
|
||||
ThumbCell createThumbCell(ThumbOverviewTab thumbOverviewTab, Model model, Recorder recorder2, HttpClient client2) {
|
||||
ThumbCell createThumbCell(ThumbOverviewTab thumbOverviewTab, ChaturbateModel model, Recorder recorder2, HttpClient client2) {
|
||||
ThumbCell newCell = new ThumbCell(this, model, recorder, client);
|
||||
newCell.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
|
||||
suspendUpdates(true);
|
||||
|
@ -476,7 +476,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
for (Iterator<Node> iterator = grid.getChildren().iterator(); iterator.hasNext();) {
|
||||
Node node = iterator.next();
|
||||
ThumbCell cell = (ThumbCell) node;
|
||||
Model m = cell.getModel();
|
||||
ChaturbateModel m = cell.getModel();
|
||||
if(!matches(m, filter)) {
|
||||
iterator.remove();
|
||||
filteredThumbCells.add(cell);
|
||||
|
@ -487,7 +487,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
// add the ones, which might have been filtered before, but now match
|
||||
for (Iterator<ThumbCell> iterator = filteredThumbCells.iterator(); iterator.hasNext();) {
|
||||
ThumbCell thumbCell = iterator.next();
|
||||
Model m = thumbCell.getModel();
|
||||
ChaturbateModel m = thumbCell.getModel();
|
||||
if(matches(m, filter)) {
|
||||
iterator.remove();
|
||||
insert(thumbCell);
|
||||
|
@ -520,7 +520,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean matches(Model m, String filter) {
|
||||
private boolean matches(ChaturbateModel m, String filter) {
|
||||
try {
|
||||
String[] tokens = filter.split(" ");
|
||||
StringBuilder searchTextBuilder = new StringBuilder(m.getName());
|
||||
|
@ -558,19 +558,19 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
|
|||
}
|
||||
}
|
||||
|
||||
private ScheduledService<List<Model>> createUpdateService() {
|
||||
ScheduledService<List<Model>> updateService = new ScheduledService<List<Model>>() {
|
||||
private ScheduledService<List<ChaturbateModel>> createUpdateService() {
|
||||
ScheduledService<List<ChaturbateModel>> updateService = new ScheduledService<List<ChaturbateModel>>() {
|
||||
@Override
|
||||
protected Task<List<Model>> createTask() {
|
||||
return new Task<List<Model>>() {
|
||||
protected Task<List<ChaturbateModel>> createTask() {
|
||||
return new Task<List<ChaturbateModel>>() {
|
||||
@Override
|
||||
public List<Model> call() throws IOException {
|
||||
public List<ChaturbateModel> call() throws IOException {
|
||||
String url = ThumbOverviewTab.this.url + "?page="+page+"&keywords=&_=" + System.currentTimeMillis();
|
||||
LOG.debug("Fetching page {}", url);
|
||||
Request request = new Request.Builder().url(url).build();
|
||||
Response response = client.execute(request, loginRequired);
|
||||
if (response.isSuccessful()) {
|
||||
List<Model> models = ModelParser.parseModels(response.body().string());
|
||||
List<ChaturbateModel> models = ModelParser.parseModels(response.body().string());
|
||||
response.close();
|
||||
return models;
|
||||
} else {
|
||||
|
|
Loading…
Reference in New Issue