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:
0xboobface 2018-10-16 18:32:54 +02:00
parent 534e9905eb
commit 43c29758c4
21 changed files with 593 additions and 452 deletions

View File

@ -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();
}
}

View File

@ -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();
}
}
}
}

View File

@ -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();

View File

@ -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();
}
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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"));

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -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();

View File

@ -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);

View File

@ -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) {

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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.");

View File

@ -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();
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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) -> {

View File

@ -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;
}

View File

@ -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 {