feat(YouTube): Add Open channel of live avatar patch

This commit is contained in:
inotia00
2025-01-18 20:22:09 +09:00
parent 439f4976bc
commit 9b299b41f5
13 changed files with 427 additions and 7 deletions

View File

@ -44,6 +44,14 @@ object PlayerRoutes {
"&alt=proto"
).compile()
@JvmField
val GET_VIDEO_DETAILS: CompiledRoute = Route(
Route.Method.POST,
"player" +
"?prettyPrint=false" +
"&fields=videoDetails.channelId"
).compile()
private const val YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"
/**

View File

@ -0,0 +1,61 @@
package app.revanced.extension.youtube.patches.general;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.patches.general.requests.VideoDetailsRequest;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.utils.VideoUtils;
@SuppressWarnings("unused")
public final class OpenChannelOfLiveAvatarPatch {
private static final boolean OPEN_CHANNEL_OF_LIVE_AVATAR =
Settings.OPEN_CHANNEL_OF_LIVE_AVATAR.get();
private static volatile String videoId = "";
private static volatile boolean liveChannelAvatarClicked = false;
public static void liveChannelAvatarClicked() {
liveChannelAvatarClicked = true;
}
public static boolean openChannelOfLiveAvatar() {
try {
if (!OPEN_CHANNEL_OF_LIVE_AVATAR) {
return false;
}
if (!liveChannelAvatarClicked) {
return false;
}
VideoDetailsRequest request = VideoDetailsRequest.getRequestForVideoId(videoId);
if (request != null) {
String channelId = request.getInfo();
if (channelId != null) {
liveChannelAvatarClicked = false;
VideoUtils.openChannel(channelId);
return true;
}
}
} catch (Exception ex) {
Logger.printException(() -> "openChannelOfLiveAvatar failure", ex);
}
return false;
}
public static void openChannelOfLiveAvatar(String newlyLoadedVideoId) {
try {
if (!OPEN_CHANNEL_OF_LIVE_AVATAR) {
return;
}
if (newlyLoadedVideoId.isEmpty()) {
return;
}
if (!liveChannelAvatarClicked) {
return;
}
videoId = newlyLoadedVideoId;
VideoDetailsRequest.fetchRequestIfNeeded(newlyLoadedVideoId);
} catch (Exception ex) {
Logger.printException(() -> "openChannelOfLiveAvatar failure", ex);
}
}
}

View File

@ -0,0 +1,141 @@
package app.revanced.extension.youtube.patches.general.requests
import android.annotation.SuppressLint
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.WebClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.net.SocketTimeoutException
import java.util.Collections
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
class VideoDetailsRequest private constructor(
private val videoId: String
) {
private val future: Future<String> = Utils.submitOnBackgroundThread {
fetch(videoId)
}
val info: String?
get() {
try {
return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS]
} catch (ex: TimeoutException) {
Logger.printInfo(
{ "getInfo timed out" },
ex
)
} catch (ex: InterruptedException) {
Logger.printException(
{ "getInfo interrupted" },
ex
)
Thread.currentThread().interrupt() // Restore interrupt status flag.
} catch (ex: ExecutionException) {
Logger.printException(
{ "getInfo failure" },
ex
)
}
return null
}
companion object {
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000L // 20 seconds
@GuardedBy("itself")
val cache: MutableMap<String, VideoDetailsRequest> = Collections.synchronizedMap(
object : LinkedHashMap<String, VideoDetailsRequest>(100) {
private val CACHE_LIMIT = 50
override fun removeEldestEntry(eldest: Map.Entry<String, VideoDetailsRequest>): Boolean {
return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit.
}
})
@JvmStatic
@SuppressLint("ObsoleteSdkInt")
fun fetchRequestIfNeeded(videoId: String) {
cache[videoId] = VideoDetailsRequest(videoId)
}
@JvmStatic
fun getRequestForVideoId(videoId: String): VideoDetailsRequest? {
synchronized(cache) {
return cache[videoId]
}
}
private fun handleConnectionError(toastMessage: String, ex: Exception?) {
Logger.printInfo({ toastMessage }, ex)
}
private fun sendRequest(videoId: String): JSONObject? {
val startTime = System.currentTimeMillis()
val clientType = WebClient.ClientType.MWEB
val clientTypeName = clientType.name
Logger.printDebug { "Fetching video details request for: $videoId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_VIDEO_DETAILS,
clientType
)
val requestBody =
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
val responseCode = connection.responseCode
if (responseCode == 200) return Requester.parseJSONObject(connection)
handleConnectionError(
(clientTypeName + " not available with response code: "
+ responseCode + " message: " + connection.responseMessage),
null
)
} catch (ex: SocketTimeoutException) {
handleConnectionError("Connection timeout", ex)
} catch (ex: IOException) {
handleConnectionError("Network error", ex)
} catch (ex: Exception) {
Logger.printException({ "sendRequest failed" }, ex)
} finally {
Logger.printDebug { "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
}
return null
}
private fun parseResponse(videoDetailsJson: JSONObject): String? {
try {
return videoDetailsJson
.getJSONObject("videoDetails")
.getString("channelId")
} catch (e: JSONException) {
Logger.printException ({ "Fetch failed while processing response data for response: $videoDetailsJson" }, e)
}
return null
}
private fun fetch(videoId: String): String? {
val videoDetailsJson = sendRequest(videoId)
if (videoDetailsJson != null) {
return parseResponse(videoDetailsJson)
}
return null
}
}
}

View File

@ -156,6 +156,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE);
public static final EnumSetting<FormFactor> CHANGE_LAYOUT = new EnumSetting<>("revanced_change_layout", FormFactor.ORIGINAL, true);
public static final BooleanSetting OPEN_CHANNEL_OF_LIVE_AVATAR = new BooleanSetting("revanced_open_channel_of_live_avatar", FALSE, true);
public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", false, true, "revanced_spoof_app_version_user_dialog_message");
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", PatchStatus.SpoofAppVersionDefaultString(), true, parent(SPOOF_APP_VERSION));

View File

@ -29,12 +29,17 @@ import app.revanced.extension.youtube.shared.VideoInformation;
@SuppressWarnings("unused")
public class VideoUtils extends IntentUtils {
private static final String CHANNEL_URL = "https://www.youtube.com/channel/";
private static final String PLAYLIST_URL = "https://www.youtube.com/playlist?list=";
private static final String VIDEO_URL = "https://youtu.be/";
private static final String VIDEO_SCHEME_INTENT_FORMAT = "vnd.youtube://%s?start=%d";
private static final String VIDEO_SCHEME_LINK_FORMAT = "https://youtu.be/%s?t=%d";
private static final AtomicBoolean isExternalDownloaderLaunched = new AtomicBoolean(false);
private static String getChannelUrl(String channelId) {
return CHANNEL_URL + channelId;
}
private static String getPlaylistUrl(String playlistId) {
return PLAYLIST_URL + playlistId;
}
@ -119,6 +124,10 @@ public class VideoUtils extends IntentUtils {
}
}
public static void openChannel(@NonNull String channelId) {
launchView(getChannelUrl(channelId), getContext().getPackageName());
}
public static void openVideo() {
openVideo(VideoInformation.getVideoId());
}