Start implementation for FC2Live

This commit is contained in:
0xboobface 2018-11-19 23:20:39 +01:00
parent 8fb5eac435
commit 7133283032
10 changed files with 674 additions and 0 deletions

View File

@ -31,6 +31,7 @@ import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.mfc.MyFreeCams;
import javafx.application.Application;
import javafx.application.HostServices;
@ -66,6 +67,7 @@ public class CamrecApplication extends Application {
sites.add(new Cam4());
sites.add(new Camsoda());
sites.add(new Chaturbate());
sites.add(new Fc2Live());
sites.add(new MyFreeCams());
loadConfig();
createHttpClient();

View File

@ -5,11 +5,13 @@ import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.ui.sites.bonga.BongaCamsSiteUi;
import ctbrec.ui.sites.cam4.Cam4SiteUi;
import ctbrec.ui.sites.camsoda.CamsodaSiteUi;
import ctbrec.ui.sites.chaturbate.ChaturbateSiteUi;
import ctbrec.ui.sites.fc2live.Fc2LiveUi;
import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi;
public class SiteUiFactory {
@ -18,6 +20,7 @@ public class SiteUiFactory {
private static Cam4SiteUi cam4SiteUi;
private static CamsodaSiteUi camsodaSiteUi;
private static ChaturbateSiteUi ctbSiteUi;
private static Fc2LiveUi fc2SiteUi;
private static MyFreeCamsSiteUi mfcSiteUi;
public static SiteUI getUi(Site site) {
@ -41,6 +44,11 @@ public class SiteUiFactory {
ctbSiteUi = new ChaturbateSiteUi((Chaturbate) site);
}
return ctbSiteUi;
} else if (site instanceof Fc2Live) {
if (fc2SiteUi == null) {
fc2SiteUi = new Fc2LiveUi((Fc2Live) site);
}
return fc2SiteUi;
} else if (site instanceof MyFreeCams) {
if (mfcSiteUi == null) {
mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site);

View File

@ -0,0 +1,35 @@
package ctbrec.ui.sites.fc2live;
import java.io.IOException;
import ctbrec.sites.ConfigUI;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.ui.SiteUI;
import ctbrec.ui.TabProvider;
public class Fc2LiveUi implements SiteUI {
private Fc2Live fc2live;
private Fc2TabProvider tabProvider;
public Fc2LiveUi(Fc2Live fc2live) {
this.fc2live = fc2live;
this.tabProvider = new Fc2TabProvider(fc2live);
}
@Override
public TabProvider getTabProvider() {
return tabProvider;
}
@Override
public ConfigUI getConfigUI() {
return null;
}
@Override
public boolean login() throws IOException {
return false;
}
}

View File

@ -0,0 +1,38 @@
package ctbrec.ui.sites.fc2live;
import java.util.ArrayList;
import java.util.List;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.ui.TabProvider;
import ctbrec.ui.ThumbOverviewTab;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
public class Fc2TabProvider extends TabProvider {
private Fc2Live fc2live;
public Fc2TabProvider(Fc2Live fc2live) {
this.fc2live = fc2live;
}
@Override
public List<Tab> getTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>();
tabs.add(createTab("Online", Fc2Live.BASE_URL + "/adult/contents/allchannellist.php"));
return tabs;
}
private Tab createTab(String title, String url) {
Fc2UpdateService updateService = new Fc2UpdateService(url, fc2live);
ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, fc2live);
tab.setRecorder(fc2live.getRecorder());
return tab;
}
@Override
public Tab getFollowedTab() {
return null;
}
}

View File

@ -0,0 +1,86 @@
package ctbrec.ui.sites.fc2live;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.fc2live.Fc2Model;
import ctbrec.ui.PaginatedScheduledService;
import javafx.concurrent.Task;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class Fc2UpdateService extends PaginatedScheduledService {
private static final transient Logger LOG = LoggerFactory.getLogger(Fc2UpdateService.class);
private String url;
private Fc2Live fc2live;
private int modelsPerPage = 30;
public Fc2UpdateService(String url, Fc2Live fc2live) {
this.url = url;
this.fc2live = fc2live;
}
@Override
protected Task<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException {
RequestBody body = RequestBody.create(null, new byte[0]);
Request req = new Request.Builder()
.url(url)
.method("POST", body)
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.5")
.header("Referer", Fc2Live.BASE_URL)
.header("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.header("X-Requested-With", "XMLHttpRequest")
.build();
LOG.debug("Fetching page {}", url);
try(Response resp = fc2live.getHttpClient().execute(req)) {
if(resp.isSuccessful()) {
List<Fc2Model> models = new ArrayList<>();
String msg = resp.body().string();
JSONObject json = new JSONObject(msg);
JSONArray channels = json.getJSONArray("channel");
for (int i = 0; i < channels.length(); i++) {
JSONObject channel = channels.getJSONObject(i);
Fc2Model model = (Fc2Model) fc2live.createModel(channel.getString("name"));
model.setId(channel.getString("id"));
model.setUrl(Fc2Live.BASE_URL + '/' + model.getId());
String previewUrl = channel.getString("image");
if(previewUrl == null || previewUrl.trim().isEmpty()) {
previewUrl = getClass().getResource("/image_not_found.png").toString();
}
model.setPreview(previewUrl);
model.setDescription(channel.optString("title"));
model.setViewerCount(channel.optInt("count"));
if(channel.getInt("login") == 0) {
models.add(model);
}
}
return models.stream()
.sorted((m1, m2) -> m2.getViewerCount() - m1.getViewerCount())
.skip( (page - 1) * modelsPerPage)
.limit(modelsPerPage)
.collect(Collectors.toList());
} else {
resp.close();
throw new IOException("HTTP status " + resp.code() + " " + resp.message());
}
}
}
};
}
}

View File

@ -30,6 +30,8 @@ import okhttp3.OkHttpClient.Builder;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
public abstract class HttpClient {
private static final transient Logger LOG = LoggerFactory.getLogger(HttpClient.class);
@ -215,4 +217,9 @@ public abstract class HttpClient {
public CookieJar getCookieJar() {
return cookieJar;
}
public WebSocket newWebSocket(String url, WebSocketListener l) {
Request request = new Request.Builder().url(url).build();
return client.newWebSocket(request, l);
}
}

View File

@ -0,0 +1,106 @@
package ctbrec.sites.fc2live;
import java.io.IOException;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.io.HtmlParser;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.sites.mfc.MyFreeCams;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
public class Fc2HttpClient extends HttpClient {
private static final transient Logger LOG = LoggerFactory.getLogger(Fc2HttpClient.class);
public Fc2HttpClient() {
super("fc2live");
}
@Override
public boolean login() throws IOException {
if(loggedIn) {
return true;
}
if(checkLogin()) {
loggedIn = true;
LOG.debug("Logged in with cookies");
return true;
}
String username = Config.getInstance().getSettings().mfcUsername;
String password = Config.getInstance().getSettings().mfcPassword;
RequestBody body = new FormBody.Builder()
.add("username", username)
.add("password", password)
.add("tz", "2")
.add("ss", "1920x1080")
.add("submit_login", "97")
.build();
Request req = new Request.Builder()
.url(MyFreeCams.BASE_URI + "/php/login.php")
.header("Referer", MyFreeCams.BASE_URI)
.header("Content-Type", "application/x-www-form-urlencoded")
.post(body)
.build();
Response resp = execute(req);
if(resp.isSuccessful()) {
String page = resp.body().string();
if(page.contains("Your username or password are incorrect")) {
return false;
} else {
loggedIn = true;
return true;
}
} else {
resp.close();
LOG.error("Login failed {} {}", resp.code(), resp.message());
return false;
}
}
private boolean checkLogin() throws IOException {
Request req = new Request.Builder().url(MyFreeCams.BASE_URI + "/php/account.php?request=status").build();
try(Response response = execute(req)) {
if(response.isSuccessful()) {
String content = response.body().string();
try {
Elements tags = HtmlParser.getTags(content, "div.content > p > b");
tags.get(2).text();
return true;
} catch(Exception e) {
LOG.debug("Token tag not found. Login failed");
return false;
}
} else {
throw new HttpException(response.code(), response.message());
}
}
}
public WebSocket newWebSocket(Request req, WebSocketListener webSocketListener) {
return client.newWebSocket(req, webSocketListener);
}
// public Cookie getCookie(String name) {
// CookieJar jar = client.cookieJar();
// HttpUrl url = HttpUrl.parse(MyFreeCams.BASE_URI);
// List<Cookie> cookies = jar.loadForRequest(url);
// for (Cookie cookie : cookies) {
// if(Objects.equals(cookie.name(), name)) {
// return cookie;
// }
// }
// throw new NoSuchElementException("No cookie with name " + name);
// }
}

View File

@ -0,0 +1,91 @@
package ctbrec.sites.fc2live;
import java.io.IOException;
import ctbrec.Model;
import ctbrec.io.HttpClient;
import ctbrec.sites.AbstractSite;
public class Fc2Live extends AbstractSite {
public static final String BASE_URL = "https://live.fc2.com";
private Fc2HttpClient httpClient;
@Override
public String getName() {
return "FC2Live";
}
@Override
public String getBaseUrl() {
return BASE_URL;
}
@Override
public String getAffiliateLink() {
return BASE_URL;
}
@Override
public Model createModel(String name) {
Fc2Model model = new Fc2Model();
model.setSite(this);
model.setName(name);
return model;
}
@Override
public Integer getTokenBalance() throws IOException {
return 0;
}
@Override
public String getBuyTokensLink() {
return BASE_URL;
}
@Override
public boolean login() throws IOException {
return false;
}
@Override
public HttpClient getHttpClient() {
if(httpClient == null) {
httpClient = new Fc2HttpClient();
}
return httpClient;
}
@Override
public void init() throws IOException {
}
@Override
public void shutdown() {
if(httpClient != null) {
httpClient.shutdown();
}
}
@Override
public boolean supportsTips() {
return false;
}
@Override
public boolean supportsFollow() {
return false;
}
@Override
public boolean isSiteForModel(Model m) {
return m instanceof Fc2Model;
}
@Override
public boolean credentialsAvailable() {
return false;
}
}

View File

@ -0,0 +1,225 @@
package ctbrec.sites.fc2live;
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.function.BiConsumer;
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.Config;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import okhttp3.FormBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class Fc2Model extends AbstractModel {
private static final transient Logger LOG = LoggerFactory.getLogger(Fc2Model.class);
private String id;
private int viewerCount;
private boolean online;
private String onlineState = "n/a";
private String version;
@Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
if(ignoreCache) {
loadModelInfo();
}
return online;
}
private void loadModelInfo() throws IOException {
String url = Fc2Live.BASE_URL + "/api/memberApi.php";
RequestBody body = new FormBody.Builder()
.add("channel", "1")
.add("profile", "1")
.add("streamid", id)
.build();
Request req = new Request.Builder()
.url(url)
.method("POST", body)
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.5")
.header("Referer", Fc2Live.BASE_URL)
.header("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.header("X-Requested-With", "XMLHttpRequest")
.build();
LOG.debug("Fetching page {}", url);
try(Response resp = getSite().getHttpClient().execute(req)) {
if(resp.isSuccessful()) {
String msg = resp.body().string();
JSONObject json = new JSONObject(msg);
JSONObject data = json.getJSONObject("data");
JSONObject channelData = data.getJSONObject("channel_data");
online = channelData.optInt("is_publish") == 1;
onlineState = online ? "online" : "offline";
version = channelData.optString("version");
} else {
resp.close();
throw new IOException("HTTP status " + resp.code() + " " + resp.message());
}
}
}
@Override
public String getOnlineState(boolean failFast) throws IOException, ExecutionException {
if(failFast) {
return onlineState;
} else if(Objects.equals(onlineState, "n/a")){
loadModelInfo();
}
return onlineState;
}
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
loadModelInfo();
List<StreamSource> sources = new ArrayList<>();
getControlToken((token, url) -> {
url = url + "?control_token=" + token;
LOG.debug("Session token: {}", token);
LOG.debug("Getting playlist token over websocket {}", url);
Fc2WebSocketClient wsClient = new Fc2WebSocketClient(url, getSite().getHttpClient());
try {
String playlistUrl = wsClient.getPlaylistUrl();
LOG.debug("Paylist url {}", playlistUrl);
sources.addAll(parseMasterPlaylist(playlistUrl));
} catch (InterruptedException | IOException | ParseException | PlaylistException e) {
LOG.error("Couldn't fetch stream information", e);
}
});
return sources;
}
private List<StreamSource> parseMasterPlaylist(String playlistUrl) throws IOException, ParseException, PlaylistException {
List<StreamSource> sources = new ArrayList<>();
Request req = new Request.Builder()
.url(playlistUrl)
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.5")
.header("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.header("Origin", Fc2Live.BASE_URL)
.header("Referer", getUrl())
.build();
try(Response response = site.getHttpClient().execute(req)) {
if(response.isSuccessful()) {
InputStream inputStream = response.body().byteStream();
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8);
Playlist playlist = parser.parse();
MasterPlaylist master = playlist.getMasterPlaylist();
sources.clear();
for (PlaylistData playlistData : master.getPlaylists()) {
StreamSource streamsource = new StreamSource();
streamsource.mediaPlaylistUrl = 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;
}
sources.add(streamsource);
}
LOG.debug(sources.toString());
return sources;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
private void getControlToken(BiConsumer<String, String> callback) throws IOException {
String url = Fc2Live.BASE_URL + "/api/getControlServer.php";
RequestBody body = new FormBody.Builder()
.add("channel_id", id)
.add("channel_version", version)
.add("client_app", "browser_hls")
.add("client_type", "pc")
.add("client_version", "1.6.0 [1]")
.add("mode", "play")
.build();
Request req = new Request.Builder()
.url(url)
.method("POST", body)
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.5")
.header("Referer", Fc2Live.BASE_URL)
.header("User-Agent", Config.getInstance().getSettings().httpUserAgent)
.header("X-Requested-With", "XMLHttpRequest")
.build();
LOG.debug("Fetching page {}", url);
try(Response resp = getSite().getHttpClient().execute(req)) {
if(resp.isSuccessful()) {
String msg = resp.body().string();
JSONObject json = new JSONObject(msg);
String wssurl = json.getString("url");
String token = json.getString("control_token");
callback.accept(token, wssurl);
} else {
resp.close();
throw new IOException("HTTP status " + resp.code() + " " + resp.message());
}
}
}
@Override
public void invalidateCacheEntries() {
}
@Override
public void receiveTip(int tokens) throws IOException {
}
@Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
return new int[2];
}
@Override
public boolean follow() throws IOException {
return false;
}
@Override
public boolean unfollow() throws IOException {
return false;
}
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
public int getViewerCount() {
return viewerCount;
}
public void setViewerCount(int viewerCount) {
this.viewerCount = viewerCount;
}
}

View File

@ -0,0 +1,76 @@
package ctbrec.sites.fc2live;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.io.HttpClient;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
public class Fc2WebSocketClient {
private static final transient Logger LOG = LoggerFactory.getLogger(Fc2WebSocketClient.class);
private String url;
private HttpClient client;
public Fc2WebSocketClient(String url, HttpClient client) {
this.url = url;
this.client = client;
}
String playlistUrl = "";
public String getPlaylistUrl() throws InterruptedException {
LOG.debug("Connecting to {}", url);
Object monitor = new Object();
client.newWebSocket(url, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
response.close();
webSocket.send("{\"name\":\"get_hls_information\",\"arguments\":{},\"id\":1}");
}
@Override
public void onMessage(WebSocket webSocket, String text) {
JSONObject json = new JSONObject(text);
if(json.optString("name").equals("_response_") && json.optInt("id") == 1) {
LOG.debug(json.toString(2));
JSONObject args = json.getJSONObject("arguments");
JSONArray playlists = args.getJSONArray("playlists_high_latency");
JSONObject playlist = playlists.getJSONObject(0);
playlistUrl = playlist.getString("url");
webSocket.close(1000, "");
}
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
LOG.debug("ws btxt {}", bytes.toString());
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
synchronized (monitor) {
monitor.notify();
}
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
LOG.debug("ws failure", t);
response.close();
synchronized (monitor) {
monitor.notify();
}
}
});
synchronized (monitor) {
monitor.wait();
}
return playlistUrl;
}
}