refactor(InnterTube): Move classes to the appropriate path

This commit is contained in:
inotia00 2025-03-26 12:31:03 +09:00
parent 4bed9f346d
commit 123082b676
18 changed files with 405 additions and 576 deletions

View File

@ -2,8 +2,10 @@ package app.revanced.extension.music.patches.misc.requests
import android.annotation.SuppressLint
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_PLAYLIST_PAGE
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.settings.AppLanguage
import app.revanced.extension.shared.utils.Logger
@ -136,10 +138,11 @@ class PlaylistRequest private constructor(
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_PLAYLIST_PAGE,
val connection = getInnerTubeResponseConnectionFromRoute(
GET_PLAYLIST_PAGE,
clientType
)
/**
* For some reason, the tracks in Top Songs have the playlistId of the album:
* [ReVanced_Extended#2835](https://github.com/inotia00/ReVanced_Extended/issues/2835)
@ -152,7 +155,7 @@ class PlaylistRequest private constructor(
* So we can work around this by setting the language to English when sending the request.
*/
val requestBody =
PlayerRoutes.createApplicationRequestBody(
createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
playlistId = playlistId,

View File

@ -1,4 +1,4 @@
package app.revanced.extension.shared.patches.client
package app.revanced.extension.shared.innertube.client
import android.os.Build
import app.revanced.extension.shared.settings.BaseSettings
@ -11,24 +11,6 @@ import java.util.Locale
*/
object YouTubeAppClient {
// IOS
/**
* Video not playable: Paid / Movie / Private / Age-restricted
* Note: Audio track available
*/
private const val PACKAGE_NAME_IOS = "com.google.ios.youtube"
/**
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
*
* It can be extracted by getting the latest release version of the app on
* [the App Store page of the YouTube app](https://apps.apple.com/us/app/youtube-watch-listen-stream/id544007664/),
* in the `Whats New` section.
*/
private val CLIENT_VERSION_IOS = if (forceAVC())
"17.40.5"
else
"20.10.4"
private const val DEVICE_MAKE_IOS = "Apple"
private const val OS_NAME_IOS = "iOS"
@ -49,7 +31,6 @@ object YouTubeAppClient {
"13_7"
else
"18_3_2"
private val USER_AGENT_IOS = iOSUserAgent(PACKAGE_NAME_IOS, CLIENT_VERSION_IOS)
// IOS UNPLUGGED
@ -202,6 +183,7 @@ object YouTubeAppClient {
* Example: 'com.google.ios.youtube/16.38.2 (iPhone9,4; U; CPU iOS 14_7_1 like Mac OS X; en_AU)'
* Source: https://github.com/mitmproxy/mitmproxy/issues/4836.
*/
@Suppress("SameParameterValue")
private fun iOSUserAgent(
packageName: String,
clientVersion: String
@ -278,10 +260,6 @@ object YouTubeAppClient {
* If true, 'Authorization' must be included.
*/
val requireAuth: Boolean = false,
/**
* Whether a poToken is required to get playback for more than 1 minute.
*/
val requirePoToken: Boolean = false,
/**
* Client name for innertube body.
*/
@ -362,22 +340,6 @@ object YouTubeAppClient {
"iOS TV Force AVC"
else
"iOS TV"
),
IOS(
id = 5,
deviceMake = DEVICE_MAKE_IOS,
deviceModel = DEVICE_MODEL_IOS,
osName = OS_NAME_IOS,
osVersion = OS_VERSION_IOS,
userAgent = USER_AGENT_IOS,
clientVersion = CLIENT_VERSION_IOS,
supportsCookies = false,
requirePoToken = true,
clientName = "IOS",
friendlyName = if (forceAVC())
"iOS Force AVC"
else
"iOS"
);
companion object {

View File

@ -1,10 +1,10 @@
package app.revanced.extension.shared.patches.client;
package app.revanced.extension.shared.innertube.client;
import android.os.Build;
import java.util.Locale;
public class MusicAppClient {
public class YouTubeMusicAppClient {
// Response to the '/next' request is 'Please update to continue using the app':
// https://github.com/inotia00/ReVanced_Extended/issues/2743
@ -46,7 +46,7 @@ public class MusicAppClient {
private static final String DEVICE_MAKE_IOS_MUSIC = "Apple";
private static final String OS_NAME_IOS_MUSIC = "iOS";
private MusicAppClient() {
private YouTubeMusicAppClient() {
}
private static String androidUserAgent(String clientVersion) {

View File

@ -1,14 +1,10 @@
package app.revanced.extension.shared.patches.client
package app.revanced.extension.shared.innertube.client
/**
* Used to fetch video information.
*/
@Suppress("unused")
object YouTubeWebClient {
/**
* This user agent does not require a PoToken in [ClientType.MWEB]
* https://github.com/yt-dlp/yt-dlp/blob/0b6b7742c2e7f2a1fcb0b54ef3dd484bab404b3f/yt_dlp/extractor/youtube.py#L259
*/
private const val USER_AGENT_SAFARI =
"Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)"
@ -26,11 +22,11 @@ object YouTubeWebClient {
* Client version.
*/
@JvmField
val clientVersion: String
val clientVersion: String,
) {
MWEB(
id = 2,
clientVersion = "2.20241202.07.00"
clientVersion = "2.20241202.07.00",
),
WEB_REMIX(
id = 29,

View File

@ -1,9 +1,8 @@
package app.revanced.extension.shared.patches.spoof.requests
package app.revanced.extension.shared.innertube.requests
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.client.YouTubeWebClient
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.client.YouTubeWebClient
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.requests.Route
import app.revanced.extension.shared.requests.Route.CompiledRoute
import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.Logger
@ -21,96 +20,17 @@ import java.util.Locale
import java.util.TimeZone
@Suppress("deprecation")
object PlayerRoutes {
@JvmField
val CREATE_PLAYLIST: CompiledRoute = Route(
Route.Method.POST,
"playlist/create" +
"?prettyPrint=false" +
"&fields=playlistId"
).compile()
@JvmField
val DELETE_PLAYLIST: CompiledRoute = Route(
Route.Method.POST,
"playlist/delete" +
"?prettyPrint=false"
).compile()
@JvmField
val EDIT_PLAYLIST: CompiledRoute = Route(
Route.Method.POST,
"browse/edit_playlist" +
"?prettyPrint=false" +
"&fields=status," +
"playlistEditResults"
).compile()
@JvmField
val GET_PLAYLISTS: CompiledRoute = Route(
Route.Method.POST,
"playlist/get_add_to_playlist" +
"?prettyPrint=false" +
"&fields=contents.addToPlaylistRenderer.playlists.playlistAddToOptionRenderer"
).compile()
@JvmField
val GET_CATEGORY: CompiledRoute = Route(
Route.Method.POST,
"player" +
"?prettyPrint=false" +
"&fields=microformat.playerMicroformatRenderer.category"
).compile()
@JvmField
val GET_SET_VIDEO_ID: CompiledRoute = Route(
Route.Method.POST,
"next" +
"?prettyPrint=false" +
"&fields=contents.singleColumnWatchNextResults." +
"playlist.playlist.contents.playlistPanelVideoRenderer." +
"playlistSetVideoId"
).compile()
@JvmField
val GET_PLAYLIST_PAGE: CompiledRoute = Route(
Route.Method.POST,
"next" +
"?prettyPrint=false" +
"&fields=contents.singleColumnWatchNextResults.playlist.playlist"
).compile()
@JvmField
val GET_STREAMING_DATA: CompiledRoute = Route(
Route.Method.POST,
"player" +
"?fields=streamingData" +
"&alt=proto"
).compile()
@JvmField
val GET_VIDEO_ACTION_BUTTON: CompiledRoute = Route(
Route.Method.POST,
"next" +
"?prettyPrint=false" +
"&fields=contents.singleColumnWatchNextResults." +
"results.results.contents.slimVideoMetadataSectionRenderer." +
"contents.elementRenderer.newElement.type.componentType." +
"model.videoActionBarModel.buttons.buttonViewModel"
).compile()
@JvmField
val GET_VIDEO_DETAILS: CompiledRoute = Route(
Route.Method.POST,
"player" +
"?prettyPrint=false" +
"&fields=videoDetails.channelId," +
"videoDetails.isLiveContent," +
"videoDetails.isUpcoming"
).compile()
object InnerTubeRequestBody {
private const val YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"
private const val AUTHORIZATION_HEADER = "Authorization"
private val REQUEST_HEADER_KEYS = setOf(
AUTHORIZATION_HEADER, // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
/**
* TCP connection and HTTP read timeout
*/
@ -230,16 +150,10 @@ object PlayerRoutes {
client.put("osName", clientType.osName)
client.put("osVersion", clientType.osVersion)
client.put("androidSdkVersion", clientType.androidSdkVersion)
if (clientType.gmscoreVersionCode != null) {
client.put("gmscoreVersionCode", clientType.gmscoreVersionCode)
}
client.put(
"hl",
LOCALE_LANGUAGE
)
client.put("hl", LOCALE_LANGUAGE)
client.put("gl", LOCALE_COUNTRY)
client.put("timeZone", TIME_ZONE_ID)
client.put("utcOffsetMinutes", "$UTC_OFFSET_MINUTES")
client.put("utcOffsetMinutes", UTC_OFFSET_MINUTES.toString())
val context = JSONObject()
context.put("client", client)
@ -269,7 +183,7 @@ object PlayerRoutes {
videoIds.put(0, videoId)
innerTubeBody.put("videoIds", videoIds)
} catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e)
Logger.printException({ "Failed to create create/playlist innerTubeBody" }, e)
}
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -284,7 +198,7 @@ object PlayerRoutes {
try {
innerTubeBody.put("playlistId", playlistId)
} catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e)
Logger.printException({ "Failed to create delete/playlist innerTubeBody" }, e)
}
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -314,7 +228,7 @@ object PlayerRoutes {
actionsArray.put(0, actionsObject)
innerTubeBody.put("actions", actionsArray)
} catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e)
Logger.printException({ "Failed to create edit/playlist innerTubeBody" }, e)
}
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -330,7 +244,7 @@ object PlayerRoutes {
innerTubeBody.put("playlistId", playlistId)
innerTubeBody.put("excludeWatchLater", false)
} catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e)
Logger.printException({ "Failed to create get/playlists innerTubeBody" }, e)
}
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -354,44 +268,57 @@ object PlayerRoutes {
actionsArray.put(0, actionsObject)
innerTubeBody.put("actions", actionsArray)
} catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e)
Logger.printException({ "Failed to create save/playlist innerTubeBody" }, e)
}
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
}
@JvmStatic
fun getPlayerResponseConnectionFromRoute(
fun getInnerTubeResponseConnectionFromRoute(
route: CompiledRoute,
clientType: YouTubeAppClient.ClientType
): HttpURLConnection {
return getPlayerResponseConnectionFromRoute(
route,
clientType.userAgent,
clientType.id.toString(),
clientType.clientVersion
)
}
clientType: YouTubeAppClient.ClientType,
requestHeader: Map<String, String>? = null,
connectTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
readTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
) = getInnerTubeResponseConnectionFromRoute(
route = route,
userAgent = clientType.userAgent,
clientId = clientType.id.toString(),
clientVersion = clientType.clientVersion,
supportsCookies = clientType.supportsCookies,
requestHeader = requestHeader,
connectTimeout = connectTimeout,
readTimeout = readTimeout,
)
@JvmStatic
fun getPlayerResponseConnectionFromRoute(
fun getInnerTubeResponseConnectionFromRoute(
route: CompiledRoute,
clientType: YouTubeWebClient.ClientType
): HttpURLConnection {
return getPlayerResponseConnectionFromRoute(
route,
clientType.userAgent,
clientType.id.toString(),
clientType.clientVersion,
)
}
clientType: YouTubeWebClient.ClientType,
requestHeader: Map<String, String>? = null,
connectTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
readTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
) = getInnerTubeResponseConnectionFromRoute(
route = route,
userAgent = clientType.userAgent,
clientId = clientType.id.toString(),
clientVersion = clientType.clientVersion,
requestHeader = requestHeader,
connectTimeout = connectTimeout,
readTimeout = readTimeout,
)
@Throws(IOException::class)
fun getPlayerResponseConnectionFromRoute(
fun getInnerTubeResponseConnectionFromRoute(
route: CompiledRoute,
userAgent: String,
clientId: String,
clientVersion: String
clientVersion: String,
supportsCookies: Boolean = true,
requestHeader: Map<String, String>? = null,
connectTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
readTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
): HttpURLConnection {
val connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route)
@ -403,8 +330,24 @@ object PlayerRoutes {
connection.useCaches = false
connection.doOutput = true
connection.connectTimeout = CONNECTION_TIMEOUT_MILLISECONDS
connection.readTimeout = CONNECTION_TIMEOUT_MILLISECONDS
connection.connectTimeout = connectTimeout
connection.readTimeout = readTimeout
if (requestHeader != null) {
for (key in REQUEST_HEADER_KEYS) {
var value = requestHeader[key]
if (value != null) {
if (key == AUTHORIZATION_HEADER) {
if (!supportsCookies) {
continue
}
}
connection.setRequestProperty(key, value)
}
}
}
return connection
}

View File

@ -0,0 +1,108 @@
package app.revanced.extension.shared.innertube.requests
import app.revanced.extension.shared.requests.Route
import app.revanced.extension.shared.requests.Route.CompiledRoute
object InnerTubeRoutes {
@JvmField
val CREATE_PLAYLIST = compileRoute(
endpoint = "playlist/create",
fields = "playlistId",
)
@JvmField
val DELETE_PLAYLIST = compileRoute(
endpoint = "playlist/delete",
)
@JvmField
val EDIT_PLAYLIST = compileRoute(
endpoint = "browse/edit_playlist",
fields = "status," + "playlistEditResults",
)
@JvmField
val GET_CATEGORY = compileRoute(
endpoint = "player",
fields = "microformat.playerMicroformatRenderer.category",
)
@JvmField
val GET_PLAYLISTS = compileRoute(
endpoint = "playlist/get_add_to_playlist",
fields = "contents.addToPlaylistRenderer.playlists.playlistAddToOptionRenderer",
)
@JvmField
val GET_SET_VIDEO_ID = compileRoute(
endpoint = "next",
fields = "contents.singleColumnWatchNextResults." +
"playlist.playlist.contents.playlistPanelVideoRenderer." +
"playlistSetVideoId",
)
@JvmField
val GET_PLAYLIST_PAGE = compileRoute(
endpoint = "next",
fields = "contents.singleColumnWatchNextResults.playlist.playlist",
)
@JvmField
val GET_STREAMING_DATA = compileRoute(
endpoint = "player",
fields = "streamingData",
alt = "proto",
prettier = true,
)
@JvmField
val GET_VIDEO_ACTION_BUTTON = compileRoute(
endpoint = "next",
fields = "contents.singleColumnWatchNextResults." +
"results.results.contents.slimVideoMetadataSectionRenderer." +
"contents.elementRenderer.newElement.type.componentType." +
"model.videoActionBarModel.buttons.buttonViewModel"
)
@JvmField
val GET_VIDEO_DETAILS = compileRoute(
endpoint = "player",
fields = "videoDetails.channelId," +
"videoDetails.isLiveContent," +
"videoDetails.isUpcoming"
)
private fun compileRoute(
endpoint: String,
fields: String? = null,
alt: String? = null,
prettier: Boolean = false,
): CompiledRoute {
var query = Array<String>(4) { "&" }
var i = 0
query[i] = "?"
val sb = StringBuilder(endpoint)
if (prettier == false) {
sb.append(query[i++])
sb.append("prettyPrint=false")
}
if (fields != null) {
sb.append(query[i++])
sb.append("fields=")
sb.append(fields)
}
if (alt != null) {
sb.append(query[i++])
sb.append("alt=")
sb.append(alt)
}
return Route(
Route.Method.POST,
sb.toString()
).compile()
}
}

View File

@ -1,11 +1,11 @@
package app.revanced.extension.shared.patches.spoof;
import app.revanced.extension.shared.patches.client.MusicAppClient.ClientType;
import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.shared.innertube.client.YouTubeMusicAppClient.ClientType;
import app.revanced.extension.shared.settings.BaseSettings;
@SuppressWarnings("unused")
public class SpoofClientPatch extends BlockRequestPatch {
private static final ClientType CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get();
private static final ClientType CLIENT_TYPE = BaseSettings.SPOOF_CLIENT_TYPE.get();
/**
* Injection point.

View File

@ -10,7 +10,7 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import app.revanced.extension.shared.patches.client.YouTubeAppClient.ClientType;
import app.revanced.extension.shared.innertube.client.YouTubeAppClient.ClientType;
import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.Setting;
@ -19,10 +19,6 @@ import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class SpoofStreamingDataPatch extends BlockRequestPatch {
private static final String PO_TOKEN =
BaseSettings.SPOOF_STREAMING_DATA_PO_TOKEN.get();
private static final String VISITOR_DATA =
BaseSettings.SPOOF_STREAMING_DATA_VISITOR_DATA.get();
private static final boolean SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION =
SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION.get();
@ -79,7 +75,7 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
/**
* Injection point.
*/
public static void fetchStreams(String url, Map<String, String> requestHeaders) {
public static void fetchStreams(String url, Map<String, String> requestHeader) {
if (SPOOF_STREAMING_DATA) {
String id = Utils.getVideoIdFromRequest(url);
if (id == null) {
@ -89,7 +85,7 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
return;
}
StreamingDataRequest.fetchRequest(id, requestHeaders, VISITOR_DATA, PO_TOKEN);
StreamingDataRequest.fetchRequest(id, requestHeader);
}
}

View File

@ -1,14 +1,13 @@
package app.revanced.extension.shared.patches.spoof.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.createApplicationRequestBody
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.getPlayerResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_STREAMING_DATA
import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
import org.apache.commons.lang3.StringUtils
import java.io.BufferedInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
@ -32,21 +31,19 @@ import java.util.concurrent.TimeoutException
* did use its own client streams.
*/
class StreamingDataRequest private constructor(
videoId: String, playerHeaders: Map<String, String>,
visitorId: String, botGuardPoToken: String
videoId: String,
requestHeader: Map<String, String>,
) {
private val videoId: String
private val future: Future<ByteBuffer?>
init {
Objects.requireNonNull(playerHeaders)
Objects.requireNonNull(requestHeader)
this.videoId = videoId
this.future = Utils.submitOnBackgroundThread {
fetch(
videoId,
playerHeaders,
visitorId,
botGuardPoToken
requestHeader,
)
}
}
@ -86,33 +83,16 @@ class StreamingDataRequest private constructor(
companion object {
private const val AUTHORIZATION_HEADER = "Authorization"
private const val VISITOR_ID_HEADER = "X-Goog-Visitor-Id"
private val REQUEST_HEADER_KEYS = arrayOf(
AUTHORIZATION_HEADER, // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
VISITOR_ID_HEADER
)
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType =
BaseSettings.SPOOF_STREAMING_DATA_TYPE.get()
private val CLIENT_ORDER_TO_USE: Array<YouTubeAppClient.ClientType> =
YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE)
private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean =
SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH
private var lastSpoofedClientType: YouTubeAppClient.ClientType? = null
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
private var lastSpoofedClientFriendlyName: String? = null
@GuardedBy("itself")
val cache: MutableMap<String, StreamingDataRequest> = Collections.synchronizedMap(
@ -126,22 +106,24 @@ class StreamingDataRequest private constructor(
@JvmStatic
val lastSpoofedClientName: String
get() = lastSpoofedClientType
?.friendlyName
?: "Unknown"
get() {
return if (lastSpoofedClientFriendlyName != null) {
lastSpoofedClientFriendlyName!!
} else {
"Unknown"
}
}
@JvmStatic
fun fetchRequest(
videoId: String, fetchHeaders: Map<String, String>,
visitorId: String, botGuardPoToken: String
videoId: String,
fetchHeaders: Map<String, String>,
) {
// Always fetch, even if there is an existing request for the same video.
cache[videoId] =
StreamingDataRequest(
videoId,
fetchHeaders,
visitorId,
botGuardPoToken
fetchHeaders
)
}
@ -157,64 +139,28 @@ class StreamingDataRequest private constructor(
private fun send(
clientType: YouTubeAppClient.ClientType,
videoId: String,
playerHeaders: Map<String, String>,
visitorId: String,
botGuardPoToken: String
requestHeader: Map<String, String>,
): HttpURLConnection? {
Objects.requireNonNull(clientType)
Objects.requireNonNull(videoId)
Objects.requireNonNull(playerHeaders)
Objects.requireNonNull(requestHeader)
val startTime = System.currentTimeMillis()
Logger.printDebug { "Fetching video streams for: $videoId using client: $clientType" }
try {
val connection =
getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
val usePoToken =
clientType.requirePoToken && !StringUtils.isAnyEmpty(botGuardPoToken, visitorId)
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
if (key == AUTHORIZATION_HEADER) {
if (!clientType.supportsCookies) {
Logger.printDebug { "Not including request header: $key" }
continue
}
}
if (key == VISITOR_ID_HEADER && usePoToken) {
val originalVisitorId: String = value
Logger.printDebug { "Original visitor id:\n$originalVisitorId" }
Logger.printDebug { "Replaced visitor id:\n$visitorId" }
value = visitorId
}
connection.setRequestProperty(key, value)
}
}
val requestBody: ByteArray
if (usePoToken) {
requestBody = createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
botGuardPoToken = botGuardPoToken,
visitorId = visitorId,
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
getInnerTubeResponseConnectionFromRoute(
GET_STREAMING_DATA,
clientType,
requestHeader
)
Logger.printDebug { "Set poToken (botGuardPoToken):\n$botGuardPoToken" }
} else {
requestBody =
createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
)
}
val requestBody = createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -243,15 +189,15 @@ class StreamingDataRequest private constructor(
}
private fun fetch(
videoId: String, playerHeaders: Map<String, String>,
visitorId: String, botGuardPoToken: String
videoId: String,
requestHeader: Map<String, String>,
): ByteBuffer? {
lastSpoofedClientType = null
lastSpoofedClientFriendlyName = null
// Retry with different client if empty response body is received.
for (clientType in CLIENT_ORDER_TO_USE) {
if (clientType.requireAuth &&
playerHeaders[AUTHORIZATION_HEADER] == null
requestHeader[AUTHORIZATION_HEADER] == null
) {
Logger.printDebug { "Skipped login-required client (incognito mode or not logged in)\nClient: $clientType\nVideo: $videoId" }
continue
@ -259,9 +205,7 @@ class StreamingDataRequest private constructor(
send(
clientType,
videoId,
playerHeaders,
visitorId,
botGuardPoToken
requestHeader,
)?.let { connection ->
try {
// gzip encoding doesn't response with content length (-1),
@ -271,14 +215,14 @@ class StreamingDataRequest private constructor(
} else {
BufferedInputStream(connection.inputStream).use { inputStream ->
ByteArrayOutputStream().use { stream ->
val buffer = ByteArray(2048)
val buffer = ByteArray(4096)
var bytesRead: Int
while ((inputStream.read(buffer)
.also { bytesRead = it }) >= 0
) {
stream.write(buffer, 0, bytesRead)
}
lastSpoofedClientType = clientType
lastSpoofedClientFriendlyName = clientType.friendlyName
return ByteBuffer.wrap(stream.toByteArray())
}
}

View File

@ -3,10 +3,10 @@ package app.revanced.extension.shared.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import app.revanced.extension.shared.innertube.client.YouTubeAppClient;
import app.revanced.extension.shared.innertube.client.YouTubeMusicAppClient;
import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat;
import app.revanced.extension.shared.patches.WatchHistoryPatch.WatchHistoryType;
import app.revanced.extension.shared.patches.client.MusicAppClient;
import app.revanced.extension.shared.patches.client.YouTubeAppClient;
import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch.AudioStreamLanguageOverrideAvailability;
/**
@ -31,7 +31,7 @@ public class BaseSettings {
* Some patches are in a shared path, so they are declared here.
*/
public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true);
public static final EnumSetting<MusicAppClient.ClientType> SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", MusicAppClient.ClientType.IOS_MUSIC_6_21, true);
public static final EnumSetting<YouTubeMusicAppClient.ClientType> SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", YouTubeMusicAppClient.ClientType.IOS_MUSIC_6_21, true);
/**
* These settings are used by YouTube.
@ -44,10 +44,7 @@ public class BaseSettings {
public static final BooleanSetting SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION = new BooleanSetting("revanced_spoof_streaming_data_skip_response_encryption", TRUE, true);
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
// Client type must be last spoof setting due to cyclic references.
public static final EnumSetting<YouTubeAppClient.ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_UNPLUGGED, true);
public static final StringSetting SPOOF_STREAMING_DATA_PO_TOKEN = new StringSetting("revanced_spoof_streaming_data_po_token", "", true);
public static final StringSetting SPOOF_STREAMING_DATA_VISITOR_DATA = new StringSetting("revanced_spoof_streaming_data_visitor_data", "", true);
public static final EnumSetting<YouTubeAppClient.ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_VR, true);
/**
* These settings are used by YouTube and YouTube Music.

View File

@ -2,8 +2,10 @@ package app.revanced.extension.youtube.patches.general.requests
import android.annotation.SuppressLint
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeWebClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeWebClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createWebInnertubeBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_VIDEO_DETAILS
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
@ -86,12 +88,11 @@ class VideoDetailsRequest private constructor(
Logger.printDebug { "Fetching video details request for: $videoId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_VIDEO_DETAILS,
val connection = getInnerTubeResponseConnectionFromRoute(
GET_VIDEO_DETAILS,
clientType
)
val requestBody =
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
val requestBody = createWebInnertubeBody(clientType, videoId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)

View File

@ -1,8 +1,10 @@
package app.revanced.extension.youtube.patches.player.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_VIDEO_ACTION_BUTTON
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
@ -20,10 +22,10 @@ import java.util.concurrent.TimeoutException
class ActionButtonRequest private constructor(
private val videoId: String,
private val playerHeaders: Map<String, String>,
private val requestHeader: Map<String, String>,
) {
private val future: Future<Array<ActionButton>> = Utils.submitOnBackgroundThread {
fetch(videoId, playerHeaders)
fetch(videoId, requestHeader)
}
val array: Array<ActionButton>
@ -52,14 +54,6 @@ class ActionButtonRequest private constructor(
}
companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself")
@ -73,11 +67,11 @@ class ActionButtonRequest private constructor(
})
@JvmStatic
fun fetchRequestIfNeeded(videoId: String, playerHeaders: Map<String, String>) {
fun fetchRequestIfNeeded(videoId: String, requestHeader: Map<String, String>) {
Objects.requireNonNull(videoId)
synchronized(cache) {
if (!cache.containsKey(videoId)) {
cache[videoId] = ActionButtonRequest(videoId, playerHeaders)
cache[videoId] = ActionButtonRequest(videoId, requestHeader)
}
}
}
@ -93,43 +87,28 @@ class ActionButtonRequest private constructor(
Logger.printInfo({ toastMessage }, ex)
}
private val REQUEST_HEADER_KEYS = arrayOf(
"Authorization", // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
private fun sendRequest(videoId: String, playerHeaders: Map<String, String>): JSONObject? {
private fun sendRequest(videoId: String, requestHeader: Map<String, String>): JSONObject? {
Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis()
// '/next' request does not require PoToken.
// '/next' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_VIDEO_ACTION_BUTTON,
clientType
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
// Since [THANKS] button and [CLIP] button are shown only with the logged in,
// Set the [Authorization] field to property to get the correct action buttons.
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val connection = getInnerTubeResponseConnectionFromRoute(
GET_VIDEO_ACTION_BUTTON,
clientType,
requestHeader,
)
val requestBody =
PlayerRoutes.createApplicationRequestBody(
clientType = clientType,
videoId = videoId
)
val requestBody = createApplicationRequestBody(
clientType = clientType,
videoId = videoId
)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -214,8 +193,11 @@ class ActionButtonRequest private constructor(
return emptyArray()
}
private fun fetch(videoId: String, playerHeaders: Map<String, String>): Array<ActionButton> {
val json = sendRequest(videoId, playerHeaders)
private fun fetch(
videoId: String,
requestHeader: Map<String, String>
): Array<ActionButton> {
val json = sendRequest(videoId, requestHeader)
if (json != null) {
return parseResponse(json)
}

View File

@ -1,12 +1,15 @@
package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createPlaylistRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.CREATE_PLAYLIST
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_SET_VIDEO_ID
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
import app.revanced.extension.youtube.patches.utils.requests.CreatePlaylistRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
@ -20,10 +23,10 @@ import java.util.concurrent.TimeoutException
class CreatePlaylistRequest private constructor(
private val videoId: String,
private val playerHeaders: Map<String, String>,
private val requestHeader: Map<String, String>,
) {
private val future: Future<Pair<String, String>> = Utils.submitOnBackgroundThread {
fetch(videoId, playerHeaders)
fetch(videoId, requestHeader)
}
val playlistId: Pair<String, String>?
@ -52,14 +55,6 @@ class CreatePlaylistRequest private constructor(
}
companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself")
@ -80,11 +75,11 @@ class CreatePlaylistRequest private constructor(
}
@JvmStatic
fun fetchRequestIfNeeded(videoId: String, playerHeaders: Map<String, String>) {
fun fetchRequestIfNeeded(videoId: String, requestHeader: Map<String, String>) {
Objects.requireNonNull(videoId)
synchronized(cache) {
if (!cache.containsKey(videoId)) {
cache[videoId] = CreatePlaylistRequest(videoId, playerHeaders)
cache[videoId] = CreatePlaylistRequest(videoId, requestHeader)
}
}
}
@ -100,40 +95,26 @@ class CreatePlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex)
}
private val REQUEST_HEADER_KEYS = arrayOf(
"Authorization", // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
private fun sendCreatePlaylistRequest(videoId: String, playerHeaders: Map<String, String>): JSONObject? {
private fun sendCreatePlaylistRequest(
videoId: String,
requestHeader: Map<String, String>
): JSONObject? {
Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis()
// 'playlist/create' request does not require PoToken.
// 'playlist/create' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name
Logger.printDebug { "Fetching create playlist request for: $videoId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.CREATE_PLAYLIST,
clientType
val connection = getInnerTubeResponseConnectionFromRoute(
CREATE_PLAYLIST,
clientType,
requestHeader,
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody =
PlayerRoutes.createPlaylistRequestBody(
videoId = videoId
)
val requestBody = createPlaylistRequestBody(videoId = videoId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -159,36 +140,31 @@ class CreatePlaylistRequest private constructor(
return null
}
private fun sendSetVideoIdRequest(videoId: String, playlistId: String, playerHeaders: Map<String, String>): JSONObject? {
private fun sendSetVideoIdRequest(
videoId: String,
playlistId: String,
requestHeader: Map<String, String>
): JSONObject? {
Objects.requireNonNull(playlistId)
val startTime = System.currentTimeMillis()
// 'playlist/create' request does not require PoToken.
// 'playlist/create' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name
Logger.printDebug { "Fetching set video id request for: $playlistId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_SET_VIDEO_ID,
clientType
val connection = getInnerTubeResponseConnectionFromRoute(
GET_SET_VIDEO_ID,
clientType,
requestHeader
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody =
PlayerRoutes.createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
playlistId = playlistId
)
val requestBody = createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
playlistId = playlistId
)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -240,8 +216,8 @@ class CreatePlaylistRequest private constructor(
if (secondaryContentsJsonObject is JSONObject) {
return secondaryContentsJsonObject
.getJSONObject("playlistPanelVideoRenderer")
.getString("playlistSetVideoId")
.getJSONObject("playlistPanelVideoRenderer")
.getString("playlistSetVideoId")
}
} catch (e: JSONException) {
val jsonForMessage = json.toString()
@ -254,12 +230,15 @@ class CreatePlaylistRequest private constructor(
return null
}
private fun fetch(videoId: String, playerHeaders: Map<String, String>): Pair<String, String>? {
val createPlaylistJson = sendCreatePlaylistRequest(videoId, playerHeaders)
private fun fetch(
videoId: String,
requestHeader: Map<String, String>
): Pair<String, String>? {
val createPlaylistJson = sendCreatePlaylistRequest(videoId, requestHeader)
if (createPlaylistJson != null) {
val playlistId = parseCreatePlaylistResponse(createPlaylistJson)
if (playlistId != null) {
val setVideoIdJson = sendSetVideoIdRequest(videoId, playlistId, playerHeaders)
val setVideoIdJson = sendSetVideoIdRequest(videoId, playlistId, requestHeader)
if (setVideoIdJson != null) {
val setVideoId = parseSetVideoIdResponse(setVideoIdJson)
if (setVideoId != null) {

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.deletePlaylistRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.DELETE_PLAYLIST
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
import app.revanced.extension.youtube.patches.utils.requests.DeletePlaylistRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
@ -20,12 +21,12 @@ import java.util.concurrent.TimeoutException
class DeletePlaylistRequest private constructor(
private val playlistId: String,
private val playerHeaders: Map<String, String>,
private val requestHeader: Map<String, String>,
) {
private val future: Future<Boolean> = Utils.submitOnBackgroundThread {
fetch(
playlistId,
playerHeaders,
requestHeader,
)
}
@ -55,14 +56,6 @@ class DeletePlaylistRequest private constructor(
}
companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself")
@ -85,14 +78,14 @@ class DeletePlaylistRequest private constructor(
@JvmStatic
fun fetchRequestIfNeeded(
playlistId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>
) {
Objects.requireNonNull(playlistId)
synchronized(cache) {
if (!cache.containsKey(playlistId)) {
cache[playlistId] = DeletePlaylistRequest(
playlistId,
playerHeaders
requestHeader
)
}
}
@ -109,40 +102,26 @@ class DeletePlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex)
}
private val REQUEST_HEADER_KEYS = arrayOf(
"Authorization", // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
private fun sendRequest(
playlistId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>
): JSONObject? {
Objects.requireNonNull(playlistId)
val startTime = System.currentTimeMillis()
// 'playlist/delete' request does not require PoToken.
// 'playlist/delete' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name
Logger.printDebug { "Fetching delete playlist request, playlistId: $playlistId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.DELETE_PLAYLIST,
val connection = getInnerTubeResponseConnectionFromRoute(
DELETE_PLAYLIST,
clientType,
requestHeader
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody = PlayerRoutes.deletePlaylistRequestBody(playlistId)
val requestBody = deletePlaylistRequestBody(playlistId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -184,9 +163,9 @@ class DeletePlaylistRequest private constructor(
private fun fetch(
playlistId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>
): Boolean? {
val json = sendRequest(playlistId, playerHeaders)
val json = sendRequest(playlistId, requestHeader)
if (json != null) {
return parseResponse(json)
}

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.editPlaylistRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.EDIT_PLAYLIST
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
import app.revanced.extension.youtube.patches.utils.requests.EditPlaylistRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.apache.commons.lang3.StringUtils
import org.json.JSONException
import org.json.JSONObject
@ -23,14 +24,14 @@ class EditPlaylistRequest private constructor(
private val videoId: String,
private val playlistId: String,
private val setVideoId: String?,
private val playerHeaders: Map<String, String>,
private val requestHeader: Map<String, String>,
) {
private val future: Future<String> = Utils.submitOnBackgroundThread {
fetch(
videoId,
playlistId,
setVideoId,
playerHeaders,
requestHeader,
)
}
@ -60,14 +61,6 @@ class EditPlaylistRequest private constructor(
}
companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself")
@ -99,7 +92,7 @@ class EditPlaylistRequest private constructor(
videoId: String,
playlistId: String,
setVideoId: String?,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>
) {
Objects.requireNonNull(videoId)
synchronized(cache) {
@ -108,7 +101,7 @@ class EditPlaylistRequest private constructor(
videoId,
playlistId,
setVideoId,
playerHeaders
requestHeader
)
}
}
@ -125,47 +118,32 @@ class EditPlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex)
}
private val REQUEST_HEADER_KEYS = arrayOf(
"Authorization", // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
private fun sendRequest(
videoId: String,
playlistId: String,
setVideoId: String?,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>
): JSONObject? {
Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis()
// 'browse/edit_playlist' request does not require PoToken.
// 'browse/edit_playlist' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name
Logger.printDebug { "Fetching edit playlist request, videoId: $videoId, playlistId: $playlistId, setVideoId: $setVideoId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.EDIT_PLAYLIST,
clientType
val connection = getInnerTubeResponseConnectionFromRoute(
EDIT_PLAYLIST,
clientType,
requestHeader
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody =
PlayerRoutes.editPlaylistRequestBody(
videoId = videoId,
playlistId = playlistId,
setVideoId = setVideoId,
)
val requestBody = editPlaylistRequestBody(
videoId = videoId,
playlistId = playlistId,
setVideoId = setVideoId
)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -197,7 +175,8 @@ class EditPlaylistRequest private constructor(
if (remove) {
return ""
}
val playlistEditResultsJSONObject = json.getJSONArray("playlistEditResults").get(0)
val playlistEditResultsJSONObject =
json.getJSONArray("playlistEditResults").get(0)
if (playlistEditResultsJSONObject is JSONObject) {
return playlistEditResultsJSONObject
@ -220,9 +199,9 @@ class EditPlaylistRequest private constructor(
videoId: String,
playlistId: String,
setVideoId: String?,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>
): String? {
val json = sendRequest(videoId, playlistId, setVideoId, playerHeaders)
val json = sendRequest(videoId, playlistId, setVideoId, requestHeader)
if (json != null) {
return parseResponse(json, StringUtils.isNotEmpty(setVideoId))
}

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getPlaylistsRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_PLAYLISTS
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
import app.revanced.extension.youtube.patches.utils.requests.GetPlaylistsRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
@ -20,12 +21,12 @@ import java.util.concurrent.TimeoutException
class GetPlaylistsRequest private constructor(
private val playlistId: String,
private val playerHeaders: Map<String, String>,
private val requestHeader: Map<String, String>,
) {
private val future: Future<Array<Pair<String, String>>> = Utils.submitOnBackgroundThread {
fetch(
playlistId,
playerHeaders,
requestHeader,
)
}
@ -55,14 +56,6 @@ class GetPlaylistsRequest private constructor(
}
companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself")
@ -85,14 +78,14 @@ class GetPlaylistsRequest private constructor(
@JvmStatic
fun fetchRequestIfNeeded(
playlistId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>
) {
Objects.requireNonNull(playlistId)
synchronized(cache) {
if (!cache.containsKey(playlistId)) {
cache[playlistId] = GetPlaylistsRequest(
playlistId,
playerHeaders
requestHeader
)
}
}
@ -109,40 +102,26 @@ class GetPlaylistsRequest private constructor(
Logger.printInfo({ toastMessage }, ex)
}
private val REQUEST_HEADER_KEYS = arrayOf(
"Authorization", // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
private fun sendRequest(
playlistId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>
): JSONObject? {
Objects.requireNonNull(playlistId)
val startTime = System.currentTimeMillis()
// 'playlist/get_add_to_playlist' request does not require PoToken.
// 'playlist/get_add_to_playlist' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name
Logger.printDebug { "Fetching get playlists request, playlistId: $playlistId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_PLAYLISTS,
clientType
val connection = getInnerTubeResponseConnectionFromRoute(
GET_PLAYLISTS,
clientType,
requestHeader
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody = PlayerRoutes.getPlaylistsRequestBody(playlistId)
val requestBody = getPlaylistsRequestBody(playlistId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -223,9 +202,9 @@ class GetPlaylistsRequest private constructor(
private fun fetch(
playlistId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>
): Array<Pair<String, String>>? {
val json = sendRequest(playlistId, playerHeaders)
val json = sendRequest(playlistId, requestHeader)
if (json != null) {
return parseResponse(json)
}

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.savePlaylistRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.EDIT_PLAYLIST
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
import app.revanced.extension.youtube.patches.utils.requests.SavePlaylistRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
@ -21,13 +22,13 @@ import java.util.concurrent.TimeoutException
class SavePlaylistRequest private constructor(
private val playlistId: String,
private val libraryId: String,
private val playerHeaders: Map<String, String>,
private val requestHeader: Map<String, String>,
) {
private val future: Future<Boolean> = Utils.submitOnBackgroundThread {
fetch(
playlistId,
libraryId,
playerHeaders,
requestHeader,
)
}
@ -57,14 +58,6 @@ class SavePlaylistRequest private constructor(
}
companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself")
@ -88,14 +81,14 @@ class SavePlaylistRequest private constructor(
fun fetchRequestIfNeeded(
playlistId: String,
libraryId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>
) {
Objects.requireNonNull(playlistId)
synchronized(cache) {
cache[libraryId] = SavePlaylistRequest(
playlistId,
libraryId,
playerHeaders
requestHeader
)
}
}
@ -111,43 +104,28 @@ class SavePlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex)
}
private val REQUEST_HEADER_KEYS = arrayOf(
"Authorization", // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
private fun sendRequest(
playlistId: String,
libraryId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>
): JSONObject? {
Objects.requireNonNull(playlistId)
Objects.requireNonNull(libraryId)
val startTime = System.currentTimeMillis()
// 'browse/edit_playlist' request does not require PoToken.
// 'browse/edit_playlist' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name
Logger.printDebug { "Fetching edit playlist request, playlistId: $playlistId, libraryId: $libraryId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.EDIT_PLAYLIST,
clientType
val connection = getInnerTubeResponseConnectionFromRoute(
EDIT_PLAYLIST,
clientType,
requestHeader
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody =
PlayerRoutes.savePlaylistRequestBody(libraryId, playlistId)
val requestBody = savePlaylistRequestBody(libraryId, playlistId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)
@ -190,9 +168,9 @@ class SavePlaylistRequest private constructor(
private fun fetch(
playlistId: String,
libraryId: String,
playerHeaders: Map<String, String>
requestHeader: Map<String, String>
): Boolean? {
val json = sendRequest(playlistId, libraryId,playerHeaders)
val json = sendRequest(playlistId, libraryId, requestHeader)
if (json != null) {
return parseResponse(json)
}

View File

@ -2,9 +2,13 @@ package app.revanced.extension.youtube.patches.video.requests
import android.annotation.SuppressLint
import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient
import app.revanced.extension.shared.patches.client.YouTubeWebClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.innertube.client.YouTubeWebClient
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createWebInnertubeBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_CATEGORY
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_PLAYLIST_PAGE
import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils
@ -124,12 +128,12 @@ class MusicRequest private constructor(
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_PLAYLIST_PAGE,
val connection = getInnerTubeResponseConnectionFromRoute(
GET_PLAYLIST_PAGE,
clientType
)
val requestBody =
PlayerRoutes.createApplicationRequestBody(
createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
playlistId = "RD$videoId"
@ -168,12 +172,11 @@ class MusicRequest private constructor(
Logger.printDebug { "Fetching microformat request for: $videoId, using client: $clientTypeName" }
try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_CATEGORY,
val connection = getInnerTubeResponseConnectionFromRoute(
GET_CATEGORY,
clientType
)
val requestBody =
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
val requestBody = createWebInnertubeBody(clientType, videoId)
connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody)