forked from j62/ctbrec
1
0
Fork 0

First working version of Showup.tv

This commit is contained in:
0xboobface 2020-05-16 16:28:44 +02:00
parent 57125b4820
commit 6c85a2a493
15 changed files with 626 additions and 18 deletions

View File

@ -46,6 +46,7 @@ import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.flirt4free.Flirt4Free;
import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.showup.Showup;
import ctbrec.sites.streamate.Streamate;
import ctbrec.sites.stripchat.Stripchat;
import ctbrec.ui.news.NewsTab;
@ -98,6 +99,7 @@ public class CamrecApplication extends Application {
sites.add(new Flirt4Free());
sites.add(new LiveJasmin());
sites.add(new MyFreeCams());
sites.add(new Showup());
sites.add(new Streamate());
sites.add(new Stripchat());
loadConfig();

View File

@ -9,6 +9,7 @@ import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.flirt4free.Flirt4Free;
import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.showup.Showup;
import ctbrec.sites.streamate.Streamate;
import ctbrec.sites.stripchat.Stripchat;
import ctbrec.ui.sites.bonga.BongaCamsSiteUi;
@ -19,6 +20,7 @@ import ctbrec.ui.sites.fc2live.Fc2LiveSiteUi;
import ctbrec.ui.sites.flirt4free.Flirt4FreeSiteUi;
import ctbrec.ui.sites.jasmin.LiveJasminSiteUi;
import ctbrec.ui.sites.myfreecams.MyFreeCamsSiteUi;
import ctbrec.ui.sites.showup.ShowupSiteUi;
import ctbrec.ui.sites.streamate.StreamateSiteUi;
import ctbrec.ui.sites.stripchat.StripchatSiteUi;
@ -32,6 +34,7 @@ public class SiteUiFactory {
private static Flirt4FreeSiteUi flirt4FreeSiteUi;
private static LiveJasminSiteUi jasminSiteUi;
private static MyFreeCamsSiteUi mfcSiteUi;
private static ShowupSiteUi showupSiteUi;
private static StreamateSiteUi streamateSiteUi;
private static StripchatSiteUi stripchatSiteUi;
@ -73,6 +76,11 @@ public class SiteUiFactory {
mfcSiteUi = new MyFreeCamsSiteUi((MyFreeCams) site);
}
return mfcSiteUi;
} else if (site instanceof Showup) {
if (showupSiteUi == null) {
showupSiteUi = new ShowupSiteUi((Showup) site);
}
return showupSiteUi;
} else if (site instanceof Streamate) {
if (streamateSiteUi == null) {
streamateSiteUi = new StreamateSiteUi((Streamate) site);

View File

@ -0,0 +1,33 @@
package ctbrec.ui.sites.showup;
import java.io.IOException;
import ctbrec.sites.ConfigUI;
import ctbrec.sites.showup.Showup;
import ctbrec.ui.sites.AbstractSiteUi;
import ctbrec.ui.tabs.TabProvider;
public class ShowupSiteUi extends AbstractSiteUi {
private Showup site;
public ShowupSiteUi(Showup site) {
this.site = site;
}
@Override
public TabProvider getTabProvider() {
return new ShowupTabProvider(site);
}
@Override
public ConfigUI getConfigUI() {
return null;
}
@Override
public boolean login() throws IOException {
return false;
}
}

View File

@ -0,0 +1,40 @@
package ctbrec.ui.sites.showup;
import java.util.ArrayList;
import java.util.List;
import ctbrec.sites.showup.Showup;
import ctbrec.ui.tabs.TabProvider;
import ctbrec.ui.tabs.ThumbOverviewTab;
import javafx.scene.Scene;
import javafx.scene.control.Tab;
public class ShowupTabProvider extends TabProvider {
private Showup site;
public ShowupTabProvider(Showup site) {
this.site = site;
}
@Override
public List<Tab> getTabs(Scene scene) {
List<Tab> tabs = new ArrayList<>();
tabs.add(createTab("Women", "female"));
tabs.add(createTab("Men", "male"));
tabs.add(createTab("All", "all"));
return tabs;
}
@Override
public Tab getFollowedTab() {
return null;
}
private Tab createTab(String title, String category) {
ShowupUpdateService updateService = new ShowupUpdateService(site, category);
ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, site);
tab.setRecorder(site.getRecorder());
return tab;
}
}

View File

@ -0,0 +1,34 @@
package ctbrec.ui.sites.showup;
import java.io.IOException;
import java.util.List;
import ctbrec.Model;
import ctbrec.sites.showup.Showup;
import ctbrec.sites.showup.ShowupHttpClient;
import ctbrec.ui.tabs.PaginatedScheduledService;
import javafx.concurrent.Task;
public class ShowupUpdateService extends PaginatedScheduledService {
private Showup showup;
private String category;
public ShowupUpdateService(Showup showup, String category) {
this.showup = showup;
this.category = category;
}
@Override
protected Task<List<Model>> createTask() {
return new Task<List<Model>>() {
@Override
public List<Model> call() throws IOException {
ShowupHttpClient httpClient = (ShowupHttpClient) showup.getHttpClient();
httpClient.setCookie("category", category);
return showup.getModelList();
}
};
}
}

View File

@ -0,0 +1,12 @@
package ctbrec;
public class ModelNotFoundException extends RuntimeException {
public ModelNotFoundException(String modelName) {
super(modelName);
}
public ModelNotFoundException(String modelName, Exception e) {
super(modelName, e);
}
}

View File

@ -37,9 +37,13 @@ public class PlaylistGenerator {
private List<ProgressListener> listeners = new ArrayList<>();
public File generate(File directory) throws IOException, ParseException, PlaylistException {
return generate(directory, "ts");
}
public File generate(File directory, String fileSuffix) throws IOException, ParseException, PlaylistException {
LOG.info("Starting playlist generation for {}", directory);
// get a list of all ts files and sort them by sequence
File[] files = directory.listFiles(f -> f.getName().endsWith(".ts"));
File[] files = directory.listFiles(f -> f.getName().endsWith('.' + fileSuffix));
if (files == null || files.length == 0) {
LOG.debug("{} is empty. Not going to generate a playlist", directory);
throw new InvalidPlaylistException("Directory is empty");
@ -57,15 +61,18 @@ public class PlaylistGenerator {
int done = 0;
for (File file : files) {
try {
float duration = (float) MpegUtil.getFileDuration(file);
if (duration <= 0) {
throw new InvalidTrackLengthException("Track has negative duration: " + file.getName());
} else {
track.add(new TrackData.Builder()
.withUri(file.getName())
.withTrackInfo(new TrackInfo(duration, file.getName()))
.build());
float duration = 0;
if (file.getName().toLowerCase().endsWith(".ts")) {
duration = (float) MpegUtil.getFileDuration(file);
if (duration <= 0) {
throw new InvalidTrackLengthException("Track has negative duration: " + file.getName());
}
}
track.add(new TrackData.Builder()
.withUri(file.getName())
.withTrackInfo(new TrackInfo(duration, file.getName()))
.build());
} catch (Exception e) {
LOG.warn("Couldn't determine duration for {}. Skipping this file.", file.getName());
File corruptedFile = new File(directory, file.getName() + ".corrupt");

View File

@ -9,6 +9,7 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.net.URL;
import java.nio.file.FileSystems;
import java.nio.file.Files;
@ -60,7 +61,7 @@ public class HlsDownload extends AbstractHlsDownload {
private NumberFormat nf = new DecimalFormat("000000");
private transient AtomicBoolean downloadFinished = new AtomicBoolean(false);
private ZonedDateTime splitRecStartTime;
private transient Config config;
protected transient Config config;
public HlsDownload(HttpClient client) {
super(client);
@ -270,6 +271,7 @@ public class HlsDownload extends AbstractHlsDownload {
@Override
void internalStop() {
running = false;
downloadThreadPool.shutdownNow();
}
private static class SegmentDownload implements Callable<Boolean> {
@ -289,7 +291,7 @@ public class HlsDownload extends AbstractHlsDownload {
@Override
public Boolean call() throws Exception {
LOG.trace("Downloading segment {} to {}", url, file);
for (int tries = 1; tries <= 3; tries++) {
for (int tries = 1; tries <= 3 && !Thread.currentThread().isInterrupted(); tries++) {
Request request = new Request.Builder()
.url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
@ -304,7 +306,7 @@ public class HlsDownload extends AbstractHlsDownload {
}
byte[] b = new byte[1024 * 100];
int length = -1;
while( (length = in.read(b)) >= 0 ) {
while( (length = in.read(b)) >= 0 && !Thread.currentThread().isInterrupted()) {
fos.write(b, 0, length);
}
return true;
@ -312,7 +314,10 @@ public class HlsDownload extends AbstractHlsDownload {
} catch(FileNotFoundException e) {
LOG.debug("Segment does not exist {}", url.getFile());
break;
} catch (InterruptedIOException e) {
break;
} catch(Exception e) {
LOG.error("Error", e);
if (tries == 3) {
LOG.warn("Error while downloading segment. Segment {} finally failed: {}", file.toFile().getName(), e.getMessage());
} else {

View File

@ -1,5 +1,7 @@
package ctbrec.recorder.download.hls;
import static ctbrec.io.HttpConstants.*;
import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
@ -50,7 +52,7 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
private transient Config config;
private transient Process ffmpeg;
private transient OutputStream ffmpegStdIn;
private transient Thread ffmpegThread;
protected transient Thread ffmpegThread;
private transient Object ffmpegStartMonitor = new Object();
public MergedFfmpegHlsDownload(HttpClient client) {
@ -178,7 +180,7 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
ffmpegThread.start();
}
private void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
protected void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
int lastSegment = 0;
int nextSegment = 0;
while (running) {
@ -307,17 +309,21 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
}
}
private void writeSegment(byte[] segmentData) throws IOException {
protected void writeSegment(byte[] segmentData, int offset, int length) throws IOException {
if (running) {
if (ffmpegStdIn != null) {
ffmpegStdIn.write(segmentData);
ffmpegStdIn.write(segmentData, offset, length);
} else {
LOG.error("FFmpeg stdin stream is null - skipping writing of segment");
}
}
}
private boolean splitRecording() {
private void writeSegment(byte[] segmentData) throws IOException {
writeSegment(segmentData, 0, segmentData.length);
}
protected boolean splitRecording() {
if (config.getSettings().splitRecordings > 0) {
Duration recordingDuration = Duration.between(splitRecStartTime, ZonedDateTime.now());
long seconds = recordingDuration.getSeconds();
@ -406,7 +412,10 @@ public class MergedFfmpegHlsDownload extends AbstractHlsDownload {
LOG.trace("Downloading segment {}", url.getFile());
int maxTries = 3;
for (int i = 1; i <= maxTries && running; i++) {
Request request = new Request.Builder().url(url).addHeader("connection", "keep-alive").build();
Request request = new Request.Builder().url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(CONNECTION, KEEP_ALIVE)
.build();
try (Response response = client.execute(request)) {
if (response.isSuccessful()) {
byte[] segment = response.body().bytes();

View File

@ -0,0 +1,169 @@
package ctbrec.sites.showup;
import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.ModelNotFoundException;
import ctbrec.UnknownModel;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.sites.AbstractSite;
import okhttp3.Request;
import okhttp3.Response;
public class Showup extends AbstractSite {
private static final transient Logger LOG = LoggerFactory.getLogger(Showup.class);
private ShowupHttpClient httpClient;
@Override
public String getName() {
return "Showup.tv";
}
@Override
public String getBaseUrl() {
return "https://showup.tv";
}
@Override
public String getAffiliateLink() {
return getBaseUrl();
}
@Override
public Model createModel(String name) {
try {
for (Model m : getModelList()) {
if (Objects.equal(m.getName(), name)) {
return m;
}
}
} catch (IOException e) {
throw new ModelNotFoundException(name, e);
}
throw new ModelNotFoundException(name);
}
public List<Model> getModelList() throws IOException {
String url = getBaseUrl() + "/site/get_stream_list/big";
Request req = new Request.Builder()
.url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.build();
try (Response response = getHttpClient().execute(req)) {
if (response.isSuccessful()) {
String body = response.body().string();
LOG.trace(body);
JSONObject json = new JSONObject(body);
List<Model> models = new ArrayList<>();
JSONArray list = json.getJSONArray("list");
for (int i = 0; i < list.length(); i++) {
JSONObject entry = list.getJSONObject(i);
ShowupModel model = new ShowupModel();
model.setUid(entry.getLong("uid"));
model.setPreview(getBaseUrl() + "/files/" + entry.optString("big_img") + ".jpg");
model.setDescription(entry.optString("description"));
model.setName(entry.optString("username"));
model.setUrl(getBaseUrl() + '/' + model.getName());
if(entry.optInt("is_group") == 1) {
model.setOnlineState(GROUP);
} else if(entry.optInt("is_prv") == 1) {
model.setOnlineState(PRIVATE);
} else {
model.setOnlineState(ONLINE);
}
model.setStreamId(entry.optString("stream_id"));
model.setStreamTranscoderAddr(entry.optString("stream_transcoder_addr"));
model.setSite(this);
models.add(model);
}
return models;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
@Override
public Double getTokenBalance() throws IOException {
return 0d;
}
@Override
public String getBuyTokensLink() {
return getBaseUrl();
}
@Override
public boolean login() throws IOException {
return false;
}
@Override
public HttpClient getHttpClient() {
if (httpClient == null) {
httpClient = new ShowupHttpClient();
}
return httpClient;
}
@Override
public void init() throws IOException {
// noop
}
@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 ShowupModel;
}
@Override
public boolean credentialsAvailable() {
return false;
}
@Override
public Model createModelFromUrl(String url) {
Matcher matcher = Pattern.compile(getBaseUrl() + "(?:/profile)?/(.*)").matcher(url);
if (matcher.find()) {
return createModel(matcher.group(1));
} else {
return new UnknownModel();
}
}
}

View File

@ -0,0 +1,39 @@
package ctbrec.sites.showup;
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Recording;
import ctbrec.io.HttpClient;
import ctbrec.recorder.PlaylistGenerator;
import ctbrec.recorder.download.hls.HlsDownload;
public class ShowupDownload extends HlsDownload {
public ShowupDownload(HttpClient client) {
super(client);
}
@Override
protected File generatePlaylist(Recording recording) throws IOException, ParseException, PlaylistException {
File recDir = recording.getAbsoluteFile();
if (!config.getSettings().generatePlaylist) {
return null;
}
PlaylistGenerator playlistGenerator = new PlaylistGenerator();
playlistGenerator.addProgressListener(recording::setProgress);
File playlist = playlistGenerator.generate(recDir, "mp4");
recording.setProgress(-1);
return playlist;
}
@Override
public Duration getLength() {
return Duration.between(getStartTime(), Instant.now());
}
}

View File

@ -0,0 +1,41 @@
package ctbrec.sites.showup;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.google.common.base.Objects;
import ctbrec.io.HttpClient;
import okhttp3.Cookie;
public class ShowupHttpClient extends HttpClient {
protected ShowupHttpClient() {
super("showup");
setCookie("accept_rules", "true");
setCookie("category", "all");
}
public void setCookie(String name, String value) {
Cookie cookie = new Cookie.Builder().domain("showup.tv").name(name).value(value).build();
Map<String, List<Cookie>> cookies = cookieJar.getCookies();
List<Cookie> cookiesForDomain = cookies.computeIfAbsent(cookie.domain(), k -> new ArrayList<Cookie>());
for (Iterator<Cookie> iterator = cookiesForDomain.iterator(); iterator.hasNext();) {
Cookie existingCookie = iterator.next();
if (Objects.equal(existingCookie.name(), cookie.name())) {
iterator.remove();
}
}
cookiesForDomain.add(cookie);
}
@Override
public boolean login() throws IOException {
return false;
}
}

View File

@ -0,0 +1,72 @@
package ctbrec.sites.showup;
import static ctbrec.io.HttpConstants.*;
import java.io.IOException;
import java.io.InputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
import okhttp3.Request;
import okhttp3.Response;
public class ShowupMergedDownload extends MergedFfmpegHlsDownload {
private static final Logger LOG = LoggerFactory.getLogger(ShowupMergedDownload.class);
public ShowupMergedDownload(HttpClient client) {
super(client);
}
@Override
protected void downloadSegments(String segmentPlaylistUri, boolean livestreamDownload) throws IOException, ParseException, PlaylistException {
try {
SegmentPlaylist lsp = getNextSegments(segmentPlaylistUri);
emptyPlaylistCheck(lsp);
for (String segment : lsp.segments) {
Request request = new Request.Builder().url(segment)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(CONNECTION, KEEP_ALIVE)
.build();
try (Response response = client.execute(request)) {
if (response.isSuccessful()) {
InputStream in = response.body().byteStream();
byte[] buffer = new byte[10240];
int length = -1;
while ((length = in.read(buffer)) >= 0 && running && !Thread.interrupted()) {
writeSegment(buffer, 0, length);
if (livestreamDownload && splitRecording()) {
break;
}
}
} else {
throw new HttpException(response.code(), response.message());
}
}
}
} catch (HttpException e) {
if (e.getResponseCode() == 404) {
LOG.debug("Playlist not found (404). Model {} probably went offline", model);
} else if (e.getResponseCode() == 403) {
LOG.debug("Playlist access forbidden (403). Model {} probably went private or offline", model);
} else {
LOG.info("Unexpected error while downloading {}", model, e);
}
running = false;
} catch (Exception e) {
LOG.info("Unexpected error while downloading {}", model, e);
running = false;
}
ffmpegThread.interrupt();
}
}

View File

@ -0,0 +1,135 @@
package ctbrec.sites.showup;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import javax.xml.bind.JAXBException;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.recorder.download.Download;
import ctbrec.recorder.download.StreamSource;
public class ShowupModel extends AbstractModel {
private long uid;
private String streamId;
private String streamTranscoderAddr;
@Override
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
List<Model> modelList = getShowupSite().getModelList();
for (Model model : modelList) {
ShowupModel m = (ShowupModel) model;
if (m.getUid() == uid) {
return m.getOnlineState(false) != State.ONLINE;
}
}
return false;
}
@Override
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
StreamSource src = new StreamSource();
src.width = 480;
src.height = 360;
if(streamId == null || streamTranscoderAddr == null) {
List<Model> modelList = getShowupSite().getModelList();
for (Model model : modelList) {
ShowupModel m = (ShowupModel) model;
if (m.getUid() == uid) {
streamId = m.getStreamId();
streamTranscoderAddr = m.getStreamTranscoderAddr();
}
}
}
int cdnHost = 1 + new Random().nextInt(5);
src.mediaPlaylistUrl = MessageFormat.format("https://cdn-e0{0}.showup.tv/h5live/http/playlist.m3u8?url=rtmp%3A%2F%2F{1}%3A1935%2Fwebrtc&stream={2}_aac", cdnHost, streamTranscoderAddr, streamId);
return Collections.singletonList(src);
}
@Override
public void invalidateCacheEntries() {
// noop
}
@Override
public void receiveTip(Double tokens) throws IOException {
// noop
}
@Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
return new int[] { 480, 360 };
}
@Override
public boolean follow() throws IOException {
return false;
}
@Override
public boolean unfollow() throws IOException {
return false;
}
public long getUid() {
return uid;
}
public void setUid(long uid) {
this.uid = uid;
}
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
reader.nextName();
uid = reader.nextLong();
}
@Override
public void writeSiteSpecificData(JsonWriter writer) throws IOException {
writer.name("uid").value(uid);
}
private Showup getShowupSite() {
return (Showup) getSite();
}
public String getStreamId() {
return streamId;
}
public void setStreamId(String streamId) {
this.streamId = streamId;
}
public String getStreamTranscoderAddr() {
return streamTranscoderAddr;
}
public void setStreamTranscoderAddr(String streamTranscoderAddr) {
this.streamTranscoderAddr = streamTranscoderAddr;
}
@Override
public Download createDownload() {
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
return new ShowupDownload(getSite().getHttpClient());
} else {
return new ShowupMergedDownload(getSite().getHttpClient());
}
}
}

View File

@ -57,6 +57,7 @@ import ctbrec.sites.fc2live.Fc2Live;
import ctbrec.sites.flirt4free.Flirt4Free;
import ctbrec.sites.jasmin.LiveJasmin;
import ctbrec.sites.mfc.MyFreeCams;
import ctbrec.sites.showup.Showup;
import ctbrec.sites.streamate.Streamate;
import ctbrec.sites.stripchat.Stripchat;
@ -110,6 +111,7 @@ public class HttpServer {
sites.add(new Flirt4Free());
sites.add(new LiveJasmin());
sites.add(new MyFreeCams());
sites.add(new Showup());
sites.add(new Streamate());
sites.add(new Stripchat());
}