mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-06-12 21:27:43 +02:00
feat(YouTube): Add Open channel of live avatar
patch
This commit is contained in:
@ -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/"
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
Reference in New Issue
Block a user