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