Fix WinkTv capture

This commit is contained in:
Jafea7 2025-05-04 21:17:44 +10:00
parent 9cfb78f750
commit 9ff933daf1
3 changed files with 105 additions and 78 deletions

View File

@ -77,7 +77,7 @@ public class Settings {
public int defaultPriority = 50; public int defaultPriority = 50;
public boolean deleteOrphanedRecordingMetadata = true; public boolean deleteOrphanedRecordingMetadata = true;
public boolean determineResolution = false; public boolean determineResolution = false;
public List<String> disabledSites = new ArrayList<>(Arrays.asList("Streamray", "WinkTv")); public List<String> disabledSites = new ArrayList<>(Arrays.asList("Streamray"));
public String downloadFilename = "$sanitize(${modelName})_$format(${localDateTime})"; public String downloadFilename = "$sanitize(${modelName})_$format(${localDateTime})";
public List<String> dreamcamTabs = new ArrayList<>(Arrays.asList("girls")); public List<String> dreamcamTabs = new ArrayList<>(Arrays.asList("girls"));
public List<EventHandlerConfiguration> eventHandlers = new ArrayList<>(); public List<EventHandlerConfiguration> eventHandlers = new ArrayList<>();

View File

@ -5,6 +5,7 @@ import ctbrec.StringUtil;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.sites.AbstractSite; import ctbrec.sites.AbstractSite;
import lombok.extern.slf4j.Slf4j;
import okhttp3.FormBody; import okhttp3.FormBody;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
@ -22,14 +23,15 @@ import java.util.regex.Pattern;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
@Slf4j
public class WinkTv extends AbstractSite { public class WinkTv extends AbstractSite {
public static String domain = "www.winktv.co.kr"; public static String domain = "www.winktv.co.kr";
public static String baseUri = "https://www.winktv.co.kr"; public static String baseUri = "https://www.winktv.co.kr";
private HttpClient httpClient; private HttpClient httpClient;
@Override @Override
public void init() throws IOException { public void init() throws IOException {
// Initialization logic if needed
} }
@Override @Override
@ -63,7 +65,7 @@ public class WinkTv extends AbstractSite {
@Override @Override
public Double getTokenBalance() throws IOException { public Double getTokenBalance() throws IOException {
return 0d; return 0.0;
} }
@Override @Override
@ -111,6 +113,7 @@ public class WinkTv extends AbstractSite {
if (StringUtil.isBlank(q)) { if (StringUtil.isBlank(q)) {
return Collections.emptyList(); return Collections.emptyList();
} }
String url = "https://api.winktv.co.kr/v1/live"; String url = "https://api.winktv.co.kr/v1/live";
FormBody body = new FormBody.Builder() FormBody body = new FormBody.Builder()
.add("offset", "0") .add("offset", "0")
@ -118,6 +121,7 @@ public class WinkTv extends AbstractSite {
.add("orderBy", "user") .add("orderBy", "user")
.add("searchVal", URLEncoder.encode(q, "utf-8")) .add("searchVal", URLEncoder.encode(q, "utf-8"))
.build(); .build();
Request req = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)
.header(USER_AGENT, getConfig().getSettings().httpUserAgent) .header(USER_AGENT, getConfig().getSettings().httpUserAgent)
@ -128,28 +132,32 @@ public class WinkTv extends AbstractSite {
.header(ORIGIN, getBaseUrl()) .header(ORIGIN, getBaseUrl())
.post(body) .post(body)
.build(); .build();
try (Response response = getHttpClient().execute(req)) { try (Response response = getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (!response.isSuccessful()) {
JSONObject json = new JSONObject(response.body().string());
if (json.optBoolean("result") && json.has("list")) {
List<Model> models = new ArrayList<>();
JSONArray results = json.getJSONArray("list");
if (results.length() == 0) {
return Collections.emptyList();
}
for (int i = 0; i < results.length(); i++) {
JSONObject result = results.getJSONObject(i);
WinkTvModel model = createModel(result.optString("userId"));
model.setPreview(result.optString("thumbUrl"));
models.add(model);
}
return models;
} else {
return Collections.emptyList();
}
} else {
throw new HttpException(response.code(), response.message()); throw new HttpException(response.code(), response.message());
} }
JSONObject json = new JSONObject(response.body().string());
log.debug("WinkTv: Parsed JSON: {}", json.toString());
if (!json.optBoolean("result") || !json.has("list")) {
return Collections.emptyList();
}
JSONArray results = json.getJSONArray("list");
if (results.length() == 0) {
return Collections.emptyList();
}
List<Model> models = new ArrayList<>();
for (int i = 0; i < results.length(); i++) {
JSONObject result = results.getJSONObject(i);
WinkTvModel model = createModel(result.optString("userId"));
model.setPreview(result.optString("thumbUrl"));
models.add(model);
}
return models;
} }
} }
@ -165,19 +173,21 @@ public class WinkTv extends AbstractSite {
@Override @Override
public Model createModelFromUrl(String url) { public Model createModelFromUrl(String url) {
String[] patterns = { String[] patterns = new String[]{
"https://.*?winktv.co.kr/live/play/([_a-zA-Z0-9]+)", "https://.*?winktv.co.kr/live/play/([_a-zA-Z0-9]+)",
"https://.*?winktv.co.kr/channel/([_a-zA-Z0-9]+)", "https://.*?winktv.co.kr/channel/([_a-zA-Z0-9]+)",
"https://.*?pandalive.co.kr/live/play/([_a-zA-Z0-9]+)", "https://.*?pandalive.co.kr/live/play/([_a-zA-Z0-9]+)",
"https://.*?pandalive.co.kr/channel/([_a-zA-Z0-9]+)" "https://.*?pandalive.co.kr/channel/([_a-zA-Z0-9]+)"
}; };
for (String p : patterns) {
Matcher m = Pattern.compile(p).matcher(url); for (String pattern : patterns) {
Matcher m = Pattern.compile(pattern).matcher(url);
if (m.matches()) { if (m.matches()) {
String modelName = m.group(1); String modelName = m.group(1);
return createModel(modelName); return createModel(modelName);
} }
} }
return super.createModelFromUrl(url); return super.createModelFromUrl(url);
} }
} }

View File

@ -9,9 +9,10 @@ import ctbrec.Config;
import ctbrec.io.HttpException; import ctbrec.io.HttpException;
import ctbrec.recorder.download.RecordingProcess; import ctbrec.recorder.download.RecordingProcess;
import ctbrec.recorder.download.StreamSource; import ctbrec.recorder.download.StreamSource;
import ctbrec.recorder.download.hls.HlsdlDownload;
import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload; import ctbrec.recorder.download.hls.MergedFfmpegHlsDownload;
import lombok.Getter; // import lombok.Getter;
import lombok.Setter; // import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import okhttp3.FormBody; import okhttp3.FormBody;
import okhttp3.Request; import okhttp3.Request;
@ -26,11 +27,13 @@ import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import static ctbrec.Model.State.*; import static ctbrec.Model.State.*;
import static ctbrec.io.HttpConstants.*; import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.UTF_8; import java.nio.charset.StandardCharsets;
@Slf4j @Slf4j
public class WinkTvModel extends AbstractModel { public class WinkTvModel extends AbstractModel {
@ -38,11 +41,8 @@ public class WinkTvModel extends AbstractModel {
private static final String KEY_MEDIA = "media"; private static final String KEY_MEDIA = "media";
private int[] resolution = new int[]{0, 0}; private int[] resolution = new int[]{0, 0};
@Getter
@Setter
private boolean adult = false; private boolean adult = false;
private transient JSONObject modelInfo; private transient JSONObject modelInfo;
private transient Instant lastInfoRequest = Instant.EPOCH; private transient Instant lastInfoRequest = Instant.EPOCH;
@Override @Override
@ -50,6 +50,7 @@ public class WinkTvModel extends AbstractModel {
if (ignoreCache) { if (ignoreCache) {
try { try {
JSONObject json = getModelInfo(); JSONObject json = getModelInfo();
log.debug("WinkTvModel-isOnline: {}", json.toString());
if (json.has(KEY_MEDIA)) { if (json.has(KEY_MEDIA)) {
JSONObject media = json.getJSONObject(KEY_MEDIA); JSONObject media = json.getJSONObject(KEY_MEDIA);
boolean isLive = media.optBoolean("isLive"); boolean isLive = media.optBoolean("isLive");
@ -71,15 +72,16 @@ public class WinkTvModel extends AbstractModel {
@Override @Override
public State getOnlineState(boolean failFast) throws IOException, ExecutionException { public State getOnlineState(boolean failFast) throws IOException, ExecutionException {
if (!failFast || onlineState == UNKNOWN || onlineState == UNCHECKED) { if (failFast && onlineState != UNKNOWN) {
try { return onlineState;
onlineState = isOnline(true) ? ONLINE : OFFLINE; }
} catch (InterruptedException e) { try {
Thread.currentThread().interrupt(); onlineState = isOnline(true) ? ONLINE : OFFLINE;
onlineState = OFFLINE; } catch (InterruptedException e) {
} catch (IOException | ExecutionException e) { Thread.currentThread().interrupt();
onlineState = OFFLINE; onlineState = OFFLINE;
} } catch (IOException | ExecutionException e) {
onlineState = OFFLINE;
} }
return onlineState; return onlineState;
} }
@ -98,8 +100,12 @@ public class WinkTvModel extends AbstractModel {
if (playlist.hasStreamInfo()) { if (playlist.hasStreamInfo()) {
StreamSource src = new StreamSource(); StreamSource src = new StreamSource();
src.setBandwidth(playlist.getStreamInfo().getBandwidth()); src.setBandwidth(playlist.getStreamInfo().getBandwidth());
src.setHeight(playlist.getStreamInfo().getResolution().height); src.setHeight(Optional.ofNullable(playlist.getStreamInfo().getResolution())
src.setWidth(playlist.getStreamInfo().getResolution().width); .map(res -> res.height)
.orElse(0));
src.setWidth(Optional.ofNullable(playlist.getStreamInfo().getResolution())
.map(res -> res.width)
.orElse(0));
src.setMediaPlaylistUrl(playlist.getUri()); src.setMediaPlaylistUrl(playlist.getUri());
log.trace("Media playlist {}", src.getMediaPlaylistUrl()); log.trace("Media playlist {}", src.getMediaPlaylistUrl());
sources.add(src); sources.add(src);
@ -113,11 +119,17 @@ public class WinkTvModel extends AbstractModel {
Request req = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent) .header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, "*/*")
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.header(REFERER, this.getUrl())
.header(ORIGIN, this.getSite().getBaseUrl())
.build(); .build();
try (Response response = getSite().getHttpClient().execute(req)) { try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
String body = response.body().string(); String body = response.body().string();
InputStream inputStream = new ByteArrayInputStream(body.getBytes(UTF_8)); InputStream inputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT); PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8, ParsingMode.LENIENT);
Playlist playlist = parser.parse(); Playlist playlist = parser.parse();
MasterPlaylist master = playlist.getMasterPlaylist(); MasterPlaylist master = playlist.getMasterPlaylist();
@ -130,36 +142,13 @@ public class WinkTvModel extends AbstractModel {
private String getMasterPlaylistUrl() throws IOException { private String getMasterPlaylistUrl() throws IOException {
JSONObject json = getModelInfo(); JSONObject json = getModelInfo();
JSONObject info = json.getJSONObject("bjInfo"); if (json.has("PlayList")) {
long userIdx = info.optLong("idx"); JSONObject playlist = json.getJSONObject("PlayList");
String url = "https://api.winktv.co.kr/v1/live/play"; JSONObject hls = playlist.getJSONArray("hls").getJSONObject(0);
FormBody body = new FormBody.Builder() String hlsUrl = hls.optString("url") + "&player_version=1.20.0";
.add("action", "watch") return hlsUrl;
.add("userIdx", String.valueOf(userIdx))
.add("password", "")
.build();
Request req = new Request.Builder()
.url(url)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.header(ACCEPT, MIMETYPE_APPLICATION_JSON)
.header(ACCEPT_LANGUAGE, Locale.ENGLISH.getLanguage())
.header(X_REQUESTED_WITH, XML_HTTP_REQUEST)
.header(REFERER, getUrl())
.header(ORIGIN, getSite().getBaseUrl())
.post(body)
.build();
try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) {
JSONObject jsonResponse = new JSONObject(response.body().string());
JSONObject playlist = jsonResponse.getJSONObject("PlayList");
JSONObject hls = playlist.getJSONArray("hls").getJSONObject(0);
String hlsUrl = hls.optString("url");
return hlsUrl;
} else {
log.debug("Error while get master playlist url for {}: {}", getName(), response.body().string());
throw new HttpException(response.code(), response.message());
}
} }
throw new IOException("Master playlist URL not found");
} }
@Override @Override
@ -179,18 +168,21 @@ public class WinkTvModel extends AbstractModel {
} }
private JSONObject getModelInfo() throws IOException { private JSONObject getModelInfo() throws IOException {
if (modelInfo == null || Duration.between(lastInfoRequest, Instant.now()).getSeconds() < 5) { if (Objects.nonNull(modelInfo) && Duration.between(lastInfoRequest, Instant.now()).getSeconds() < 5L) {
lastInfoRequest = Instant.now(); return modelInfo;
modelInfo = loadModelInfo();
} }
lastInfoRequest = Instant.now();
modelInfo = loadModelInfo();
return modelInfo; return modelInfo;
} }
private JSONObject loadModelInfo() throws IOException { private JSONObject loadModelInfo() throws IOException {
String url = "https://api.winktv.co.kr/v1/member/bj"; String url = "https://api.winktv.co.kr/v1/live/play";
FormBody body = new FormBody.Builder() FormBody body = new FormBody.Builder()
.add("userId", getName()) .add("userId", getName())
.add("info", KEY_MEDIA) .add("action", "watch")
.add("password", "")
.add("shareLinkType", "")
.build(); .build();
Request req = new Request.Builder() Request req = new Request.Builder()
.url(url) .url(url)
@ -205,6 +197,7 @@ public class WinkTvModel extends AbstractModel {
try (Response response = getSite().getHttpClient().execute(req)) { try (Response response = getSite().getHttpClient().execute(req)) {
if (response.isSuccessful()) { if (response.isSuccessful()) {
JSONObject jsonResponse = new JSONObject(response.body().string()); JSONObject jsonResponse = new JSONObject(response.body().string());
log.debug("WinkTvModel-loadModelInfo: {}", jsonResponse.toString());
return jsonResponse; return jsonResponse;
} else { } else {
throw new HttpException(response.code(), response.message()); throw new HttpException(response.code(), response.message());
@ -212,6 +205,19 @@ public class WinkTvModel extends AbstractModel {
} }
} }
public String getPreviewURL() throws IOException {
JSONObject json = this.getModelInfo();
if (json.has("media")) {
JSONObject media = json.getJSONObject("media");
return media.optString("ivsThumbnail");
}
if (json.has("bjInfo")) {
JSONObject info = json.getJSONObject("bjInfo");
return info.optString("thumbUrl");
}
return "";
}
@Override @Override
public boolean follow() throws IOException { public boolean follow() throws IOException {
return false; return false;
@ -222,6 +228,14 @@ public class WinkTvModel extends AbstractModel {
return false; return false;
} }
public boolean isAdult() {
return adult;
}
public void setAdult(boolean a) {
adult = a;
}
@Override @Override
public void receiveTip(Double tokens) throws IOException { public void receiveTip(Double tokens) throws IOException {
// not implemented // not implemented
@ -236,6 +250,9 @@ public class WinkTvModel extends AbstractModel {
@Override @Override
public RecordingProcess createDownload() { public RecordingProcess createDownload() {
return new MergedFfmpegHlsDownload(getSite().getHttpClient()); if (Config.getInstance().getSettings().useHlsdl) {
return new HlsdlDownload();
}
return new MergedFfmpegHlsDownload(new WinkTvHttpClient(Config.getInstance()));
} }
} }