Add basic support for Amateur.tv

This commit is contained in:
0xb00bface 2021-05-22 09:41:19 +02:00
parent cf3849024f
commit 436272f5ed
9 changed files with 492 additions and 1 deletions

View File

@ -45,6 +45,7 @@ import ctbrec.recorder.OnlineMonitor;
import ctbrec.recorder.Recorder;
import ctbrec.recorder.RemoteRecorder;
import ctbrec.sites.Site;
import ctbrec.sites.amateurtv.AmateurTv;
import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda;
@ -154,6 +155,7 @@ public class CamrecApplication extends Application {
}
private void initSites() {
sites.add(new AmateurTv());
sites.add(new BongaCams());
sites.add(new Cam4());
sites.add(new Camsoda());

View File

@ -1,6 +1,7 @@
package ctbrec.ui;
import ctbrec.sites.Site;
import ctbrec.sites.amateurtv.AmateurTv;
import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda;
@ -13,6 +14,7 @@ import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.showup.Showup;
import ctbrec.sites.streamate.Streamate;
import ctbrec.sites.stripchat.Stripchat;
import ctbrec.ui.sites.amateurtv.AmateurTvSiteUi;
import ctbrec.ui.sites.bonga.BongaCamsSiteUi;
import ctbrec.ui.sites.cam4.Cam4SiteUi;
import ctbrec.ui.sites.camsoda.CamsodaSiteUi;
@ -28,6 +30,7 @@ import ctbrec.ui.sites.stripchat.StripchatSiteUi;
public class SiteUiFactory {
private static AmateurTvSiteUi amateurTvUi;
private static BongaCamsSiteUi bongaSiteUi;
private static Cam4SiteUi cam4SiteUi;
private static CamsodaSiteUi camsodaSiteUi;
@ -44,7 +47,12 @@ public class SiteUiFactory {
private SiteUiFactory () {}
public static synchronized SiteUI getUi(Site site) {
if (site instanceof BongaCams) {
if (site instanceof AmateurTv) {
if (amateurTvUi == null) {
amateurTvUi = new AmateurTvSiteUi((AmateurTv) site);
}
return amateurTvUi;
} else if (site instanceof BongaCams) {
if (bongaSiteUi == null) {
bongaSiteUi = new BongaCamsSiteUi((BongaCams) site);
}

View File

@ -0,0 +1,34 @@
package ctbrec.ui.sites.amateurtv;
import java.io.IOException;
import ctbrec.sites.amateurtv.AmateurTv;
import ctbrec.ui.sites.AbstractSiteUi;
import ctbrec.ui.sites.ConfigUI;
import ctbrec.ui.tabs.TabProvider;
public class AmateurTvSiteUi extends AbstractSiteUi {
private final AmateurTvTabProvider tabProvider;
private final AmateurTv amateurTv;
public AmateurTvSiteUi(AmateurTv amateurTv) {
this.amateurTv = amateurTv;
tabProvider = new AmateurTvTabProvider(amateurTv);
}
@Override
public TabProvider getTabProvider() {
return tabProvider;
}
@Override
public ConfigUI getConfigUI() {
return null;
}
@Override
public synchronized boolean login() throws IOException {
return amateurTv.login();
}
}

View File

@ -0,0 +1,52 @@
package ctbrec.ui.sites.amateurtv;
import java.util.ArrayList;
import java.util.List;
import ctbrec.recorder.Recorder;
import ctbrec.sites.amateurtv.AmateurTv;
import ctbrec.ui.tabs.PaginatedScheduledService;
import ctbrec.ui.tabs.TabProvider;
import ctbrec.ui.tabs.ThumbOverviewTab;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
public class AmateurTvTabProvider implements TabProvider {
private AmateurTv amateurTv;
private Recorder recorder;
public AmateurTvTabProvider(AmateurTv amateurTv) {
this.amateurTv = amateurTv;
this.recorder = amateurTv.getRecorder();
}
@Override
public List<Tab> getTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>();
// female
String url = AmateurTv.baseUrl + "/v3/readmodel/cache/cams/W";
var updateService = new AmateurTvUpdateService(amateurTv, url);
tabs.add(createTab("Female", updateService));
// male
url = AmateurTv.baseUrl + "/v3/readmodel/cache/cams/M";
updateService = new AmateurTvUpdateService(amateurTv, url);
tabs.add(createTab("Male", updateService));
return tabs;
}
private Tab createTab(String title, PaginatedScheduledService updateService) {
var tab = new ThumbOverviewTab(title, updateService, amateurTv);
tab.setRecorder(recorder);
return tab;
}
@Override
public Tab getFollowedTab() {
return null;
}
}

View File

@ -0,0 +1,84 @@
package ctbrec.ui.sites.amateurtv;
import static ctbrec.io.HttpConstants.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
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.amateurtv.AmateurTv;
import ctbrec.sites.amateurtv.AmateurTvModel;
import ctbrec.ui.tabs.PaginatedScheduledService;
import javafx.concurrent.Task;
import okhttp3.Request;
public class AmateurTvUpdateService extends PaginatedScheduledService {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTvUpdateService.class);
private static final int ITEMS_PER_PAGE = 50;
private AmateurTv site;
private String url;
public AmateurTvUpdateService(AmateurTv site, String url) {
this.site = site;
this.url = url;
}
@Override
protected Task<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException {
return loadModelList();
}
};
}
private List<Model> loadModelList() throws IOException {
int offset = page - 1;
String pageUrl = url + '/' + offset * ITEMS_PER_PAGE + '/' + ITEMS_PER_PAGE + "/es";
LOG.debug("Fetching page {}", pageUrl);
var request = new Request.Builder()
.url(pageUrl)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT, Locale.ENGLISH.getLanguage())
.header(REFERER, site.getBaseUrl())
.build();
try (var response = site.getHttpClient().execute(request)) {
if (response.isSuccessful()) {
var content = response.body().string();
List<Model> models = new ArrayList<>();
var json = new JSONObject(content);
var modelNodes = json.getJSONObject("cams").getJSONArray("nodes");
parseModels(modelNodes, models);
return models;
} else {
int code = response.code();
throw new IOException("HTTP status " + code);
}
}
}
private void parseModels(JSONArray jsonModels, List<Model> models) {
for (var i = 0; i < jsonModels.length(); i++) {
var m = jsonModels.getJSONObject(i);
var user = m.getJSONObject("user");
var name = user.optString("username");
AmateurTvModel model = (AmateurTvModel) site.createModel(name);
model.setPreview(m.optString("imageURL"));
model.setDescription(m.optJSONObject("topic").optString("text"));
models.add(model);
}
}
}

View File

@ -0,0 +1,132 @@
package ctbrec.sites.amateurtv;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Model;
import ctbrec.io.HttpClient;
import ctbrec.sites.AbstractSite;
public class AmateurTv extends AbstractSite {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTv.class);
public static String baseUrl = "https://www.amateur.tv";
private AmateurTvHttpClient httpClient;
@Override
public void init() throws IOException {
// nothing to do
}
@Override
public String getName() {
return "Amateur.tv";
}
@Override
public String getBaseUrl() {
return baseUrl;
}
@Override
public Model createModel(String name) {
AmateurTvModel model = new AmateurTvModel();
model.setName(name);
model.setUrl(baseUrl + '/' + name);
model.setDescription("");
model.setSite(this);
return model;
}
@Override
public Double getTokenBalance() throws IOException {
return Double.valueOf(0);
}
@Override
public String getBuyTokensLink() {
return getAffiliateLink();
}
@Override
public synchronized boolean login() throws IOException {
return credentialsAvailable() && getHttpClient().login();
}
@Override
public HttpClient getHttpClient() {
if (httpClient == null) {
httpClient = new AmateurTvHttpClient();
}
return httpClient;
}
@Override
public void shutdown() {
if (httpClient != null) {
httpClient.shutdown();
}
}
@Override
public boolean supportsTips() {
return true;
}
@Override
public boolean supportsFollow() {
return true;
}
@Override
public boolean supportsSearch() {
return true;
}
@Override
public boolean searchRequiresLogin() {
return true;
}
@Override
public List<Model> search(String q) throws IOException, InterruptedException {
return Collections.emptyList();
}
@Override
public boolean isSiteForModel(Model m) {
return m instanceof AmateurTvModel;
}
@Override
public boolean credentialsAvailable() {
//String username = Config.getInstance().getSettings().bongaUsername;
//return username != null && !username.trim().isEmpty();
return false;
}
@Override
public Model createModelFromUrl(String url) {
Matcher m = Pattern.compile("https?://.*?amateur.tv/.*").matcher(url);
if(m.matches()) {
String modelName = m.group(1);
return createModel(modelName);
} else {
return super.createModelFromUrl(url);
}
}
@Override
public String getAffiliateLink() {
return baseUrl;
}
}

View File

@ -0,0 +1,22 @@
package ctbrec.sites.amateurtv;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.io.HttpClient;
public class AmateurTvHttpClient extends HttpClient {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTvHttpClient.class);
public AmateurTvHttpClient() {
super("amateurtv");
}
@Override
public boolean login() throws IOException {
return false;
}
}

View File

@ -0,0 +1,155 @@
package ctbrec.sites.amateurtv;
import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import javax.xml.bind.JAXBException;
import org.json.JSONObject;
import org.jsoup.nodes.Element;
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.ParsingMode;
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.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import okhttp3.Request;
import okhttp3.Response;
public class AmateurTvModel extends AbstractModel {
private static final Logger LOG = LoggerFactory.getLogger(AmateurTvModel.class);
private boolean online = false;
@Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
if (ignoreCache) {
JSONObject json = getModelInfo();
online = json.optString("status").equalsIgnoreCase("online");
onlineState = online ? ONLINE : OFFLINE;
}
return online;
}
@Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if (failFast) {
return onlineState;
} else {
try {
isOnline(true);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
onlineState = OFFLINE;
} catch (IOException | ExecutionException e) {
onlineState = OFFLINE;
}
return onlineState;
}
}
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
List<StreamSource> streamSources = new ArrayList<>();
String streamUrl = getStreamUrl();
Request req = new Request.Builder().url(streamUrl).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, ParsingMode.LENIENT);
Playlist playlist = parser.parse();
MasterPlaylist master = playlist.getMasterPlaylist();
for (PlaylistData playlistData : master.getPlaylists()) {
StreamSource streamsource = new StreamSource();
Element img = new Element("img");
img.setBaseUri(streamUrl);
img.attr("src", playlistData.getUri());
streamsource.mediaPlaylistUrl = img.absUrl("src");
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.add(streamsource);
}
} else {
throw new HttpException(response.code(), response.message());
}
}
return streamSources;
}
private String getStreamUrl() throws IOException {
JSONObject json = getModelInfo();
JSONObject videoTech = json.getJSONObject("videoTechnologies");
return videoTech.getString("hlsV2");
}
@Override
public void invalidateCacheEntries() {
// nothing to do
}
@Override
public void receiveTip(Double tokens) throws IOException {
// not supported
}
@Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
try {
return new int[] { getStreamSources().get(0).width, getStreamSources().get(0).height };
} catch (Exception e) {
throw new ExecutionException(e);
}
}
@Override
public boolean follow() throws IOException {
return false;
}
@Override
public boolean unfollow() throws IOException {
return false;
}
private JSONObject getModelInfo() throws IOException {
String url = AmateurTv.baseUrl + "/v3/readmodel/show/" + getName() + "/es";
Request req = new Request.Builder().url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, "en")
.header(REFERER, getSite().getBaseUrl() + '/' + getName())
.build();
try (Response resp = site.getHttpClient().execute(req)) {
JSONObject json = new JSONObject(HttpClient.bodyToJsonObject(resp));
return json;
}
}
}

View File

@ -65,6 +65,7 @@ import ctbrec.recorder.OnlineMonitor;
import ctbrec.recorder.Recorder;
import ctbrec.servlet.StaticFileServlet;
import ctbrec.sites.Site;
import ctbrec.sites.amateurtv.AmateurTv;
import ctbrec.sites.bonga.BongaCams;
import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda;
@ -142,6 +143,7 @@ public class HttpServer {
}
private void createSites() {
sites.add(new AmateurTv());
sites.add(new BongaCams());
sites.add(new Cam4());
sites.add(new Camsoda());