diff --git a/src/main/java/ctbrec/AbstractModel.java b/src/main/java/ctbrec/AbstractModel.java new file mode 100644 index 00000000..267f9661 --- /dev/null +++ b/src/main/java/ctbrec/AbstractModel.java @@ -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 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 getTags() { + return tags; + } + + @Override + public void setTags(List 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 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(); + } + +} diff --git a/src/main/java/ctbrec/ChaturbateModel.java b/src/main/java/ctbrec/ChaturbateModel.java new file mode 100644 index 00000000..acc0a826 --- /dev/null +++ b/src/main/java/ctbrec/ChaturbateModel.java @@ -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 getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + invalidateCacheEntries(); + StreamInfo streamInfo = getStreamInfo(); + MasterPlaylist masterPlaylist = getMasterPlaylist(); + List 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 streamInfoCache = CacheBuilder.newBuilder() + .initialCapacity(10_000) + .maximumSize(10_000) + .expireAfterWrite(5, TimeUnit.MINUTES) + .build(new CacheLoader () { + @Override + public StreamInfo load(String model) throws Exception { + return loadStreamInfo(model); + } + }); + + private LoadingCache streamResolutionCache = CacheBuilder.newBuilder() + .initialCapacity(10_000) + .maximumSize(10_000) + .expireAfterWrite(5, TimeUnit.MINUTES) + .build(new CacheLoader () { + @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 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(); + } + } + } +} diff --git a/src/main/java/ctbrec/Config.java b/src/main/java/ctbrec/Config.java index 0369b5d1..13522edf 100644 --- a/src/main/java/ctbrec/Config.java +++ b/src/main/java/ctbrec/Config.java @@ -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 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 adapter = moshi.adapter(Settings.class).indent(" "); String json = adapter.toJson(settings); File configDir = OS.getConfigDir(); diff --git a/src/main/java/ctbrec/Model.java b/src/main/java/ctbrec/Model.java index d1f92de3..1034b41f 100644 --- a/src/main/java/ctbrec/Model.java +++ b/src/main/java/ctbrec/Model.java @@ -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 getTags(); + public void setTags(List 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 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 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 getTags() { - return tags; - } - - public void setTags(List 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 streamInfoCache = CacheBuilder.newBuilder() - .initialCapacity(10_000) - .maximumSize(10_000) - .expireAfterWrite(5, TimeUnit.MINUTES) - .build(new CacheLoader () { - @Override - public StreamInfo load(String model) throws Exception { - return loadStreamInfo(model); - } - }); - - private LoadingCache streamResolutionCache = CacheBuilder.newBuilder() - .initialCapacity(10_000) - .maximumSize(10_000) - .expireAfterWrite(5, TimeUnit.MINUTES) - .build(new CacheLoader () { - @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 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(); - } - } - } -} +} \ No newline at end of file diff --git a/src/main/java/ctbrec/ModelAdapter.java b/src/main/java/ctbrec/ModelAdapter.java new file mode 100644 index 00000000..ccb0989f --- /dev/null +++ b/src/main/java/ctbrec/ModelAdapter.java @@ -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 { + + @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); + } + } + +} diff --git a/src/main/java/ctbrec/ModelParser.java b/src/main/java/ctbrec/ModelParser.java index fc39c87d..dc16dd89 100644 --- a/src/main/java/ctbrec/ModelParser.java +++ b/src/main/java/ctbrec/ModelParser.java @@ -15,13 +15,13 @@ import ctbrec.ui.HtmlParser; public class ModelParser { private static final transient Logger LOG = LoggerFactory.getLogger(ModelParser.class); - public static List parseModels(String html) { - List models = new ArrayList<>(); + public static List parseModels(String html) { + List 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")); diff --git a/src/main/java/ctbrec/Settings.java b/src/main/java/ctbrec/Settings.java index 35e949bd..fbab0adb 100644 --- a/src/main/java/ctbrec/Settings.java +++ b/src/main/java/ctbrec/Settings.java @@ -23,6 +23,7 @@ public class Settings { public String username = ""; public String password = ""; public String lastDownloadDir = ""; + public List models = new ArrayList(); public boolean determineResolution = false; public boolean requireAuthentication = false; diff --git a/src/main/java/ctbrec/recorder/LocalRecorder.java b/src/main/java/ctbrec/recorder/LocalRecorder.java index a5b2ae27..e71d1b63 100644 --- a/src/main/java/ctbrec/recorder/LocalRecorder.java +++ b/src/main/java/ctbrec/recorder/LocalRecorder.java @@ -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 restart = new ArrayList(); + List restart = new ArrayList<>(); for (Iterator> iterator = recordingProcesses.entrySet().iterator(); iterator.hasNext();) { Entry 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 followed = ModelParser.parseModels(response.body().string()); + List 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); diff --git a/src/main/java/ctbrec/recorder/RemoteRecorder.java b/src/main/java/ctbrec/recorder/RemoteRecorder.java index baf6d3f5..abe6a876 100644 --- a/src/main/java/ctbrec/recorder/RemoteRecorder.java +++ b/src/main/java/ctbrec/recorder/RemoteRecorder.java @@ -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 modelListResponseAdapter = moshi.adapter(ModelListResponse.class); private JsonAdapter recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class); diff --git a/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java b/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java index 05952252..a6bce88e 100644 --- a/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java +++ b/src/main/java/ctbrec/recorder/download/AbstractHlsDownload.java @@ -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(); diff --git a/src/main/java/ctbrec/recorder/download/HlsDownload.java b/src/main/java/ctbrec/recorder/download/HlsDownload.java index 5a2546a4..8d52217c 100644 --- a/src/main/java/ctbrec/recorder/download/HlsDownload.java +++ b/src/main/java/ctbrec/recorder/download/HlsDownload.java @@ -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); diff --git a/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java b/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java index f5f655d9..c970c015 100644 --- a/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java +++ b/src/main/java/ctbrec/recorder/download/MergedHlsDownload.java @@ -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) { diff --git a/src/main/java/ctbrec/recorder/download/StreamSource.java b/src/main/java/ctbrec/recorder/download/StreamSource.java index ab9da42c..2ed7a8f8 100644 --- a/src/main/java/ctbrec/recorder/download/StreamSource.java +++ b/src/main/java/ctbrec/recorder/download/StreamSource.java @@ -2,7 +2,7 @@ package ctbrec.recorder.download; import java.text.DecimalFormat; -public class StreamSource { +public class StreamSource implements Comparable { 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; + } + } } diff --git a/src/main/java/ctbrec/recorder/server/RecorderServlet.java b/src/main/java/ctbrec/recorder/server/RecorderServlet.java index 88eadb68..7cf93228 100644 --- a/src/main/java/ctbrec/recorder/server/RecorderServlet.java +++ b/src/main/java/ctbrec/recorder/server/RecorderServlet.java @@ -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 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; } } diff --git a/src/main/java/ctbrec/ui/CtbrecApplication.java b/src/main/java/ctbrec/ui/CtbrecApplication.java index 6cfec90a..b3655abd 100644 --- a/src/main/java/ctbrec/ui/CtbrecApplication.java +++ b/src/main/java/ctbrec/ui/CtbrecApplication.java @@ -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."); diff --git a/src/main/java/ctbrec/ui/JavaFxModel.java b/src/main/java/ctbrec/ui/JavaFxModel.java index 1e9d7579..7948bd1e 100644 --- a/src/main/java/ctbrec/ui/JavaFxModel.java +++ b/src/main/java/ctbrec/ui/JavaFxModel.java @@ -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 getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + return delegate.getStreamSources(); + } } diff --git a/src/main/java/ctbrec/ui/RecordedModelsTab.java b/src/main/java/ctbrec/ui/RecordedModelsTab.java index 9a0bc08d..3b36a18b 100644 --- a/src/main/java/ctbrec/ui/RecordedModelsTab.java +++ b/src/main/java/ctbrec/ui/RecordedModelsTab.java @@ -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 { diff --git a/src/main/java/ctbrec/ui/RecordingsTab.java b/src/main/java/ctbrec/ui/RecordingsTab.java index 0cfa8a98..f79e0cde 100644 --- a/src/main/java/ctbrec/ui/RecordingsTab.java +++ b/src/main/java/ctbrec/ui/RecordingsTab.java @@ -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 { diff --git a/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java b/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java index 14db2b6b..fbc30e57 100644 --- a/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java +++ b/src/main/java/ctbrec/ui/StreamSourceSelectionDialog.java @@ -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 onSuccess, Function onFail) { Task> selectStreamSource = new Task>() { @Override protected List call() throws Exception { - model.invalidateCacheEntries(); - StreamInfo streamInfo = model.getStreamInfo(); - MasterPlaylist masterPlaylist = model.getMasterPlaylist(); - List 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) -> { diff --git a/src/main/java/ctbrec/ui/ThumbCell.java b/src/main/java/ctbrec/ui/ThumbCell.java index 0d1db977..16f5177b 100644 --- a/src/main/java/ctbrec/ui/ThumbCell.java +++ b/src/main/java/ctbrec/ui/ThumbCell.java @@ -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; } diff --git a/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/src/main/java/ctbrec/ui/ThumbOverviewTab.java index f2c3eaea..b9656e4e 100644 --- a/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -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 resolutionProcessing = Collections.synchronizedSet(new HashSet<>()); + static Set resolutionProcessing = Collections.synchronizedSet(new HashSet<>()); static BlockingQueue queue = new LinkedBlockingQueue<>(); static ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 10, TimeUnit.MINUTES, queue); - ScheduledService> updateService; + ScheduledService> updateService; Recorder recorder; List filteredThumbCells = Collections.synchronizedList(new ArrayList<>()); List selectedThumbCells = Collections.synchronizedList(new ArrayList<>()); @@ -223,7 +223,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } gridLock.lock(); try { - List models = updateService.getValue(); + List models = updateService.getValue(); ObservableList 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 positionChangedOrNew = new ArrayList<>(); int index = 0; - for (Model model : models) { + for (ChaturbateModel model : models) { boolean found = false; for (Iterator 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 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 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> createUpdateService() { - ScheduledService> updateService = new ScheduledService>() { + private ScheduledService> createUpdateService() { + ScheduledService> updateService = new ScheduledService>() { @Override - protected Task> createTask() { - return new Task>() { + protected Task> createTask() { + return new Task>() { @Override - public List call() throws IOException { + public List 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 models = ModelParser.parseModels(response.body().string()); + List models = ModelParser.parseModels(response.body().string()); response.close(); return models; } else {