231 lines
8.8 KiB
Java
231 lines
8.8 KiB
Java
package ctbrec.sites.manyvids;
|
|
|
|
import static ctbrec.Model.State.*;
|
|
import static ctbrec.io.HttpConstants.*;
|
|
import static ctbrec.sites.manyvids.MVLive.*;
|
|
import static java.nio.charset.StandardCharsets.*;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
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.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 ctbrec.AbstractModel;
|
|
import ctbrec.Config;
|
|
import ctbrec.Model;
|
|
import ctbrec.StringUtil;
|
|
import ctbrec.io.HttpException;
|
|
import ctbrec.recorder.download.Download;
|
|
import ctbrec.recorder.download.StreamSource;
|
|
import okhttp3.Request;
|
|
import okhttp3.Response;
|
|
|
|
public class MVLiveModel extends AbstractModel {
|
|
|
|
private static final transient Logger LOG = LoggerFactory.getLogger(MVLiveModel.class);
|
|
|
|
private MVLiveHttpClient httpClient;
|
|
private MVLiveClient client;
|
|
private String roomNumber;
|
|
|
|
@Override
|
|
public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException {
|
|
if (ignoreCache) {
|
|
MVLive site = (MVLive) getSite();
|
|
for (Model model : site.getModels()) {
|
|
if (model.getName().equalsIgnoreCase(getName()) || model.getDisplayName().equalsIgnoreCase(getName())) {
|
|
this.onlineState = model.getOnlineState(true);
|
|
setName(model.getName());
|
|
setDisplayName(model.getDisplayName());
|
|
setUrl(model.getUrl());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return this.onlineState == ONLINE;
|
|
}
|
|
|
|
@Override
|
|
public List<StreamSource> getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException {
|
|
LOG.debug("Loading {}", getUrl());
|
|
try {
|
|
StreamLocation streamLocation = getClient().getStreamLocation(this);
|
|
LOG.debug("Got the stream location from WS {}", streamLocation.masterPlaylist);
|
|
roomNumber = streamLocation.roomNumber;
|
|
updateCloudFlareCookies();
|
|
MasterPlaylist masterPlaylist = getMasterPlaylist(streamLocation.masterPlaylist);
|
|
List<StreamSource> 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 = streamLocation.masterPlaylist;
|
|
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
|
|
String segmentUri = baseUrl + playlist.getUri();
|
|
src.mediaPlaylistUrl = segmentUri;
|
|
if(src.mediaPlaylistUrl.contains("?")) {
|
|
src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?'));
|
|
}
|
|
LOG.debug("Media playlist {}", src.mediaPlaylistUrl);
|
|
sources.add(src);
|
|
}
|
|
}
|
|
return sources;
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
}
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
private MasterPlaylist getMasterPlaylist(String url) throws IOException, ParseException, PlaylistException {
|
|
LOG.trace("Loading master playlist {}", url);
|
|
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();
|
|
InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8));
|
|
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
|
|
Playlist playlist = parser.parse();
|
|
MasterPlaylist master = playlist.getMasterPlaylist();
|
|
return master;
|
|
} else {
|
|
throw new HttpException(response.code(), response.message());
|
|
}
|
|
}
|
|
}
|
|
|
|
public void updateCloudFlareCookies() throws IOException, InterruptedException {
|
|
String url = "https://" + APP_HOST + "/api/" + getRoomNumber() + "/player-settings/" + getDisplayName();
|
|
LOG.debug("Getting CF cookies: {}", url);
|
|
Request req = new Request.Builder()
|
|
.url(url)
|
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
|
.build();
|
|
try (Response response = getHttpClient().execute(req)) {
|
|
if (!response.isSuccessful()) {
|
|
LOG.debug(response.body().string());
|
|
throw new HttpException(response.code(), response.message());
|
|
}
|
|
}
|
|
}
|
|
|
|
public String getRoomNumber() throws IOException, InterruptedException {
|
|
if (StringUtil.isBlank(roomNumber)) {
|
|
JSONObject json = getRoomLocation();
|
|
if (json.optBoolean("success")) {
|
|
roomNumber = json.getString("floorId");
|
|
} else {
|
|
LOG.debug("Room number response: {}", json.toString(2));
|
|
throw new RuntimeException(getName() + " is offline");
|
|
}
|
|
}
|
|
return roomNumber;
|
|
}
|
|
|
|
private void fetchGeneralCookies() throws IOException {
|
|
Request req = new Request.Builder()
|
|
.url(getSite().getBaseUrl())
|
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
|
.build();
|
|
try (Response response = getHttpClient().execute(req)) {
|
|
if (!response.isSuccessful()) {
|
|
throw new HttpException(response.code(), response.message());
|
|
}
|
|
}
|
|
}
|
|
|
|
public JSONObject getRoomLocation() throws IOException {
|
|
fetchGeneralCookies();
|
|
httpClient.fetchAuthenticationCookies();
|
|
String url = "https://roompool.live.manyvids.com/roompool/" + getDisplayName() + "?private=false";
|
|
LOG.debug("Fetching room location from {}", url);
|
|
Request req = new Request.Builder()
|
|
.url(url)
|
|
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
|
|
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
|
|
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
|
|
.header(REFERER, MVLive.WS_ORIGIN + "/stream/" + getName())
|
|
.build();
|
|
try (Response response = getHttpClient().execute(req)) {
|
|
if (response.isSuccessful()) {
|
|
String body = response.body().string();
|
|
JSONObject json = new JSONObject(body);
|
|
LOG.trace("Room location response: {}", json);
|
|
return json;
|
|
} else {
|
|
throw new HttpException(response.code(), response.message());
|
|
}
|
|
}
|
|
}
|
|
|
|
private synchronized MVLiveClient getClient() {
|
|
if (client == null) {
|
|
client = new MVLiveClient(getHttpClient());
|
|
}
|
|
return client;
|
|
}
|
|
|
|
@Override
|
|
public void invalidateCacheEntries() {
|
|
roomNumber = null;
|
|
}
|
|
|
|
@Override
|
|
public void receiveTip(Double tokens) throws IOException {
|
|
}
|
|
|
|
@Override
|
|
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
|
|
return new int[] {1280, 720};
|
|
}
|
|
|
|
@Override
|
|
public boolean follow() throws IOException {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public boolean unfollow() throws IOException {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public Download createDownload() {
|
|
if (Config.isServerMode() && !Config.getInstance().getSettings().recordSingleFile) {
|
|
return new MVLiveHlsDownload(getHttpClient());
|
|
} else {
|
|
return new MVLiveMergedHlsDownload(getHttpClient());
|
|
}
|
|
}
|
|
|
|
private synchronized MVLiveHttpClient getHttpClient() {
|
|
if (httpClient == null) {
|
|
MVLiveHttpClient siteHttpClient = (MVLiveHttpClient) getSite().getHttpClient();
|
|
httpClient = siteHttpClient.newSession();
|
|
}
|
|
return httpClient;
|
|
}
|
|
}
|