package ctbrec.sites.flirt4free; import java.io.IOException; import java.io.InputStream; import java.net.URLEncoder; import java.util.ArrayList; 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.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.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import ctbrec.AbstractModel; import ctbrec.Config; import ctbrec.Model; import ctbrec.io.HttpException; import ctbrec.recorder.download.StreamSource; import okhttp3.Request; import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; public class Flirt4FreeModel extends AbstractModel { private static final transient Logger LOG = LoggerFactory.getLogger(Flirt4FreeModel.class); private String id; private String chatHost; private String chatPort; private String chatToken; private String streamHost; private String streamUrl; int[] resolution = new int[2]; private Object monitor = new Object(); private boolean online = false; @Override public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { if(ignoreCache) { String url = "https://ws.vs3.com/rooms/check-model-status.php?model_name=" + getName(); Request request = new Request.Builder() .url(url) .header("Accept", "*/*") .header("Accept-Language", "en-US,en;q=0.5") .header("Referer", getUrl()) .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) .header("X-Requested-With", "XMLHttpRequest") .build(); try(Response response = getSite().getHttpClient().execute(request)) { if(response.isSuccessful()) { JSONObject json = new JSONObject(response.body().string()); online = Objects.equals(json.optString("status"), "online"); if(online) { try { loadStreamUrl(); } catch(Exception e) { online = false; onlineState = Model.State.OFFLINE; } } } else { throw new HttpException(response.code(), response.message()); } } } return online; } private void loadModelInfo() throws IOException { String url = getSite().getBaseUrl() + "/webservices/chat-room-interface.php?a=login_room&model_id=" + id; LOG.trace("Loading url {}", url); Request request = new Request.Builder() .url(url) .header("Accept", "*/*") .header("Accept-Language", "en-US,en;q=0.5") .header("Referer", getUrl()) .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) .header("X-Requested-With", "XMLHttpRequest") .build(); try(Response response = getSite().getHttpClient().execute(request)) { if(response.isSuccessful()) { JSONObject json = new JSONObject(response.body().string()); if(json.optString("status").equals("success")) { JSONObject config = json.getJSONObject("config"); JSONObject performer = config.getJSONObject("performer"); setName(performer.optString("name_seo", "n/a")); setDisplayName(performer.optString("name", "n/a")); setUrl(getSite().getBaseUrl() + "/rooms/" + getName() + '/'); JSONObject room = config.getJSONObject("room"); chatHost = room.getString("host"); chatPort = room.getString("port_to_be"); chatToken = json.getString("token_enc"); } else { LOG.trace("Loading model info failed. Assuming model {} is offline", getName()); online = false; onlineState = Model.State.OFFLINE; } } else { throw new HttpException(response.code(), response.message()); } } } @Override public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { return getStreamSources(true); } private List getStreamSources(boolean withWebsocket) throws IOException, ExecutionException, ParseException, PlaylistException { MasterPlaylist masterPlaylist = null; try { if(withWebsocket) { loadStreamUrl(); } masterPlaylist = getMasterPlaylist(); } catch (InterruptedException e) { throw new ExecutionException(e); } List 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; src.mediaPlaylistUrl = "https://manifest.vscdns.com/" + playlist.getUri(); LOG.trace("Media playlist {}", src.mediaPlaylistUrl); sources.add(src); } } return sources; } public MasterPlaylist getMasterPlaylist() throws IOException, ParseException, PlaylistException, InterruptedException { LOG.trace("Loading master playlist {}", streamUrl); Request req = new Request.Builder() .url(streamUrl) .header("Accept", "*/*") .header("Accept-Language", "en-US,en;q=0.5") .header("Referer", getUrl()) .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) .header("X-Requested-With", "XMLHttpRequest") .build(); try (Response response = getSite().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(); return master; } else { throw new HttpException(response.code(), response.message()); } } } private void loadStreamUrl() throws IOException, InterruptedException { loadModelInfo(); Objects.requireNonNull(chatHost, "chatHost is null"); String h = chatHost.replaceAll("chat", "chat-vip"); String url = "https://" + h + "/chat?token=" + URLEncoder.encode(chatToken, "utf-8") + "&port_to_be=" + chatPort; LOG.trace("Opening chat websocket {}", url); Request req = new Request.Builder() .url(url) .header("Accept", "*/*") .header("Accept-Language", "en-US,en;q=0.5") .header("Referer", getUrl()) .header("User-Agent", Config.getInstance().getSettings().httpUserAgent) .header("X-Requested-With", "XMLHttpRequest") .build(); getSite().getHttpClient().newWebSocket(req, new WebSocketListener() { @Override public void onOpen(WebSocket webSocket, Response response) { LOG.trace("Chat websocket for {} opened", getName()); } @Override public void onMessage(WebSocket webSocket, String text) { LOG.trace("Chat wbesocket for {}: {}", getName(), text); JSONObject json = new JSONObject(text); if (json.optString("command").equals("8011")) { JSONObject data = json.getJSONObject("data"); streamHost = data.getString("stream_host"); online = true; try { resolution[0] = Integer.parseInt(data.getString("stream_width")); resolution[1] = Integer.parseInt(data.getString("stream_height")); } catch(Exception e) { LOG.warn("Couldn't determine stream resolution", e); } webSocket.close(1000, ""); } } @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { LOG.error("Chat websocket for {} failed", getName(), t); synchronized (monitor) { monitor.notify(); } } @Override public void onClosed(WebSocket webSocket, int code, String reason) { LOG.trace("Chat websocket for {} closed {} {}", getName(), code, reason); synchronized (monitor) { monitor.notify(); } } }); synchronized (monitor) { monitor.wait(10_000); if (streamHost == null) { throw new RuntimeException("Couldn't determine streaming server for model " + getName()); } else { streamUrl = "https://manifest.vscdns.com/manifest.m3u8.m3u8?key=nil&provider=level3&secure=true&host=" + streamHost + "&model_id=" + id; } } } @Override public void invalidateCacheEntries() { } @Override public void receiveTip(Double tokens) throws IOException { } @Override public int[] getStreamResolution(boolean failFast) throws ExecutionException { if(failFast) { return resolution; } else { if(streamUrl != null) { try { List streamSources = getStreamSources(false); Collections.sort(streamSources); StreamSource best = streamSources.get(streamSources.size()-1); resolution = new int[] {best.width, best.height}; } catch (IOException | ParseException | PlaylistException e) { throw new ExecutionException("Couldn't determine stream resolution", e); } } return resolution; } } @Override public boolean follow() throws IOException { return false; } @Override public boolean unfollow() throws IOException { return false; } public void setId(String id) { this.id = id; } @Override public void readSiteSpecificData(JsonReader reader) throws IOException { reader.nextName(); id = reader.nextString(); } @Override public void writeSiteSpecificData(JsonWriter writer) throws IOException { writer.name("id").value(id); } public void setStreamUrl(String streamUrl) { this.streamUrl = streamUrl; } public void setOnline(boolean b) { online = b; } }