diff --git a/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeUpdateService.java b/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeUpdateService.java index 5651549c..0bdc73e4 100644 --- a/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeUpdateService.java +++ b/client/src/main/java/ctbrec/ui/sites/flirt4free/Flirt4FreeUpdateService.java @@ -14,8 +14,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; @@ -74,13 +72,13 @@ public class Flirt4FreeUpdateService extends PaginatedScheduledService { try { Flirt4FreeModel model = parseModel(modelJson); models.add(model); - } catch(Exception e) { + } catch (Exception e) { LOG.warn("Couldn't parse model {}", modelJson); } } return models.stream() .filter(filter) - .skip((page - 1) * (long)MODELS_PER_PAGE) + .skip((page - 1) * (long) MODELS_PER_PAGE) .limit(MODELS_PER_PAGE) .collect(Collectors.toList()); } else { @@ -112,6 +110,14 @@ public class Flirt4FreeUpdateService extends PaginatedScheduledService { if (modelData.has("category_id_3")) { model.getCategories().add(modelData.getString("category_id_3")); } + if (modelData.has("video_width") && modelData.has("video_aspect_ratio")) { + String[] aspectRatioParts = modelData.getString("video_aspect_ratio").split(":"); + double aspectWidth = Integer.parseInt(aspectRatioParts[0]); + double aspectHeight = Integer.parseInt(aspectRatioParts[1]); + int width = Integer.parseInt(modelData.getString("video_width")); + int height = (int) Math.ceil(width * aspectHeight / aspectWidth); + model.setStreamResolution(new int[]{width, height}); + } return model; } } diff --git a/common/src/main/java/ctbrec/StringConstants.java b/common/src/main/java/ctbrec/StringConstants.java new file mode 100644 index 00000000..f664d3f2 --- /dev/null +++ b/common/src/main/java/ctbrec/StringConstants.java @@ -0,0 +1,10 @@ +package ctbrec; + +public class StringConstants { + + public static final String MODEL_ID = "model_id"; + public static final String STATUS = "status"; + + private StringConstants() { + } +} diff --git a/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java index e74ecd1a..6406f9d9 100644 --- a/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java +++ b/common/src/main/java/ctbrec/sites/flirt4free/Flirt4FreeModel.java @@ -1,6 +1,21 @@ package ctbrec.sites.flirt4free; -import static ctbrec.io.HttpConstants.*; +import com.iheartradio.m3u8.*; +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.*; +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; @@ -12,35 +27,15 @@ import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; -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; +import static ctbrec.StringConstants.MODEL_ID; +import static ctbrec.StringConstants.STATUS; +import static ctbrec.io.HttpConstants.*; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Locale.ENGLISH; public class Flirt4FreeModel extends AbstractModel { - private static final transient Logger LOG = LoggerFactory.getLogger(Flirt4FreeModel.class); + private static final Logger LOG = LoggerFactory.getLogger(Flirt4FreeModel.class); private String id; private String chatHost; private String chatPort; @@ -72,7 +67,7 @@ public class Flirt4FreeModel extends AbstractModel { Request request = new Request.Builder() .url(url) .header(ACCEPT, "*/*") - .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") + .header(ACCEPT_LANGUAGE, ENGLISH.getLanguage()) .header(REFERER, getUrl()) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) @@ -97,7 +92,7 @@ public class Flirt4FreeModel extends AbstractModel { return; } JSONObject json = new JSONObject(body); - online = Objects.equals(json.optString("status"), "online"); // online is true, even if the model is in private or away + online = Objects.equals(json.optString(STATUS), "online"); // online is true, even if the model is in private or away updateModelId(json); if (online) { try { @@ -110,12 +105,11 @@ public class Flirt4FreeModel extends AbstractModel { } private void updateModelId(JSONObject json) { - if (json.has("model_id")) { - Object modelId = json.get("model_id"); - if (modelId instanceof Number) { - Number n = (Number) modelId; + if (json.has(MODEL_ID)) { + Object modelId = json.get(MODEL_ID); + if (modelId instanceof Number n) { if (n.intValue() > 0) { - id = String.valueOf(json.get("model_id")); + id = String.valueOf(json.get(MODEL_ID)); } } } @@ -127,7 +121,7 @@ public class Flirt4FreeModel extends AbstractModel { Request request = new Request.Builder() .url(url) .header(ACCEPT, "*/*") - .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") + .header(ACCEPT_LANGUAGE, ENGLISH.getLanguage()) .header(REFERER, getUrl()) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) @@ -135,7 +129,7 @@ public class Flirt4FreeModel extends AbstractModel { try (Response response = getSite().getHttpClient().execute(request)) { if (response.isSuccessful()) { JSONObject json = new JSONObject(response.body().string()); - if (json.optString("status").equals("success")) { + if (json.optString(STATUS).equals("success")) { JSONObject config = json.getJSONObject("config"); JSONObject performer = config.getJSONObject("performer"); setUrl(getSite().getBaseUrl() + "/rooms/" + getName() + '/'); @@ -144,7 +138,7 @@ public class Flirt4FreeModel extends AbstractModel { chatHost = room.getString("host"); chatPort = room.getString("port_to_be"); chatToken = json.getString("token_enc"); - String status = room.optString("status"); + String status = room.optString(STATUS); setOnlineState(mapStatus(status)); online = onlineState == State.ONLINE; JSONObject user = config.getJSONObject("user"); @@ -161,16 +155,12 @@ public class Flirt4FreeModel extends AbstractModel { } private State mapStatus(String status) { - switch (status) { - case "P": - case "F": - return State.PRIVATE; - case "A": - return State.AWAY; - case "O": - default: - return State.ONLINE; - } + return switch (status) { + case "P", "F" -> State.PRIVATE; + case "A" -> State.AWAY; + case "O" -> State.ONLINE; + default -> State.UNKNOWN; + }; } @Override @@ -179,7 +169,7 @@ public class Flirt4FreeModel extends AbstractModel { } private List getStreamSources(boolean withWebsocket) throws IOException, ExecutionException, ParseException, PlaylistException { - MasterPlaylist masterPlaylist = null; + MasterPlaylist masterPlaylist; try { if (withWebsocket) { acquireSlot(); @@ -200,7 +190,8 @@ public class Flirt4FreeModel extends AbstractModel { StreamSource src = new StreamSource(); src.bandwidth = playlist.getStreamInfo().getBandwidth(); src.height = playlist.getStreamInfo().getResolution().height; - src.mediaPlaylistUrl = "https://manifest.vscdns.com/" + playlist.getUri(); + HttpUrl masterPlaylistUrl = HttpUrl.parse(streamUrl); + src.mediaPlaylistUrl = "https://" + masterPlaylistUrl.host() + '/' + playlist.getUri(); LOG.trace("Media playlist {}", src.mediaPlaylistUrl); sources.add(src); } @@ -213,7 +204,7 @@ public class Flirt4FreeModel extends AbstractModel { Request req = new Request.Builder() .url(streamUrl) .header(ACCEPT, "*/*") - .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") + .header(ACCEPT_LANGUAGE, ENGLISH.getLanguage()) .header(REFERER, getUrl()) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) @@ -238,12 +229,12 @@ public class Flirt4FreeModel extends AbstractModel { loadModelInfo(); Objects.requireNonNull(chatHost, "chatHost is null"); String h = chatHost.replace("chat", "chat-vip"); - String url = "https://" + h + "/chat?token=" + URLEncoder.encode(chatToken, "utf-8") + "&port_to_be=" + chatPort; + 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(ACCEPT_LANGUAGE, ENGLISH.getLanguage()) .header(REFERER, getUrl()) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(X_REQUESTED_WITH, XML_HTTP_REQUEST) @@ -268,14 +259,14 @@ public class Flirt4FreeModel extends AbstractModel { String roomState = data.optString("room_state"); onlineState = mapStatus(roomState); online = onlineState == State.ONLINE; - if(data.optString("room_state").equals("0") && data.optString("login_group_id").equals("14")) { + if (data.optString("room_state").equals("0") && data.optString("login_group_id").equals("14")) { onlineState = Model.State.GROUP; online = false; } try { resolution[0] = Integer.parseInt(data.getString("stream_width")); resolution[1] = Integer.parseInt(data.getString("stream_height")); - } catch(Exception e) { + } catch (Exception e) { LOG.warn("Couldn't determine stream resolution", e); } webSocket.close(1000, ""); @@ -288,7 +279,9 @@ public class Flirt4FreeModel extends AbstractModel { synchronized (monitor) { monitor.notifyAll(); } - response.close(); + if (response != null) { + response.close(); + } } @Override @@ -305,8 +298,25 @@ public class Flirt4FreeModel extends AbstractModel { 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=highwinds&secure=true&host=" + streamHost + "&model_id=" + id; - LOG.debug("Stream URL is {}", streamUrl); + url = getSite().getBaseUrl() + "/ws/chat/get-stream-urls.php?" + + "model_id=" + id + + "&video_host=" + streamHost + + "&t=" + System.currentTimeMillis(); + LOG.debug("Loading master playlist information: {}", url); + req = new Request.Builder() + .url(url) + .header(ACCEPT, "*/*") + .header(ACCEPT_LANGUAGE, ENGLISH.getLanguage()) + .header(REFERER, getUrl()) + .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) + .header(REFERER, getUrl()) + .build(); + try (Response response = getSite().getHttpClient().execute(req)) { + JSONObject json = new JSONObject(Objects.requireNonNull(response.body(), "HTTP response body is null").string()); + JSONArray hls = json.getJSONObject("data").getJSONArray("hls"); + streamUrl = "https:" + hls.getJSONObject(0).getString("url"); + LOG.debug("Stream URL is {}", streamUrl); + } } } } @@ -339,7 +349,7 @@ public class Flirt4FreeModel extends AbstractModel { Request req = new Request.Builder() .url(url) .header(ACCEPT, "*/*") - .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") + .header(ACCEPT_LANGUAGE, ENGLISH.getLanguage()) .header(REFERER, getUrl()) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(REFERER, getUrl()) @@ -412,21 +422,21 @@ public class Flirt4FreeModel extends AbstractModel { @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); - } + if (!failFast && streamUrl != null && resolution[0] == 0) { + try { + List streamSources = getStreamSources(true); + 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; } + return resolution; + } + + public void setStreamResolution(int[] res) { + this.resolution = res; } @Override @@ -521,7 +531,7 @@ public class Flirt4FreeModel extends AbstractModel { requestThrottle.acquire(); long now = System.currentTimeMillis(); long millisSinceLastRequest = now - lastRequest; - if(millisSinceLastRequest < 500) { + if (millisSinceLastRequest < 500) { Thread.sleep(500 - millisSinceLastRequest); } }