diff --git a/src/main/java/ctbrec/AbstractModel.java b/src/main/java/ctbrec/AbstractModel.java index 267f9661..d68aa190 100644 --- a/src/main/java/ctbrec/AbstractModel.java +++ b/src/main/java/ctbrec/AbstractModel.java @@ -20,6 +20,11 @@ public abstract class AbstractModel implements Model { private List tags = new ArrayList<>(); private int streamUrlIndex = -1; + @Override + public boolean isOnline() throws IOException, ExecutionException, InterruptedException { + return isOnline(false); + } + @Override public String getUrl() { return url; diff --git a/src/main/java/ctbrec/sites/camsoda/Camsoda.java b/src/main/java/ctbrec/sites/camsoda/Camsoda.java index 4cf02389..c0ce5337 100644 --- a/src/main/java/ctbrec/sites/camsoda/Camsoda.java +++ b/src/main/java/ctbrec/sites/camsoda/Camsoda.java @@ -5,12 +5,13 @@ import java.io.IOException; import ctbrec.Model; import ctbrec.io.HttpClient; import ctbrec.recorder.Recorder; -import ctbrec.sites.Site; +import ctbrec.sites.AbstractSite; import ctbrec.ui.TabProvider; import javafx.scene.Node; -public class Camsoda implements Site { +public class Camsoda extends AbstractSite { + public static final String BASE_URI = "https://www.camsoda.com"; private Recorder recorder; private HttpClient httpClient; @@ -21,7 +22,7 @@ public class Camsoda implements Site { @Override public String getBaseUrl() { - return "https://www.camsoda.com"; + return BASE_URI; } @Override @@ -36,7 +37,7 @@ public class Camsoda implements Site { @Override public TabProvider getTabProvider() { - return new CamsodaTabProvider(); + return new CamsodaTabProvider(this, recorder); } @Override @@ -44,6 +45,7 @@ public class Camsoda implements Site { CamsodaModel model = new CamsodaModel(); model.setName(name); model.setUrl(getBaseUrl() + "/" + name); + model.setSite(this); return model; } @@ -107,5 +109,4 @@ public class Camsoda implements Site { public boolean credentialsAvailable() { return false; } - } diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index dc1b2c24..ede30e93 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -1,46 +1,148 @@ package ctbrec.sites.camsoda; import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.ExecutionException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.iheartradio.m3u8.Encoding; +import com.iheartradio.m3u8.Format; import com.iheartradio.m3u8.ParseException; import com.iheartradio.m3u8.PlaylistException; +import com.iheartradio.m3u8.PlaylistParser; +import com.iheartradio.m3u8.data.MasterPlaylist; +import com.iheartradio.m3u8.data.Playlist; +import com.iheartradio.m3u8.data.PlaylistData; +import com.iheartradio.m3u8.data.StreamInfo; import ctbrec.AbstractModel; import ctbrec.recorder.download.StreamSource; import ctbrec.sites.Site; +import okhttp3.Request; +import okhttp3.Response; public class CamsodaModel extends AbstractModel { - @Override - public boolean isOnline() throws IOException, ExecutionException, InterruptedException { - // TODO Auto-generated method stub - return false; + private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaModel.class); + private String streamUrl; + private Site site; + private List streamSources = null; + private int[] resolution; + private String status = "n/a"; + + public String getStreamUrl() throws IOException { + if(streamUrl == null) { + // load model + loadModel(); + } + return streamUrl; + } + + private void loadModel() throws IOException { + String modelUrl = site.getBaseUrl() + "/api/v1/user/" + getName(); + Request req = new Request.Builder().url(modelUrl).build(); + Response response = site.getHttpClient().execute(req); + try { + JSONObject result = new JSONObject(response.body().string()); + if(result.getBoolean("status")) { + JSONObject chat = result.getJSONObject("user").getJSONObject("chat"); + status = chat.getString("status"); + if(chat.has("edge_servers")) { + String edgeServer = chat.getJSONArray("edge_servers").getString(0); + String streamName = chat.getString("stream_name"); + streamUrl = "https://" + edgeServer + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"; + } + + } else { + throw new IOException("Result was not ok"); + } + } finally { + response.close(); + } } @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { - // TODO Auto-generated method stub - return false; + if(ignoreCache) { + loadModel(); + } + return Objects.equals(status, "online"); } @Override public String getOnlineState(boolean failFast) throws IOException, ExecutionException { - // TODO Auto-generated method stub - return null; + if(failFast) { + return status; + } else { + if(status.equals("n/a")) { + loadModel(); + } + return status; + } } @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { - // TODO Auto-generated method stub - return null; + LOG.trace("Loading master playlist {}", streamUrl); + if(streamSources == null) { + Request req = new Request.Builder().url(streamUrl).build(); + Response response = site.getHttpClient().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(); + PlaylistData playlistData = master.getPlaylists().get(0); + StreamSource streamsource = new StreamSource(); + streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", playlistData.getUri()); + if(playlistData.hasStreamInfo()) { + StreamInfo info = playlistData.getStreamInfo(); + streamsource.bandwidth = info.getBandwidth(); + streamsource.width = info.hasResolution() ? info.getResolution().width : 0; + streamsource.height = info.hasResolution() ? info.getResolution().height : 0; + } else { + streamsource.bandwidth = 0; + streamsource.width = 0; + streamsource.height = 0; + } + streamSources = Collections.singletonList(streamsource); + } finally { + response.close(); + } + } + return streamSources; } @Override public void invalidateCacheEntries() { - // TODO Auto-generated method stub + streamSources = null; + resolution = null; + } + @Override + public int[] getStreamResolution(boolean failFast) throws ExecutionException { + if(resolution != null) { + return resolution; + } else { + if(failFast) { + return new int[] {0,0}; + } else { + try { + List streamSources = getStreamSources(); + StreamSource src = streamSources.get(0); + resolution = new int[] {src.width, src.height}; + return resolution; + } catch (IOException | ParseException | PlaylistException e) { + throw new ExecutionException(e); + } + } + } } @Override @@ -49,12 +151,6 @@ public class CamsodaModel extends AbstractModel { } - @Override - public int[] getStreamResolution(boolean failFast) throws ExecutionException { - // TODO Auto-generated method stub - return null; - } - @Override public boolean follow() throws IOException { // TODO Auto-generated method stub @@ -69,8 +165,11 @@ public class CamsodaModel extends AbstractModel { @Override public void setSite(Site site) { - // TODO Auto-generated method stub + this.site = site; + } + public void setStreamUrl(String streamUrl) { + this.streamUrl = streamUrl; } } diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java b/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java index 0a4d205b..30e9b771 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaTabProvider.java @@ -1,17 +1,42 @@ package ctbrec.sites.camsoda; -import java.util.Collections; +import static ctbrec.sites.camsoda.Camsoda.*; + +import java.util.ArrayList; import java.util.List; +import ctbrec.recorder.Recorder; import ctbrec.ui.TabProvider; +import ctbrec.ui.ThumbOverviewTab; import javafx.scene.Scene; import javafx.scene.control.Tab; public class CamsodaTabProvider extends TabProvider { + private Camsoda camsoda; + private Recorder recorder; + + public CamsodaTabProvider(Camsoda camsoda, Recorder recorder) { + this.camsoda = camsoda; + this.recorder = recorder; + } + @Override public List getTabs(Scene scene) { - return Collections.emptyList(); + List tabs = new ArrayList<>(); + tabs.add(createTab("Featured", BASE_URI + "/api/v1/browse/online")); + // ChaturbateFollowedTab followedTab = new ChaturbateFollowedTab("Followed", BASE_URI + "/followed-cams/", chaturbate); + // followedTab.setRecorder(recorder); + // followedTab.setScene(scene); + // tabs.add(followedTab); + return tabs; + } + + private Tab createTab(String title, String url) { + CamsodaUpdateService updateService = new CamsodaUpdateService(url, false, camsoda); + ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, camsoda); + tab.setRecorder(recorder); + return tab; } } diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java b/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java new file mode 100644 index 00000000..df9e6f0d --- /dev/null +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaUpdateService.java @@ -0,0 +1,97 @@ +package ctbrec.sites.camsoda; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.jetty.util.StringUtil; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Model; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import okhttp3.Response; + +public class CamsodaUpdateService extends PaginatedScheduledService { + + private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaUpdateService.class); + + private String url; + private boolean loginRequired; + private Camsoda camsoda; + int modelsPerPage = 50; + + public CamsodaUpdateService(String url, boolean loginRequired, Camsoda camsoda) { + this.url = url; + this.loginRequired = loginRequired; + this.camsoda = camsoda; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + List models = new ArrayList<>(); + if(loginRequired && StringUtil.isBlank(ctbrec.Config.getInstance().getSettings().username)) { + return models; + } else { + String url = CamsodaUpdateService.this.url; + LOG.debug("Fetching page {}", url); + Request request = new Request.Builder().url(url).build(); + Response response = camsoda.getHttpClient().execute(request, loginRequired); + if (response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.has("status") && json.getBoolean("status")) { + JSONArray results = json.getJSONArray("results"); + for (int i = 0; i < results.length(); i++) { + JSONObject result = results.getJSONObject(i); + if(result.has("tpl")) { + JSONArray tpl = result.getJSONArray("tpl"); + String name = tpl.getString(0); + // int connections = tpl.getInt(2); + // float sortValue = tpl.getFloat(3); + String streamName = tpl.getString(5); + String tsize = tpl.getString(6); + String serverPrefix = tpl.getString(7); + JSONArray edgeServers = result.getJSONArray("edge_servers"); + CamsodaModel model = (CamsodaModel) camsoda.createModel(name); + model.setDescription(tpl.getString(4)); + model.setStreamUrl("https://" + edgeServers.getString(0) + "/cam/mp4:" + streamName + "_h264_aac_480p/playlist.m3u8"); + long unixtime = System.currentTimeMillis() / 1000; + String preview = "https://thumbs-orig.camsoda.com/thumbs/" + + streamName + '/' + serverPrefix + '/' + tsize + '/' + unixtime + '/' + name + ".jpg?cb=" + unixtime; + model.setPreview(preview); + //LOG.debug(model.getPreview()); + models.add(model); + // https://vide16-ord.camsoda.com/cam/mp4:kipsyrose-enc6-ord_h264_aac_480p/playlist.m3u8 + // https://enc42-ord.camsoda.com/cam/mp4:elizasmile-enc42-ord_h264_aac_480p/playlist.m3u8 + // https://thumbs-orig.camsoda.com/thumbs/marriednaughtycol-enc35-ord/enc35-ord/340x255/51349794/marriednaughtycol.jpg?cb=51349794 + } else { + //LOG.debug("HÖ? {}", result.toString(2)); + } + } + return models.stream() + .skip( (page-1) * modelsPerPage) + .limit(modelsPerPage) + .collect(Collectors.toList()); + } else { + response.close(); + return models; + } + } else { + int code = response.code(); + response.close(); + throw new IOException("HTTP status " + code); + } + } + } + }; + } + +} diff --git a/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java b/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java index 28773232..493486e2 100644 --- a/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java +++ b/src/main/java/ctbrec/sites/chaturbate/ChaturbateModel.java @@ -38,11 +38,6 @@ public class ChaturbateModel extends AbstractModel { this.site = site; } - @Override - public boolean isOnline() throws IOException, ExecutionException, InterruptedException { - return isOnline(false); - } - @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { StreamInfo info;