Added PreConnectionEvent with LiveType, made optimizations, and added fallback to default request in proxy class in case proxy protocol is not supported by TikTok or Signing server.

This commit is contained in:
kohlerpop1
2024-02-15 11:46:13 -05:00
parent 4f141edb1a
commit 243ce9bc94
13 changed files with 78 additions and 188 deletions

View File

@@ -1,35 +0,0 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok.data.requests;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class SignServerResponse
{
private String signedUrl;
private String userAgent;
}

View File

@@ -80,6 +80,11 @@ public class LiveClientSettings {
*/
private String roomId;
/**
* Optional: API Key for increased limit to signing server
*/
private String apiKey;
public static LiveClientSettings createDefault()
{
var httpSettings = new HttpClientSettings();

View File

@@ -33,7 +33,7 @@ import java.util.function.Consumer;
@Setter
public class ProxyClientSettings implements Iterator<ProxyData>
{
private boolean enabled, lastSuccess, autoDiscard = true, fallback = true;
private boolean enabled, autoDiscard = true, fallback = true;
private Rotation rotation = Rotation.CONSECUTIVE;
private final List<ProxyData> proxyList = new ArrayList<>();
private int index = -1;
@@ -63,10 +63,6 @@ public class ProxyClientSettings implements Iterator<ProxyData>
@Override
public ProxyData next() {
return lastSuccess ? proxyList.get(index) : rotate();
}
public ProxyData rotate() {
var nextProxy = switch (rotation)
{
case CONSECUTIVE -> {
@@ -84,12 +80,11 @@ public class ProxyClientSettings implements Iterator<ProxyData>
};
onProxyUpdated.accept(nextProxy);
return nextProxy;
}
}
@Override
public void remove() {
proxyList.remove(index);
lastSuccess = false; // index is no longer valid and lastSuccess needs falsified
}
public void setIndex(int index) {

View File

@@ -27,9 +27,8 @@ import io.github.jwdeveloper.tiktok.data.requests.LiveConnectionData;
import io.github.jwdeveloper.tiktok.data.requests.LiveData;
import io.github.jwdeveloper.tiktok.data.requests.LiveUserData;
public interface LiveHttpClient {
public interface LiveHttpClient
{
/**
* @return list of gifts that are available in your country
*/
@@ -37,28 +36,32 @@ public interface LiveHttpClient {
/**
* Returns information about user that is having a livestream
*
* @param userName
* @return
* @param userName name of user
* @return {@link LiveUserData.Response}
*/
LiveUserData.Response fetchLiveUserData(String userName);
default LiveUserData.Response fetchLiveUserData(String userName) {
return fetchLiveUserData(new LiveUserData.Request(userName));
}
LiveUserData.Response fetchLiveUserData(LiveUserData.Request request);
/**
* @param roomId can be obtained from browsers cookies or by invoked fetchLiveUserData
* @return
* @return {@link LiveData.Response}
*/
LiveData.Response fetchLiveData(String roomId);
default LiveData.Response fetchLiveData(String roomId) {
return fetchLiveData(new LiveData.Request(roomId));
}
LiveData.Response fetchLiveData(LiveData.Request request);
/**
* @param roomId can be obtained from browsers cookies or by invoked fetchLiveUserData
* @return
* @return {@link LiveConnectionData.Response}
*/
LiveConnectionData.Response fetchLiveConnectionData(String roomId);
default LiveConnectionData.Response fetchLiveConnectionData(String roomId) {
return fetchLiveConnectionData(new LiveConnectionData.Request(roomId));
}
LiveConnectionData.Response fetchLiveConnectionData(LiveConnectionData.Request request);
}
}

View File

@@ -32,8 +32,7 @@ import io.github.jwdeveloper.tiktok.data.events.room.TikTokRoomInfoEvent;
import io.github.jwdeveloper.tiktok.data.requests.LiveConnectionData;
import io.github.jwdeveloper.tiktok.data.requests.LiveData;
import io.github.jwdeveloper.tiktok.data.requests.LiveUserData;
import io.github.jwdeveloper.tiktok.exceptions.TikTokLiveException;
import io.github.jwdeveloper.tiktok.exceptions.TikTokLiveOfflineHostException;
import io.github.jwdeveloper.tiktok.exceptions.*;
import io.github.jwdeveloper.tiktok.gifts.TikTokGiftManager;
import io.github.jwdeveloper.tiktok.listener.ListenersManager;
import io.github.jwdeveloper.tiktok.listener.TikTokListenersManager;
@@ -127,22 +126,26 @@ public class TikTokLiveClient implements LiveClient {
var userData = httpClient.fetchLiveUserData(userDataRequest);
liveRoomInfo.setStartTime(userData.getStartedAtTimeStamp());
liveRoomInfo.setRoomId(userData.getRoomId());
if (userData.getUserStatus() == LiveUserData.UserStatus.Offline) {
throw new TikTokLiveOfflineHostException("User is offline: "+liveRoomInfo.getHostUser());
}
if (userData.getUserStatus() == LiveUserData.UserStatus.NotFound) {
throw new TikTokLiveOfflineHostException("User not found: "+liveRoomInfo.getHostUser());
}
if (userData.getUserStatus() == LiveUserData.UserStatus.Offline)
throw new TikTokLiveOfflineHostException("User is offline: "+liveRoomInfo.getHostName());
if (userData.getUserStatus() == LiveUserData.UserStatus.NotFound)
throw new TikTokLiveOfflineHostException("User not found: "+liveRoomInfo.getHostName());
var liveDataRequest = new LiveData.Request(userData.getRoomId());
var liveData = httpClient.fetchLiveData(liveDataRequest);
if (liveData.isAgeRestricted())
throw new TikTokLiveException("Livestream for "+liveRoomInfo.getHostName()+" is 18+ or age restricted!");
if (liveData.getLiveStatus() == LiveData.LiveStatus.HostNotFound)
throw new TikTokLiveOfflineHostException("LiveStream for "+liveRoomInfo.getHostName()+" could not be found.");
if (liveData.getLiveStatus() == LiveData.LiveStatus.HostOffline)
throw new TikTokLiveOfflineHostException("LiveStream for "+liveRoomInfo.getHostName()+" not found, is the Host offline?");
tikTokEventHandler.publish(this, new TikTokRoomDataResponseEvent(liveData));
if (liveData.getLiveStatus() == LiveData.LiveStatus.HostNotFound) {
throw new TikTokLiveOfflineHostException("LiveStream for Host name could not be found.");
}
if (liveData.getLiveStatus() == LiveData.LiveStatus.HostOffline) {
throw new TikTokLiveOfflineHostException("LiveStream for not be found, is the Host offline?");
}
liveRoomInfo.setTitle(liveData.getTitle());
liveRoomInfo.setViewersCount(liveData.getViewers());

View File

@@ -99,7 +99,8 @@ public class TikTokLiveClientBuilder implements LiveClientBuilder {
}
public TikTokLiveClientBuilder addListener(TikTokEventListener listener) {
listeners.add(listener);
if (listener != null)
listeners.add(listener);
return this;
}

View File

@@ -36,10 +36,9 @@ import java.util.Optional;
public class TikTokLiveHttpClient implements LiveHttpClient {
/**
* Signing API by Isaac Kogan
* https://github-wiki-see.page/m/isaackogan/TikTokLive/wiki/All-About-Signatures
*/
private static final String TIKTOK_SIGN_API = "https://tiktok.eulerstream.com/webcast/sign_url";
* <a href="https://github-wiki-see.page/m/isaackogan/TikTokLive/wiki/All-About-Signatures">Signing API by Isaac Kogan</a>
*/
private static final String TIKTOK_SIGN_API = "https://tiktok.eulerstream.com/webcast/fetch";
private static final String TIKTOK_URL_WEB = "https://www.tiktok.com/";
private static final String TIKTOK_URL_WEBCAST = "https://webcast.tiktok.com/webcast/";
@@ -47,7 +46,6 @@ public class TikTokLiveHttpClient implements LiveHttpClient {
private final LiveClientSettings clientSettings;
private final LiveUserDataMapper liveUserDataMapper;
private final LiveDataMapper liveDataMapper;
private final SignServerResponseMapper signServerResponseMapper;
private final GiftsDataMapper giftsDataMapper;
public TikTokLiveHttpClient(HttpClientFactory factory, LiveClientSettings settings) {
@@ -55,7 +53,6 @@ public class TikTokLiveHttpClient implements LiveHttpClient {
clientSettings = settings;
liveUserDataMapper = new LiveUserDataMapper();
liveDataMapper = new LiveDataMapper();
signServerResponseMapper = new SignServerResponseMapper();
giftsDataMapper = new GiftsDataMapper();
}
@@ -94,12 +91,6 @@ public class TikTokLiveHttpClient implements LiveHttpClient {
return giftsDataMapper.map(json);
}
@Override
public LiveUserData.Response fetchLiveUserData(String userName) {
return fetchLiveUserData(new LiveUserData.Request(userName));
}
@Override
public LiveUserData.Response fetchLiveUserData(LiveUserData.Request request) {
var url = TIKTOK_URL_WEB + "api-live/user/room";
@@ -136,11 +127,6 @@ public class TikTokLiveHttpClient implements LiveHttpClient {
return liveUserDataMapper.map(json);
}
@Override
public LiveData.Response fetchLiveData(String roomId) {
return fetchLiveData(new LiveData.Request(roomId));
}
@Override
public LiveData.Response fetchLiveData(LiveData.Request request) {
var url = TIKTOK_URL_WEBCAST + "room/info";
@@ -175,20 +161,13 @@ public class TikTokLiveHttpClient implements LiveHttpClient {
return liveDataMapper.map(json);
}
@Override
public LiveConnectionData.Response fetchLiveConnectionData(String roomId) {
return fetchLiveConnectionData(new LiveConnectionData.Request(roomId));
}
@Override
public LiveConnectionData.Response fetchLiveConnectionData(LiveConnectionData.Request request) {
HttpResponse<byte[]> credentialsResponse = getOptionalProxyResponse(request).orElseGet(()-> {
SignServerResponse signServerResponse = getSignedUrl(request.getRoomId());
return getWebsocketCredentialsResponse(signServerResponse.getSignedUrl());
});
HttpResponse<byte[]> credentialsResponse = getOptionalProxyResponse(request).orElseGet(()-> getStarterPayload(request.getRoomId()));
try {
var optionalHeader = credentialsResponse.headers().firstValue("set-cookie");
System.out.println(credentialsResponse.headers().map());
var optionalHeader = credentialsResponse.headers().firstValue("x-set-tt-cookie");
if (optionalHeader.isEmpty()) {
throw new TikTokSignServerException("Sign server did not return the set-cookie header");
}
@@ -210,39 +189,21 @@ public class TikTokLiveHttpClient implements LiveHttpClient {
}
}
SignServerResponse getSignedUrl(String roomId) {
var urlToSign = httpFactory
.client(TikTokLiveHttpClient.TIKTOK_URL_WEBCAST + "im/fetch")
.withParam("room_id", roomId)
.build()
.toUrl();
HttpResponse<byte[]> getStarterPayload(String room_id) {
HttpClientBuilder builder = httpFactory.client(TIKTOK_SIGN_API)
.withParam("client", "ttlive-java")
.withParam("uuc", "1")
.withParam("room_id", room_id);
if (clientSettings.getApiKey() != null)
builder.withParam("apiKey", clientSettings.getApiKey());
var optional = httpFactory
.client(TikTokLiveHttpClient.TIKTOK_SIGN_API)
.withParam("client", "ttlive-java")
.withParam("uuc", "1")
.withParam("url", urlToSign.toString())
.build()
.toJsonResponse();
var optional = builder.build().toResponse();
if (optional.isEmpty()) {
throw new TikTokSignServerException("Unable to sign url: " + urlToSign);
}
var json = optional.get();
return signServerResponseMapper.map(json);
}
HttpResponse<byte[]> getWebsocketCredentialsResponse(String signedUrl) {
var optionalResponse = httpFactory
.clientEmpty(signedUrl)
.build()
.toResponse();
if (optionalResponse.isEmpty()) {
throw new TikTokSignServerException("Unable to get websocket connection credentials");
}
return optionalResponse.get();
return optional.get();
}
Optional<HttpResponse<byte[]>> getOptionalProxyResponse(LiveConnectionData.Request request) {
@@ -250,9 +211,7 @@ public class TikTokLiveHttpClient implements LiveHttpClient {
if (proxyClientSettings.isEnabled()) {
while (proxyClientSettings.hasNext()) {
try {
SignServerResponse signServerResponse = getSignedUrl(request.getRoomId());
HttpResponse<byte[]> credentialsResponse = getWebsocketCredentialsResponse(signServerResponse.getSignedUrl());
clientSettings.getHttpSettings().getProxyClientSettings().rotate();
HttpResponse<byte[]> credentialsResponse = getStarterPayload(request.getRoomId());
return Optional.of(credentialsResponse);
} catch (TikTokProxyRequestException | TikTokSignServerException ignored) {}
}

View File

@@ -67,16 +67,12 @@ public class HttpProxyClient extends HttpClient
var request = prepareGetRequest();
var response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() != 200) {
proxySettings.setLastSuccess(false);
if (response.statusCode() != 200)
continue;
}
proxySettings.setLastSuccess(true);
return Optional.of(response);
} catch (HttpConnectTimeoutException | ConnectException e) {
if (proxySettings.isAutoDiscard())
proxySettings.remove();
proxySettings.setLastSuccess(false);
throw new TikTokProxyRequestException(e);
} catch (IOException e) {
if (e.getMessage().contains("503") && proxySettings.isFallback()) // Indicates proxy protocol is not supported
@@ -121,14 +117,12 @@ public class HttpProxyClient extends HttpClient
var response = createHttpResponse(body, toUrl(), responseInfo);
proxySettings.setLastSuccess(true);
return Optional.of(response);
} catch (IOException e) {
if (e.getMessage().contains("503") && proxySettings.isFallback()) // Indicates proxy protocol is not supported
return super.toResponse();
if (proxySettings.isAutoDiscard())
proxySettings.remove();
proxySettings.setLastSuccess(false);
throw new TikTokProxyRequestException(e);
} catch (Exception e) {
throw new TikTokLiveRequestException(e);
@@ -137,7 +131,7 @@ public class HttpProxyClient extends HttpClient
throw new TikTokLiveRequestException("No more proxies available!");
} catch (NoSuchAlgorithmException | MalformedURLException | KeyManagementException e) {
// Should never be reached!
System.out.println("handleSocksProxyRequest()! If you see this message, reach us on discord!");
System.out.println("handleSocksProxyRequest: If you see this, message us on discord!");
e.printStackTrace();
return Optional.empty();
} catch (TikTokLiveRequestException e) {

View File

@@ -64,6 +64,10 @@ public class LiveDataMapper {
default -> LiveData.LiveStatus.HostNotFound;
};
response.setLiveStatus(statusValue);
} else if (data.has("prompts") && jsonObject.has("status_code") &&
data.get("prompts").getAsString().isEmpty() && jsonObject.get("status_code").isJsonPrimitive()) {
// 4003110 is age restriction code
response.setAgeRestricted(jsonObject.get("status_code").getAsInt() == 4003110);
} else {
response.setLiveStatus(LiveData.LiveStatus.HostNotFound);
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright (c) 2023-2023 jwdeveloper jacekwoln@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.github.jwdeveloper.tiktok.http.mappers;
import com.google.gson.JsonParser;
import io.github.jwdeveloper.tiktok.data.requests.SignServerResponse;
public class SignServerResponseMapper {
public SignServerResponse map(String json) {
var parsedJson = JsonParser.parseString(json);
var jsonObject = parsedJson.getAsJsonObject();
var signUrl = jsonObject.get("signedUrl").getAsString();
var userAgent = jsonObject.get("User-Agent").getAsString();
return new SignServerResponse(signUrl, userAgent);
}
}

View File

@@ -97,10 +97,14 @@ public class CodeExample {
//RoomId can be used as an override if you're having issues with HostId.
//You can find it in the HTML for the livestream-page
settings.setRoomId("XXXXXXXXXXXXXXXXX");
//Optional:
//API Key for increased limit to signing server
settings.setApiKey("XXXXXXXXXXXXXXXXX");
})
.buildAndConnect();
// </code>
}
}
}

View File

@@ -30,7 +30,6 @@ import java.util.function.Consumer;
public class TikTokLiveRecorder
{
public static LiveRecorder use(Consumer<RecorderSettings> consumer)
{
return new RecorderListener(consumer);
@@ -40,4 +39,4 @@ public class TikTokLiveRecorder
{
return use(x ->{});
}
}
}

View File

@@ -27,7 +27,6 @@ import io.github.jwdeveloper.tiktok.annotations.TikTokEventObserver;
import io.github.jwdeveloper.tiktok.data.events.*;
import io.github.jwdeveloper.tiktok.data.events.http.TikTokRoomDataResponseEvent;
import io.github.jwdeveloper.tiktok.data.settings.LiveClientSettings;
import io.github.jwdeveloper.tiktok.exceptions.TikTokLiveException;
import io.github.jwdeveloper.tiktok.extension.recorder.api.LiveRecorder;
import io.github.jwdeveloper.tiktok.extension.recorder.impl.data.*;
import io.github.jwdeveloper.tiktok.extension.recorder.impl.enums.LiveQuality;
@@ -70,16 +69,12 @@ public class RecorderListener implements LiveRecorder {
var json = event.getLiveData().getJson();
liveClient.getLogger().info("Searching for live download url");
if (settings.getPrepareDownloadData() != null) {
downloadData = settings.getPrepareDownloadData().apply(json);
} else {
downloadData = mapToDownloadData(json);
}
downloadData = settings.getPrepareDownloadData() != null ? settings.getPrepareDownloadData().apply(json) : mapToDownloadData(json);
if (downloadData.getDownloadLiveUrl().isEmpty()) {
throw new TikTokLiveException("Unable to find download live url!");
}
liveClient.getLogger().info("Live download url found!");
if (downloadData.getDownloadLiveUrl().isEmpty())
liveClient.getLogger().warning("Unable to find download live url!");
else
liveClient.getLogger().info("Live download url found!");
}
@TikTokEventObserver
@@ -88,13 +83,13 @@ public class RecorderListener implements LiveRecorder {
try {
var bufferSize = 1024;
var url = new URL(downloadData.getFullUrl());
HttpsURLConnection socksConnection = (HttpsURLConnection) url.openConnection();
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
var headers = LiveClientSettings.DefaultRequestHeaders();
for (var entry : headers.entrySet()) {
socksConnection.setRequestProperty(entry.getKey(), entry.getValue());
connection.setRequestProperty(entry.getKey(), entry.getValue());
}
var in = new BufferedInputStream(socksConnection.getInputStream());
var in = new BufferedInputStream(connection.getInputStream());
var path = settings.getOutputPath() + File.separator + settings.getOutputFileName();
var file = new File(path);
file.getParentFile().mkdirs();