mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-05-02 15:44:40 +02:00
fix(YouTube - Client spoof): Show seekbar thumbnail for age restricted and paid videos
This commit is contained in:
parent
62f92c38c1
commit
01019b09c1
@ -1,20 +1,19 @@
|
|||||||
package app.revanced.integrations.patches.spoof;
|
package app.revanced.integrations.patches.spoof;
|
||||||
|
|
||||||
import static app.revanced.integrations.patches.spoof.requests.StoryBoardRendererRequester.fetchStoryboardRenderer;
|
|
||||||
import static app.revanced.integrations.utils.ReVancedUtils.containsAny;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import app.revanced.integrations.patches.VideoInformation;
|
||||||
|
import app.revanced.integrations.settings.SettingsEnum;
|
||||||
|
import app.revanced.integrations.shared.PlayerType;
|
||||||
|
import app.revanced.integrations.utils.LogHelper;
|
||||||
|
import app.revanced.integrations.utils.ReVancedUtils;
|
||||||
|
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
import app.revanced.integrations.patches.VideoInformation;
|
import static app.revanced.integrations.patches.spoof.requests.StoryboardRendererRequester.getStoryboardRenderer;
|
||||||
import app.revanced.integrations.settings.SettingsEnum;
|
import static app.revanced.integrations.utils.ReVancedUtils.containsAny;
|
||||||
import app.revanced.integrations.shared.PlayerType;
|
|
||||||
import app.revanced.integrations.utils.LogHelper;
|
|
||||||
import app.revanced.integrations.utils.ReVancedUtils;
|
|
||||||
|
|
||||||
/** @noinspection unused*/
|
/** @noinspection unused*/
|
||||||
public class SpoofSignaturePatch {
|
public class SpoofSignaturePatch {
|
||||||
@ -51,6 +50,24 @@ public class SpoofSignaturePatch {
|
|||||||
|
|
||||||
private static volatile Future<StoryboardRenderer> rendererFuture;
|
private static volatile Future<StoryboardRenderer> rendererFuture;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static volatile StoryboardRenderer renderer;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static StoryboardRenderer getRenderer() {
|
||||||
|
if (rendererFuture != null) {
|
||||||
|
try {
|
||||||
|
return rendererFuture.get(5000, TimeUnit.MILLISECONDS);
|
||||||
|
} catch (TimeoutException ex) {
|
||||||
|
LogHelper.printDebug(() -> "Could not get renderer (get timed out)");
|
||||||
|
} catch (ExecutionException | InterruptedException ex) {
|
||||||
|
// Should never happen.
|
||||||
|
LogHelper.printException(() -> "Could not get renderer", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*
|
*
|
||||||
@ -83,27 +100,12 @@ public class SpoofSignaturePatch {
|
|||||||
String videoId = VideoInformation.getVideoId();
|
String videoId = VideoInformation.getVideoId();
|
||||||
if (!videoId.equals(currentVideoId)) {
|
if (!videoId.equals(currentVideoId)) {
|
||||||
currentVideoId = videoId;
|
currentVideoId = videoId;
|
||||||
rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> fetchStoryboardRenderer(videoId));
|
rendererFuture = ReVancedUtils.submitOnBackgroundThread(() -> getStoryboardRenderer(videoId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return INCOGNITO_PARAMETERS;
|
return INCOGNITO_PARAMETERS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static StoryboardRenderer getRenderer() {
|
|
||||||
if (rendererFuture != null) {
|
|
||||||
try {
|
|
||||||
return rendererFuture.get(5000, TimeUnit.MILLISECONDS);
|
|
||||||
} catch (TimeoutException ex) {
|
|
||||||
LogHelper.printDebug(() -> "Could not get renderer (get timed out)");
|
|
||||||
} catch (ExecutionException | InterruptedException ex) {
|
|
||||||
// Should never happen.
|
|
||||||
LogHelper.printException(() -> "Could not get renderer", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@ -136,5 +138,4 @@ public class SpoofSignaturePatch {
|
|||||||
|
|
||||||
return renderer.getRecommendedLevel();
|
return renderer.getRecommendedLevel();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,73 @@ package app.revanced.integrations.patches.spoof.requests;
|
|||||||
|
|
||||||
import app.revanced.integrations.requests.Requester;
|
import app.revanced.integrations.requests.Requester;
|
||||||
import app.revanced.integrations.requests.Route;
|
import app.revanced.integrations.requests.Route;
|
||||||
|
import app.revanced.integrations.utils.LogHelper;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
|
||||||
final class PlayerRoutes {
|
final class PlayerRoutes {
|
||||||
private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/";
|
private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/";
|
||||||
static final Route.CompiledRoute POST_STORYBOARD_SPEC_RENDERER = new Route(
|
static final Route.CompiledRoute GET_STORYBOARD_SPEC_RENDERER = new Route(
|
||||||
Route.Method.POST,
|
Route.Method.POST,
|
||||||
"player" +
|
"player" +
|
||||||
"?fields=storyboards.playerStoryboardSpecRenderer," +
|
"?fields=storyboards.playerStoryboardSpecRenderer," +
|
||||||
"storyboards.playerLiveStoryboardSpecRenderer"
|
"storyboards.playerLiveStoryboardSpecRenderer," +
|
||||||
|
"playabilityStatus.status"
|
||||||
).compile();
|
).compile();
|
||||||
|
|
||||||
|
static final String ANDROID_INNER_TUBE_BODY;
|
||||||
|
static final String TV_EMBED_INNER_TUBE_BODY;
|
||||||
|
|
||||||
|
static {
|
||||||
|
JSONObject innerTubeBody = new JSONObject();
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONObject context = new JSONObject();
|
||||||
|
|
||||||
|
JSONObject client = new JSONObject();
|
||||||
|
client.put("clientName", "ANDROID");
|
||||||
|
client.put("clientVersion", "18.37.36");
|
||||||
|
client.put("androidSdkVersion", 34);
|
||||||
|
|
||||||
|
context.put("client", client);
|
||||||
|
|
||||||
|
innerTubeBody.put("context", context);
|
||||||
|
innerTubeBody.put("videoId", "%s");
|
||||||
|
} catch (JSONException e) {
|
||||||
|
LogHelper.printException(() -> "Failed to create innerTubeBody", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ANDROID_INNER_TUBE_BODY = innerTubeBody.toString();
|
||||||
|
|
||||||
|
JSONObject tvEmbedInnerTubeBody = new JSONObject();
|
||||||
|
|
||||||
|
try {
|
||||||
|
JSONObject context = new JSONObject();
|
||||||
|
|
||||||
|
JSONObject client = new JSONObject();
|
||||||
|
client.put("clientName", "TVHTML5_SIMPLY_EMBEDDED_PLAYER");
|
||||||
|
client.put("clientVersion", "2.0");
|
||||||
|
client.put("platform", "TV");
|
||||||
|
client.put("clientScreen", "EMBED");
|
||||||
|
|
||||||
|
JSONObject thirdParty = new JSONObject();
|
||||||
|
thirdParty.put("embedUrl", "https://www.youtube.com/watch?v=%s");
|
||||||
|
|
||||||
|
context.put("thirdParty", thirdParty);
|
||||||
|
context.put("client", client);
|
||||||
|
|
||||||
|
tvEmbedInnerTubeBody.put("context", context);
|
||||||
|
tvEmbedInnerTubeBody.put("videoId", "%s");
|
||||||
|
} catch (JSONException e) {
|
||||||
|
LogHelper.printException(() -> "Failed to create tvEmbedInnerTubeBody", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
TV_EMBED_INNER_TUBE_BODY = tvEmbedInnerTubeBody.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private PlayerRoutes() {
|
private PlayerRoutes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,14 +78,12 @@ final class PlayerRoutes {
|
|||||||
connection.setRequestProperty("User-Agent", "com.google.android.youtube/18.37.36 (Linux; U; Android 12; GB) gzip");
|
connection.setRequestProperty("User-Agent", "com.google.android.youtube/18.37.36 (Linux; U; Android 12; GB) gzip");
|
||||||
connection.setRequestProperty("X-Goog-Api-Format-Version", "2");
|
connection.setRequestProperty("X-Goog-Api-Format-Version", "2");
|
||||||
connection.setRequestProperty("Content-Type", "application/json");
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
connection.setRequestProperty("Accept-Language", "en-GB, en;q=0.9");
|
|
||||||
connection.setRequestProperty("Pragma", "no-cache");
|
|
||||||
connection.setRequestProperty("Cache-Control", "no-cache");
|
|
||||||
connection.setUseCaches(false);
|
connection.setUseCaches(false);
|
||||||
connection.setDoOutput(true);
|
connection.setDoOutput(true);
|
||||||
|
|
||||||
connection.setConnectTimeout(5000);
|
connection.setConnectTimeout(5000);
|
||||||
connection.setReadTimeout(5000);
|
connection.setReadTimeout(5000);
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,80 +0,0 @@
|
|||||||
package app.revanced.integrations.patches.spoof.requests;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import app.revanced.integrations.patches.spoof.StoryboardRenderer;
|
|
||||||
import app.revanced.integrations.requests.Requester;
|
|
||||||
import app.revanced.integrations.utils.LogHelper;
|
|
||||||
import app.revanced.integrations.utils.ReVancedUtils;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.SocketTimeoutException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.POST_STORYBOARD_SPEC_RENDERER;
|
|
||||||
|
|
||||||
public class StoryBoardRendererRequester {
|
|
||||||
private static final String INNER_TUBE_BODY =
|
|
||||||
"{" +
|
|
||||||
"\"context\": " +
|
|
||||||
"{" +
|
|
||||||
"\"client\": " +
|
|
||||||
"{ " +
|
|
||||||
"\"clientName\": \"ANDROID\", \"clientVersion\": \"18.37.36\", \"platform\": \"MOBILE\", " +
|
|
||||||
"\"osName\": \"Android\", \"osVersion\": \"12\", \"androidSdkVersion\": 31 " +
|
|
||||||
"} " +
|
|
||||||
"}, " +
|
|
||||||
"\"videoId\": \"%s\"" +
|
|
||||||
"}";
|
|
||||||
|
|
||||||
private StoryBoardRendererRequester() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public static StoryboardRenderer fetchStoryboardRenderer(@NonNull String videoId) {
|
|
||||||
try {
|
|
||||||
ReVancedUtils.verifyOffMainThread();
|
|
||||||
Objects.requireNonNull(videoId);
|
|
||||||
|
|
||||||
final byte[] innerTubeBody = String.format(INNER_TUBE_BODY, videoId).getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(POST_STORYBOARD_SPEC_RENDERER);
|
|
||||||
connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length);
|
|
||||||
|
|
||||||
final int responseCode = connection.getResponseCode();
|
|
||||||
|
|
||||||
if (responseCode == 200) {
|
|
||||||
final JSONObject playerResponse = Requester.parseJSONObject(connection);
|
|
||||||
|
|
||||||
if (!playerResponse.has("storyboards")) {
|
|
||||||
// Video is age restricted or paid.
|
|
||||||
LogHelper.printDebug(() -> "Video has no public storyboard: " + videoId);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final JSONObject storyboards = playerResponse.getJSONObject("storyboards");
|
|
||||||
final String storyboardsRendererTag = storyboards.has("playerLiveStoryboardSpecRenderer")
|
|
||||||
? "playerLiveStoryboardSpecRenderer"
|
|
||||||
: "playerStoryboardSpecRenderer";
|
|
||||||
|
|
||||||
final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag);
|
|
||||||
StoryboardRenderer renderer = new StoryboardRenderer(
|
|
||||||
rendererElement.getString("spec"),
|
|
||||||
rendererElement.getInt("recommendedLevel")
|
|
||||||
);
|
|
||||||
LogHelper.printDebug(() -> "Fetched: " + renderer);
|
|
||||||
return renderer;
|
|
||||||
} else {
|
|
||||||
LogHelper.printException(() -> "API not available: " + responseCode);
|
|
||||||
connection.disconnect();
|
|
||||||
}
|
|
||||||
} catch (SocketTimeoutException ex) {
|
|
||||||
LogHelper.printException(() -> "API timed out", ex);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LogHelper.printException(() -> "Failed to fetch StoryBoard URL", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,113 @@
|
|||||||
|
package app.revanced.integrations.patches.spoof.requests;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import app.revanced.integrations.patches.spoof.StoryboardRenderer;
|
||||||
|
import app.revanced.integrations.requests.Requester;
|
||||||
|
import app.revanced.integrations.utils.LogHelper;
|
||||||
|
import app.revanced.integrations.utils.ReVancedUtils;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static app.revanced.integrations.patches.spoof.requests.PlayerRoutes.*;
|
||||||
|
|
||||||
|
public class StoryboardRendererRequester {
|
||||||
|
private StoryboardRendererRequester() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static JSONObject fetchPlayerResponse(@NonNull String requestBody) {
|
||||||
|
try {
|
||||||
|
ReVancedUtils.verifyOffMainThread();
|
||||||
|
Objects.requireNonNull(requestBody);
|
||||||
|
|
||||||
|
final byte[] innerTubeBody = requestBody.getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STORYBOARD_SPEC_RENDERER);
|
||||||
|
connection.getOutputStream().write(innerTubeBody, 0, innerTubeBody.length);
|
||||||
|
|
||||||
|
final int responseCode = connection.getResponseCode();
|
||||||
|
if (responseCode == 200) return Requester.parseJSONObject(connection);
|
||||||
|
|
||||||
|
LogHelper.printException(() -> "API not available: " + responseCode);
|
||||||
|
connection.disconnect();
|
||||||
|
} catch (SocketTimeoutException ex) {
|
||||||
|
LogHelper.printException(() -> "API timed out", ex);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LogHelper.printException(() -> "Failed to fetch storyboard URL", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) {
|
||||||
|
try {
|
||||||
|
return playerResponse.getJSONObject("playabilityStatus").getString("status").equals("OK");
|
||||||
|
} catch (JSONException e) {
|
||||||
|
LogHelper.printException(() -> "Failed to get playabilityStatus", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the storyboardRenderer from the innerTubeBody.
|
||||||
|
* @param innerTubeBody The innerTubeBody to use to fetch the storyboardRenderer.
|
||||||
|
* @return StoryboardRenderer or null if playabilityStatus is not OK.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private static StoryboardRenderer getStoryboardRendererUsingBody(@NonNull String innerTubeBody) {
|
||||||
|
final JSONObject playerResponse = fetchPlayerResponse(innerTubeBody);
|
||||||
|
Objects.requireNonNull(playerResponse);
|
||||||
|
|
||||||
|
if (isPlayabilityStatusOk(playerResponse)) return getStoryboardRendererUsingResponse(playerResponse);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static StoryboardRenderer getStoryboardRendererUsingResponse(@NonNull JSONObject playerResponse) {
|
||||||
|
try {
|
||||||
|
final JSONObject storyboards = playerResponse.getJSONObject("storyboards");
|
||||||
|
final String storyboardsRendererTag = storyboards.has("playerLiveStoryboardSpecRenderer")
|
||||||
|
? "playerLiveStoryboardSpecRenderer"
|
||||||
|
: "playerStoryboardSpecRenderer";
|
||||||
|
|
||||||
|
final var rendererElement = storyboards.getJSONObject(storyboardsRendererTag);
|
||||||
|
StoryboardRenderer renderer = new StoryboardRenderer(
|
||||||
|
rendererElement.getString("spec"),
|
||||||
|
rendererElement.getInt("recommendedLevel")
|
||||||
|
);
|
||||||
|
|
||||||
|
LogHelper.printDebug(() -> "Fetched: " + renderer);
|
||||||
|
|
||||||
|
return renderer;
|
||||||
|
} catch (JSONException e) {
|
||||||
|
LogHelper.printException(() -> "Failed to get storyboardRenderer", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static StoryboardRenderer getStoryboardRenderer(@NonNull String videoId) {
|
||||||
|
try {
|
||||||
|
Objects.requireNonNull(videoId);
|
||||||
|
|
||||||
|
var renderer = getStoryboardRendererUsingBody(String.format(ANDROID_INNER_TUBE_BODY, videoId));
|
||||||
|
if (renderer == null)
|
||||||
|
renderer = getStoryboardRendererUsingBody(String.format(TV_EMBED_INNER_TUBE_BODY, videoId, videoId));
|
||||||
|
|
||||||
|
return renderer;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LogHelper.printException(() -> "Failed to fetch storyboard URL", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user