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 android.annotation.SuppressLint
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes 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.requests.Requester
import app.revanced.extension.shared.settings.AppLanguage import app.revanced.extension.shared.settings.AppLanguage
import app.revanced.extension.shared.utils.Logger 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" } Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.GET_PLAYLIST_PAGE, GET_PLAYLIST_PAGE,
clientType clientType
) )
/** /**
* For some reason, the tracks in Top Songs have the playlistId of the album: * 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) * [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. * So we can work around this by setting the language to English when sending the request.
*/ */
val requestBody = val requestBody =
PlayerRoutes.createApplicationRequestBody( createApplicationRequestBody(
clientType = clientType, clientType = clientType,
videoId = videoId, videoId = videoId,
playlistId = playlistId, 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 android.os.Build
import app.revanced.extension.shared.settings.BaseSettings import app.revanced.extension.shared.settings.BaseSettings
@ -11,24 +11,6 @@ import java.util.Locale
*/ */
object YouTubeAppClient { object YouTubeAppClient {
// IOS // 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 DEVICE_MAKE_IOS = "Apple"
private const val OS_NAME_IOS = "iOS" private const val OS_NAME_IOS = "iOS"
@ -49,7 +31,6 @@ object YouTubeAppClient {
"13_7" "13_7"
else else
"18_3_2" "18_3_2"
private val USER_AGENT_IOS = iOSUserAgent(PACKAGE_NAME_IOS, CLIENT_VERSION_IOS)
// IOS UNPLUGGED // 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)' * 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. * Source: https://github.com/mitmproxy/mitmproxy/issues/4836.
*/ */
@Suppress("SameParameterValue")
private fun iOSUserAgent( private fun iOSUserAgent(
packageName: String, packageName: String,
clientVersion: String clientVersion: String
@ -278,10 +260,6 @@ object YouTubeAppClient {
* If true, 'Authorization' must be included. * If true, 'Authorization' must be included.
*/ */
val requireAuth: Boolean = false, 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. * Client name for innertube body.
*/ */
@ -362,22 +340,6 @@ object YouTubeAppClient {
"iOS TV Force AVC" "iOS TV Force AVC"
else else
"iOS TV" "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 { 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 android.os.Build;
import java.util.Locale; import java.util.Locale;
public class MusicAppClient { public class YouTubeMusicAppClient {
// Response to the '/next' request is 'Please update to continue using the app': // Response to the '/next' request is 'Please update to continue using the app':
// https://github.com/inotia00/ReVanced_Extended/issues/2743 // 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 DEVICE_MAKE_IOS_MUSIC = "Apple";
private static final String OS_NAME_IOS_MUSIC = "iOS"; private static final String OS_NAME_IOS_MUSIC = "iOS";
private MusicAppClient() { private YouTubeMusicAppClient() {
} }
private static String androidUserAgent(String clientVersion) { 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. * Used to fetch video information.
*/ */
@Suppress("unused") @Suppress("unused")
object YouTubeWebClient { 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 = 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)" "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. * Client version.
*/ */
@JvmField @JvmField
val clientVersion: String val clientVersion: String,
) { ) {
MWEB( MWEB(
id = 2, id = 2,
clientVersion = "2.20241202.07.00" clientVersion = "2.20241202.07.00",
), ),
WEB_REMIX( WEB_REMIX(
id = 29, 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.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.client.YouTubeWebClient import app.revanced.extension.shared.innertube.client.YouTubeWebClient
import app.revanced.extension.shared.requests.Requester 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.requests.Route.CompiledRoute
import app.revanced.extension.shared.settings.BaseSettings import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
@ -21,96 +20,17 @@ import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
@Suppress("deprecation") @Suppress("deprecation")
object PlayerRoutes { object InnerTubeRequestBody {
@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()
private const val YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/" 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 * TCP connection and HTTP read timeout
*/ */
@ -230,16 +150,10 @@ object PlayerRoutes {
client.put("osName", clientType.osName) client.put("osName", clientType.osName)
client.put("osVersion", clientType.osVersion) client.put("osVersion", clientType.osVersion)
client.put("androidSdkVersion", clientType.androidSdkVersion) client.put("androidSdkVersion", clientType.androidSdkVersion)
if (clientType.gmscoreVersionCode != null) { client.put("hl", LOCALE_LANGUAGE)
client.put("gmscoreVersionCode", clientType.gmscoreVersionCode)
}
client.put(
"hl",
LOCALE_LANGUAGE
)
client.put("gl", LOCALE_COUNTRY) client.put("gl", LOCALE_COUNTRY)
client.put("timeZone", TIME_ZONE_ID) client.put("timeZone", TIME_ZONE_ID)
client.put("utcOffsetMinutes", "$UTC_OFFSET_MINUTES") client.put("utcOffsetMinutes", UTC_OFFSET_MINUTES.toString())
val context = JSONObject() val context = JSONObject()
context.put("client", client) context.put("client", client)
@ -269,7 +183,7 @@ object PlayerRoutes {
videoIds.put(0, videoId) videoIds.put(0, videoId)
innerTubeBody.put("videoIds", videoIds) innerTubeBody.put("videoIds", videoIds)
} catch (e: JSONException) { } 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) return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -284,7 +198,7 @@ object PlayerRoutes {
try { try {
innerTubeBody.put("playlistId", playlistId) innerTubeBody.put("playlistId", playlistId)
} catch (e: JSONException) { } 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) return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -314,7 +228,7 @@ object PlayerRoutes {
actionsArray.put(0, actionsObject) actionsArray.put(0, actionsObject)
innerTubeBody.put("actions", actionsArray) innerTubeBody.put("actions", actionsArray)
} catch (e: JSONException) { } 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) return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -330,7 +244,7 @@ object PlayerRoutes {
innerTubeBody.put("playlistId", playlistId) innerTubeBody.put("playlistId", playlistId)
innerTubeBody.put("excludeWatchLater", false) innerTubeBody.put("excludeWatchLater", false)
} catch (e: JSONException) { } 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) return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -354,44 +268,57 @@ object PlayerRoutes {
actionsArray.put(0, actionsObject) actionsArray.put(0, actionsObject)
innerTubeBody.put("actions", actionsArray) innerTubeBody.put("actions", actionsArray)
} catch (e: JSONException) { } 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) return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
} }
@JvmStatic @JvmStatic
fun getPlayerResponseConnectionFromRoute( fun getInnerTubeResponseConnectionFromRoute(
route: CompiledRoute, route: CompiledRoute,
clientType: YouTubeAppClient.ClientType clientType: YouTubeAppClient.ClientType,
): HttpURLConnection { requestHeader: Map<String, String>? = null,
return getPlayerResponseConnectionFromRoute( connectTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
route, readTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
clientType.userAgent, ) = getInnerTubeResponseConnectionFromRoute(
clientType.id.toString(), route = route,
clientType.clientVersion userAgent = clientType.userAgent,
) clientId = clientType.id.toString(),
} clientVersion = clientType.clientVersion,
supportsCookies = clientType.supportsCookies,
requestHeader = requestHeader,
connectTimeout = connectTimeout,
readTimeout = readTimeout,
)
@JvmStatic @JvmStatic
fun getPlayerResponseConnectionFromRoute( fun getInnerTubeResponseConnectionFromRoute(
route: CompiledRoute, route: CompiledRoute,
clientType: YouTubeWebClient.ClientType clientType: YouTubeWebClient.ClientType,
): HttpURLConnection { requestHeader: Map<String, String>? = null,
return getPlayerResponseConnectionFromRoute( connectTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
route, readTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
clientType.userAgent, ) = getInnerTubeResponseConnectionFromRoute(
clientType.id.toString(), route = route,
clientType.clientVersion, userAgent = clientType.userAgent,
) clientId = clientType.id.toString(),
} clientVersion = clientType.clientVersion,
requestHeader = requestHeader,
connectTimeout = connectTimeout,
readTimeout = readTimeout,
)
@Throws(IOException::class) @Throws(IOException::class)
fun getPlayerResponseConnectionFromRoute( fun getInnerTubeResponseConnectionFromRoute(
route: CompiledRoute, route: CompiledRoute,
userAgent: String, userAgent: String,
clientId: 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 { ): HttpURLConnection {
val connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route) val connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route)
@ -403,8 +330,24 @@ object PlayerRoutes {
connection.useCaches = false connection.useCaches = false
connection.doOutput = true connection.doOutput = true
connection.connectTimeout = CONNECTION_TIMEOUT_MILLISECONDS connection.connectTimeout = connectTimeout
connection.readTimeout = CONNECTION_TIMEOUT_MILLISECONDS 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 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; package app.revanced.extension.shared.patches.spoof;
import app.revanced.extension.shared.patches.client.MusicAppClient.ClientType; import app.revanced.extension.shared.innertube.client.YouTubeMusicAppClient.ClientType;
import app.revanced.extension.music.settings.Settings; import app.revanced.extension.shared.settings.BaseSettings;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class SpoofClientPatch extends BlockRequestPatch { 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. * Injection point.

View File

@ -10,7 +10,7 @@ import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; 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.patches.spoof.requests.StreamingDataRequest;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
@ -19,10 +19,6 @@ import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class SpoofStreamingDataPatch extends BlockRequestPatch { 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 = private static final boolean SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION =
SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION.get(); SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION.get();
@ -79,7 +75,7 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
/** /**
* Injection point. * 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) { if (SPOOF_STREAMING_DATA) {
String id = Utils.getVideoIdFromRequest(url); String id = Utils.getVideoIdFromRequest(url);
if (id == null) { if (id == null) {
@ -89,7 +85,7 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
return; 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 package app.revanced.extension.shared.patches.spoof.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.createApplicationRequestBody import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.getPlayerResponseConnectionFromRoute import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_STREAMING_DATA
import app.revanced.extension.shared.settings.BaseSettings import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils import app.revanced.extension.shared.utils.Utils
import org.apache.commons.lang3.StringUtils
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
@ -32,21 +31,19 @@ import java.util.concurrent.TimeoutException
* did use its own client streams. * did use its own client streams.
*/ */
class StreamingDataRequest private constructor( class StreamingDataRequest private constructor(
videoId: String, playerHeaders: Map<String, String>, videoId: String,
visitorId: String, botGuardPoToken: String requestHeader: Map<String, String>,
) { ) {
private val videoId: String private val videoId: String
private val future: Future<ByteBuffer?> private val future: Future<ByteBuffer?>
init { init {
Objects.requireNonNull(playerHeaders) Objects.requireNonNull(requestHeader)
this.videoId = videoId this.videoId = videoId
this.future = Utils.submitOnBackgroundThread { this.future = Utils.submitOnBackgroundThread {
fetch( fetch(
videoId, videoId,
playerHeaders, requestHeader,
visitorId,
botGuardPoToken
) )
} }
} }
@ -86,33 +83,16 @@ class StreamingDataRequest private constructor(
companion object { companion object {
private const val AUTHORIZATION_HEADER = "Authorization" private const val AUTHORIZATION_HEADER = "Authorization"
private const val VISITOR_ID_HEADER = "X-Goog-Visitor-Id" private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
private val REQUEST_HEADER_KEYS = arrayOf(
AUTHORIZATION_HEADER, // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
VISITOR_ID_HEADER
)
private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType = private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType =
BaseSettings.SPOOF_STREAMING_DATA_TYPE.get() BaseSettings.SPOOF_STREAMING_DATA_TYPE.get()
private val CLIENT_ORDER_TO_USE: Array<YouTubeAppClient.ClientType> = private val CLIENT_ORDER_TO_USE: Array<YouTubeAppClient.ClientType> =
YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE) YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE)
private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean = private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean =
SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH
private var lastSpoofedClientType: YouTubeAppClient.ClientType? = null private var lastSpoofedClientFriendlyName: String? = 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
@GuardedBy("itself") @GuardedBy("itself")
val cache: MutableMap<String, StreamingDataRequest> = Collections.synchronizedMap( val cache: MutableMap<String, StreamingDataRequest> = Collections.synchronizedMap(
@ -126,22 +106,24 @@ class StreamingDataRequest private constructor(
@JvmStatic @JvmStatic
val lastSpoofedClientName: String val lastSpoofedClientName: String
get() = lastSpoofedClientType get() {
?.friendlyName return if (lastSpoofedClientFriendlyName != null) {
?: "Unknown" lastSpoofedClientFriendlyName!!
} else {
"Unknown"
}
}
@JvmStatic @JvmStatic
fun fetchRequest( fun fetchRequest(
videoId: String, fetchHeaders: Map<String, String>, videoId: String,
visitorId: String, botGuardPoToken: String fetchHeaders: Map<String, String>,
) { ) {
// Always fetch, even if there is an existing request for the same video. // Always fetch, even if there is an existing request for the same video.
cache[videoId] = cache[videoId] =
StreamingDataRequest( StreamingDataRequest(
videoId, videoId,
fetchHeaders, fetchHeaders
visitorId,
botGuardPoToken
) )
} }
@ -157,64 +139,28 @@ class StreamingDataRequest private constructor(
private fun send( private fun send(
clientType: YouTubeAppClient.ClientType, clientType: YouTubeAppClient.ClientType,
videoId: String, videoId: String,
playerHeaders: Map<String, String>, requestHeader: Map<String, String>,
visitorId: String,
botGuardPoToken: String
): HttpURLConnection? { ): HttpURLConnection? {
Objects.requireNonNull(clientType) Objects.requireNonNull(clientType)
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
Objects.requireNonNull(playerHeaders) Objects.requireNonNull(requestHeader)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
Logger.printDebug { "Fetching video streams for: $videoId using client: $clientType" } Logger.printDebug { "Fetching video streams for: $videoId using client: $clientType" }
try { try {
val connection = val connection =
getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType) getInnerTubeResponseConnectionFromRoute(
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS GET_STREAMING_DATA,
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS clientType,
requestHeader
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,
) )
Logger.printDebug { "Set poToken (botGuardPoToken):\n$botGuardPoToken" }
} else { val requestBody = createApplicationRequestBody(
requestBody = clientType = clientType,
createApplicationRequestBody( videoId = videoId,
clientType = clientType, setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
videoId = videoId, )
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
)
}
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -243,15 +189,15 @@ class StreamingDataRequest private constructor(
} }
private fun fetch( private fun fetch(
videoId: String, playerHeaders: Map<String, String>, videoId: String,
visitorId: String, botGuardPoToken: String requestHeader: Map<String, String>,
): ByteBuffer? { ): ByteBuffer? {
lastSpoofedClientType = null lastSpoofedClientFriendlyName = null
// Retry with different client if empty response body is received. // Retry with different client if empty response body is received.
for (clientType in CLIENT_ORDER_TO_USE) { for (clientType in CLIENT_ORDER_TO_USE) {
if (clientType.requireAuth && 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" } Logger.printDebug { "Skipped login-required client (incognito mode or not logged in)\nClient: $clientType\nVideo: $videoId" }
continue continue
@ -259,9 +205,7 @@ class StreamingDataRequest private constructor(
send( send(
clientType, clientType,
videoId, videoId,
playerHeaders, requestHeader,
visitorId,
botGuardPoToken
)?.let { connection -> )?.let { connection ->
try { try {
// gzip encoding doesn't response with content length (-1), // gzip encoding doesn't response with content length (-1),
@ -271,14 +215,14 @@ class StreamingDataRequest private constructor(
} else { } else {
BufferedInputStream(connection.inputStream).use { inputStream -> BufferedInputStream(connection.inputStream).use { inputStream ->
ByteArrayOutputStream().use { stream -> ByteArrayOutputStream().use { stream ->
val buffer = ByteArray(2048) val buffer = ByteArray(4096)
var bytesRead: Int var bytesRead: Int
while ((inputStream.read(buffer) while ((inputStream.read(buffer)
.also { bytesRead = it }) >= 0 .also { bytesRead = it }) >= 0
) { ) {
stream.write(buffer, 0, bytesRead) stream.write(buffer, 0, bytesRead)
} }
lastSpoofedClientType = clientType lastSpoofedClientFriendlyName = clientType.friendlyName
return ByteBuffer.wrap(stream.toByteArray()) 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.FALSE;
import static java.lang.Boolean.TRUE; 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.ReturnYouTubeUsernamePatch.DisplayFormat;
import app.revanced.extension.shared.patches.WatchHistoryPatch.WatchHistoryType; 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; 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. * 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 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. * 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_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); 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. // 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 EnumSetting<YouTubeAppClient.ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_VR, 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);
/** /**
* These settings are used by YouTube and YouTube Music. * 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 android.annotation.SuppressLint
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeWebClient import app.revanced.extension.shared.innertube.client.YouTubeWebClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes 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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils 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" } Logger.printDebug { "Fetching video details request for: $videoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.GET_VIDEO_DETAILS, GET_VIDEO_DETAILS,
clientType clientType
) )
val requestBody = val requestBody = createWebInnertubeBody(clientType, videoId)
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)

View File

@ -1,8 +1,10 @@
package app.revanced.extension.youtube.patches.player.requests package app.revanced.extension.youtube.patches.player.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes 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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils import app.revanced.extension.shared.utils.Utils
@ -20,10 +22,10 @@ import java.util.concurrent.TimeoutException
class ActionButtonRequest private constructor( class ActionButtonRequest private constructor(
private val videoId: String, private val videoId: String,
private val playerHeaders: Map<String, String>, private val requestHeader: Map<String, String>,
) { ) {
private val future: Future<Array<ActionButton>> = Utils.submitOnBackgroundThread { private val future: Future<Array<ActionButton>> = Utils.submitOnBackgroundThread {
fetch(videoId, playerHeaders) fetch(videoId, requestHeader)
} }
val array: Array<ActionButton> val array: Array<ActionButton>
@ -52,14 +54,6 @@ class ActionButtonRequest private constructor(
} }
companion object { 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 private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself") @GuardedBy("itself")
@ -73,11 +67,11 @@ class ActionButtonRequest private constructor(
}) })
@JvmStatic @JvmStatic
fun fetchRequestIfNeeded(videoId: String, playerHeaders: Map<String, String>) { fun fetchRequestIfNeeded(videoId: String, requestHeader: Map<String, String>) {
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
synchronized(cache) { synchronized(cache) {
if (!cache.containsKey(videoId)) { 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) Logger.printInfo({ toastMessage }, ex)
} }
private val REQUEST_HEADER_KEYS = arrayOf( private fun sendRequest(videoId: String, requestHeader: Map<String, String>): JSONObject? {
"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? {
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
// '/next' request does not require PoToken. // '/next' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" } Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try { 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, // 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. // Set the [Authorization] field to property to get the correct action buttons.
for (key in REQUEST_HEADER_KEYS) { val connection = getInnerTubeResponseConnectionFromRoute(
var value = playerHeaders[key] GET_VIDEO_ACTION_BUTTON,
if (value != null) { clientType,
connection.setRequestProperty(key, value) requestHeader,
} )
}
val requestBody = val requestBody = createApplicationRequestBody(
PlayerRoutes.createApplicationRequestBody( clientType = clientType,
clientType = clientType, videoId = videoId
videoId = videoId )
)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -214,8 +193,11 @@ class ActionButtonRequest private constructor(
return emptyArray() return emptyArray()
} }
private fun fetch(videoId: String, playerHeaders: Map<String, String>): Array<ActionButton> { private fun fetch(
val json = sendRequest(videoId, playerHeaders) videoId: String,
requestHeader: Map<String, String>
): Array<ActionButton> {
val json = sendRequest(videoId, requestHeader)
if (json != null) { if (json != null) {
return parseResponse(json) return parseResponse(json)
} }

View File

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

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes 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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils 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.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -20,12 +21,12 @@ import java.util.concurrent.TimeoutException
class DeletePlaylistRequest private constructor( class DeletePlaylistRequest private constructor(
private val playlistId: String, private val playlistId: String,
private val playerHeaders: Map<String, String>, private val requestHeader: Map<String, String>,
) { ) {
private val future: Future<Boolean> = Utils.submitOnBackgroundThread { private val future: Future<Boolean> = Utils.submitOnBackgroundThread {
fetch( fetch(
playlistId, playlistId,
playerHeaders, requestHeader,
) )
} }
@ -55,14 +56,6 @@ class DeletePlaylistRequest private constructor(
} }
companion object { 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 private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself") @GuardedBy("itself")
@ -85,14 +78,14 @@ class DeletePlaylistRequest private constructor(
@JvmStatic @JvmStatic
fun fetchRequestIfNeeded( fun fetchRequestIfNeeded(
playlistId: String, playlistId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>
) { ) {
Objects.requireNonNull(playlistId) Objects.requireNonNull(playlistId)
synchronized(cache) { synchronized(cache) {
if (!cache.containsKey(playlistId)) { if (!cache.containsKey(playlistId)) {
cache[playlistId] = DeletePlaylistRequest( cache[playlistId] = DeletePlaylistRequest(
playlistId, playlistId,
playerHeaders requestHeader
) )
} }
} }
@ -109,40 +102,26 @@ class DeletePlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex) 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( private fun sendRequest(
playlistId: String, playlistId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>
): JSONObject? { ): JSONObject? {
Objects.requireNonNull(playlistId) Objects.requireNonNull(playlistId)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
// 'playlist/delete' request does not require PoToken. // 'playlist/delete' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching delete playlist request, playlistId: $playlistId, using client: $clientTypeName" } Logger.printDebug { "Fetching delete playlist request, playlistId: $playlistId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.DELETE_PLAYLIST, DELETE_PLAYLIST,
clientType, clientType,
requestHeader
) )
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) { val requestBody = deletePlaylistRequestBody(playlistId)
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody = PlayerRoutes.deletePlaylistRequestBody(playlistId)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -184,9 +163,9 @@ class DeletePlaylistRequest private constructor(
private fun fetch( private fun fetch(
playlistId: String, playlistId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>
): Boolean? { ): Boolean? {
val json = sendRequest(playlistId, playerHeaders) val json = sendRequest(playlistId, requestHeader)
if (json != null) { if (json != null) {
return parseResponse(json) return parseResponse(json)
} }

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes 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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils 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.apache.commons.lang3.StringUtils
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
@ -23,14 +24,14 @@ class EditPlaylistRequest private constructor(
private val videoId: String, private val videoId: String,
private val playlistId: String, private val playlistId: String,
private val setVideoId: String?, private val setVideoId: String?,
private val playerHeaders: Map<String, String>, private val requestHeader: Map<String, String>,
) { ) {
private val future: Future<String> = Utils.submitOnBackgroundThread { private val future: Future<String> = Utils.submitOnBackgroundThread {
fetch( fetch(
videoId, videoId,
playlistId, playlistId,
setVideoId, setVideoId,
playerHeaders, requestHeader,
) )
} }
@ -60,14 +61,6 @@ class EditPlaylistRequest private constructor(
} }
companion object { 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 private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself") @GuardedBy("itself")
@ -99,7 +92,7 @@ class EditPlaylistRequest private constructor(
videoId: String, videoId: String,
playlistId: String, playlistId: String,
setVideoId: String?, setVideoId: String?,
playerHeaders: Map<String, String> requestHeader: Map<String, String>
) { ) {
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
synchronized(cache) { synchronized(cache) {
@ -108,7 +101,7 @@ class EditPlaylistRequest private constructor(
videoId, videoId,
playlistId, playlistId,
setVideoId, setVideoId,
playerHeaders requestHeader
) )
} }
} }
@ -125,47 +118,32 @@ class EditPlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex) 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( private fun sendRequest(
videoId: String, videoId: String,
playlistId: String, playlistId: String,
setVideoId: String?, setVideoId: String?,
playerHeaders: Map<String, String> requestHeader: Map<String, String>
): JSONObject? { ): JSONObject? {
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis() 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 clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching edit playlist request, videoId: $videoId, playlistId: $playlistId, setVideoId: $setVideoId, using client: $clientTypeName" } Logger.printDebug { "Fetching edit playlist request, videoId: $videoId, playlistId: $playlistId, setVideoId: $setVideoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.EDIT_PLAYLIST, EDIT_PLAYLIST,
clientType clientType,
requestHeader
) )
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) { val requestBody = editPlaylistRequestBody(
var value = playerHeaders[key] videoId = videoId,
if (value != null) { playlistId = playlistId,
connection.setRequestProperty(key, value) setVideoId = setVideoId
} )
}
val requestBody =
PlayerRoutes.editPlaylistRequestBody(
videoId = videoId,
playlistId = playlistId,
setVideoId = setVideoId,
)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -197,7 +175,8 @@ class EditPlaylistRequest private constructor(
if (remove) { if (remove) {
return "" return ""
} }
val playlistEditResultsJSONObject = json.getJSONArray("playlistEditResults").get(0) val playlistEditResultsJSONObject =
json.getJSONArray("playlistEditResults").get(0)
if (playlistEditResultsJSONObject is JSONObject) { if (playlistEditResultsJSONObject is JSONObject) {
return playlistEditResultsJSONObject return playlistEditResultsJSONObject
@ -220,9 +199,9 @@ class EditPlaylistRequest private constructor(
videoId: String, videoId: String,
playlistId: String, playlistId: String,
setVideoId: String?, setVideoId: String?,
playerHeaders: Map<String, String> requestHeader: Map<String, String>
): String? { ): String? {
val json = sendRequest(videoId, playlistId, setVideoId, playerHeaders) val json = sendRequest(videoId, playlistId, setVideoId, requestHeader)
if (json != null) { if (json != null) {
return parseResponse(json, StringUtils.isNotEmpty(setVideoId)) return parseResponse(json, StringUtils.isNotEmpty(setVideoId))
} }

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes 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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils 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.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -20,12 +21,12 @@ import java.util.concurrent.TimeoutException
class GetPlaylistsRequest private constructor( class GetPlaylistsRequest private constructor(
private val playlistId: String, 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 { private val future: Future<Array<Pair<String, String>>> = Utils.submitOnBackgroundThread {
fetch( fetch(
playlistId, playlistId,
playerHeaders, requestHeader,
) )
} }
@ -55,14 +56,6 @@ class GetPlaylistsRequest private constructor(
} }
companion object { 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 private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself") @GuardedBy("itself")
@ -85,14 +78,14 @@ class GetPlaylistsRequest private constructor(
@JvmStatic @JvmStatic
fun fetchRequestIfNeeded( fun fetchRequestIfNeeded(
playlistId: String, playlistId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>
) { ) {
Objects.requireNonNull(playlistId) Objects.requireNonNull(playlistId)
synchronized(cache) { synchronized(cache) {
if (!cache.containsKey(playlistId)) { if (!cache.containsKey(playlistId)) {
cache[playlistId] = GetPlaylistsRequest( cache[playlistId] = GetPlaylistsRequest(
playlistId, playlistId,
playerHeaders requestHeader
) )
} }
} }
@ -109,40 +102,26 @@ class GetPlaylistsRequest private constructor(
Logger.printInfo({ toastMessage }, ex) 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( private fun sendRequest(
playlistId: String, playlistId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>
): JSONObject? { ): JSONObject? {
Objects.requireNonNull(playlistId) Objects.requireNonNull(playlistId)
val startTime = System.currentTimeMillis() 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 clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching get playlists request, playlistId: $playlistId, using client: $clientTypeName" } Logger.printDebug { "Fetching get playlists request, playlistId: $playlistId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.GET_PLAYLISTS, GET_PLAYLISTS,
clientType clientType,
requestHeader
) )
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) { val requestBody = getPlaylistsRequestBody(playlistId)
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody = PlayerRoutes.getPlaylistsRequestBody(playlistId)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -223,9 +202,9 @@ class GetPlaylistsRequest private constructor(
private fun fetch( private fun fetch(
playlistId: String, playlistId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>
): Array<Pair<String, String>>? { ): Array<Pair<String, String>>? {
val json = sendRequest(playlistId, playerHeaders) val json = sendRequest(playlistId, requestHeader)
if (json != null) { if (json != null) {
return parseResponse(json) return parseResponse(json)
} }

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes 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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils 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.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -21,13 +22,13 @@ import java.util.concurrent.TimeoutException
class SavePlaylistRequest private constructor( class SavePlaylistRequest private constructor(
private val playlistId: String, private val playlistId: String,
private val libraryId: String, private val libraryId: String,
private val playerHeaders: Map<String, String>, private val requestHeader: Map<String, String>,
) { ) {
private val future: Future<Boolean> = Utils.submitOnBackgroundThread { private val future: Future<Boolean> = Utils.submitOnBackgroundThread {
fetch( fetch(
playlistId, playlistId,
libraryId, libraryId,
playerHeaders, requestHeader,
) )
} }
@ -57,14 +58,6 @@ class SavePlaylistRequest private constructor(
} }
companion object { 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 private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself") @GuardedBy("itself")
@ -88,14 +81,14 @@ class SavePlaylistRequest private constructor(
fun fetchRequestIfNeeded( fun fetchRequestIfNeeded(
playlistId: String, playlistId: String,
libraryId: String, libraryId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>
) { ) {
Objects.requireNonNull(playlistId) Objects.requireNonNull(playlistId)
synchronized(cache) { synchronized(cache) {
cache[libraryId] = SavePlaylistRequest( cache[libraryId] = SavePlaylistRequest(
playlistId, playlistId,
libraryId, libraryId,
playerHeaders requestHeader
) )
} }
} }
@ -111,43 +104,28 @@ class SavePlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex) 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( private fun sendRequest(
playlistId: String, playlistId: String,
libraryId: String, libraryId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>
): JSONObject? { ): JSONObject? {
Objects.requireNonNull(playlistId) Objects.requireNonNull(playlistId)
Objects.requireNonNull(libraryId) Objects.requireNonNull(libraryId)
val startTime = System.currentTimeMillis() 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 clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching edit playlist request, playlistId: $playlistId, libraryId: $libraryId, using client: $clientTypeName" } Logger.printDebug { "Fetching edit playlist request, playlistId: $playlistId, libraryId: $libraryId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.EDIT_PLAYLIST, EDIT_PLAYLIST,
clientType clientType,
requestHeader
) )
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) { val requestBody = savePlaylistRequestBody(libraryId, playlistId)
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody =
PlayerRoutes.savePlaylistRequestBody(libraryId, playlistId)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -190,9 +168,9 @@ class SavePlaylistRequest private constructor(
private fun fetch( private fun fetch(
playlistId: String, playlistId: String,
libraryId: String, libraryId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>
): Boolean? { ): Boolean? {
val json = sendRequest(playlistId, libraryId,playerHeaders) val json = sendRequest(playlistId, libraryId, requestHeader)
if (json != null) { if (json != null) {
return parseResponse(json) return parseResponse(json)
} }

View File

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