mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-05-29 21:30:19 +02:00
feat(YouTube Music): Add Spoof streaming data
patch
This commit is contained in:
parent
21acf6f003
commit
7dfd817ba3
@ -1,19 +1,44 @@
|
|||||||
package app.revanced.extension.music.patches.misc;
|
package app.revanced.extension.music.patches.misc;
|
||||||
|
|
||||||
import app.revanced.extension.music.patches.misc.client.AppClient.ClientType;
|
|
||||||
import app.revanced.extension.music.settings.Settings;
|
import app.revanced.extension.music.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class SpoofClientPatch {
|
public class SpoofClientPatch {
|
||||||
|
private static final int CLIENT_ID_IOS_MUSIC = 26;
|
||||||
|
/**
|
||||||
|
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* It can be extracted by getting the latest release version of the app on
|
||||||
|
* <a href="https://apps.apple.com/us/app/youtube-music/id1017492454/">the App
|
||||||
|
* Store page of the YouTube app</a>, in the {@code What¡¯s New} section.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private static final String CLIENT_VERSION_IOS_MUSIC = "6.21";
|
||||||
|
/**
|
||||||
|
* See <a href="https://gist.github.com/adamawolf/3048717">this GitHub Gist</a> for more
|
||||||
|
* information.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private static final String DEVICE_MODEL_IOS_MUSIC = "iPhone16,2";
|
||||||
|
private static final String OS_VERSION_IOS_MUSIC = "17.7.2.21H221";
|
||||||
|
private static final String USER_AGENT_VERSION_IOS_MUSIC = "17_7_2";
|
||||||
|
private static final String USER_AGENT_IOS_MUSIC = "com.google.ios.youtubemusic/" +
|
||||||
|
CLIENT_VERSION_IOS_MUSIC +
|
||||||
|
"(" +
|
||||||
|
DEVICE_MODEL_IOS_MUSIC +
|
||||||
|
"; U; CPU iOS " +
|
||||||
|
USER_AGENT_VERSION_IOS_MUSIC +
|
||||||
|
" like Mac OS X)";
|
||||||
|
|
||||||
private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get();
|
private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get();
|
||||||
private static final ClientType clientType = ClientType.IOS_MUSIC;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static int getClientTypeId(int originalClientTypeId) {
|
public static int getClientTypeId(int originalClientTypeId) {
|
||||||
if (SPOOF_CLIENT_ENABLED) {
|
if (SPOOF_CLIENT_ENABLED) {
|
||||||
return clientType.id;
|
return CLIENT_ID_IOS_MUSIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalClientTypeId;
|
return originalClientTypeId;
|
||||||
@ -24,7 +49,7 @@ public class SpoofClientPatch {
|
|||||||
*/
|
*/
|
||||||
public static String getClientVersion(String originalClientVersion) {
|
public static String getClientVersion(String originalClientVersion) {
|
||||||
if (SPOOF_CLIENT_ENABLED) {
|
if (SPOOF_CLIENT_ENABLED) {
|
||||||
return clientType.clientVersion;
|
return CLIENT_VERSION_IOS_MUSIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalClientVersion;
|
return originalClientVersion;
|
||||||
@ -35,7 +60,7 @@ public class SpoofClientPatch {
|
|||||||
*/
|
*/
|
||||||
public static String getClientModel(String originalClientModel) {
|
public static String getClientModel(String originalClientModel) {
|
||||||
if (SPOOF_CLIENT_ENABLED) {
|
if (SPOOF_CLIENT_ENABLED) {
|
||||||
return clientType.deviceModel;
|
return DEVICE_MODEL_IOS_MUSIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalClientModel;
|
return originalClientModel;
|
||||||
@ -46,7 +71,7 @@ public class SpoofClientPatch {
|
|||||||
*/
|
*/
|
||||||
public static String getOsVersion(String originalOsVersion) {
|
public static String getOsVersion(String originalOsVersion) {
|
||||||
if (SPOOF_CLIENT_ENABLED) {
|
if (SPOOF_CLIENT_ENABLED) {
|
||||||
return clientType.osVersion;
|
return OS_VERSION_IOS_MUSIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalOsVersion;
|
return originalOsVersion;
|
||||||
@ -57,7 +82,7 @@ public class SpoofClientPatch {
|
|||||||
*/
|
*/
|
||||||
public static String getUserAgent(String originalUserAgent) {
|
public static String getUserAgent(String originalUserAgent) {
|
||||||
if (SPOOF_CLIENT_ENABLED) {
|
if (SPOOF_CLIENT_ENABLED) {
|
||||||
return clientType.userAgent;
|
return USER_AGENT_IOS_MUSIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalUserAgent;
|
return originalUserAgent;
|
||||||
|
@ -1,122 +0,0 @@
|
|||||||
package app.revanced.extension.music.patches.misc.client;
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
public class AppClient {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* It can be extracted by getting the latest release version of the app on
|
|
||||||
* <a href="https://apps.apple.com/us/app/music-watch-listen-stream/id544007664/">the App
|
|
||||||
* Store page of the YouTube app</a>, in the {@code What’s New} section.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
private static final String CLIENT_VERSION_IOS = "6.21";
|
|
||||||
private static final String DEVICE_MAKE_IOS = "Apple";
|
|
||||||
/**
|
|
||||||
* See <a href="https://gist.github.com/adamawolf/3048717">this GitHub Gist</a> for more
|
|
||||||
* information.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
private static final String DEVICE_MODEL_IOS = "iPhone16,2";
|
|
||||||
private static final String OS_NAME_IOS = "iOS";
|
|
||||||
private static final String OS_VERSION_IOS = "17.7.2.21H221";
|
|
||||||
private static final String USER_AGENT_VERSION_IOS = "17_7_2";
|
|
||||||
private static final String USER_AGENT_IOS = "com.google.ios.youtubemusic/" +
|
|
||||||
CLIENT_VERSION_IOS +
|
|
||||||
"(" +
|
|
||||||
DEVICE_MODEL_IOS +
|
|
||||||
"; U; CPU iOS " +
|
|
||||||
USER_AGENT_VERSION_IOS +
|
|
||||||
" like Mac OS X)";
|
|
||||||
|
|
||||||
private AppClient() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ClientType {
|
|
||||||
IOS_MUSIC(26,
|
|
||||||
DEVICE_MAKE_IOS,
|
|
||||||
DEVICE_MODEL_IOS,
|
|
||||||
CLIENT_VERSION_IOS,
|
|
||||||
OS_NAME_IOS,
|
|
||||||
OS_VERSION_IOS,
|
|
||||||
null,
|
|
||||||
USER_AGENT_IOS,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* YouTube
|
|
||||||
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
|
|
||||||
*/
|
|
||||||
public final int id;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device manufacturer.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public final String deviceMake;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
|
|
||||||
*/
|
|
||||||
public final String deviceModel;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device OS name.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public final String osName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Device OS version.
|
|
||||||
*/
|
|
||||||
public final String osVersion;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Player user-agent.
|
|
||||||
*/
|
|
||||||
public final String userAgent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
|
|
||||||
* Field is null if not applicable.
|
|
||||||
*/
|
|
||||||
public final Integer androidSdkVersion;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* App version.
|
|
||||||
*/
|
|
||||||
public final String clientVersion;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the client can access the API logged in.
|
|
||||||
*/
|
|
||||||
public final boolean canLogin;
|
|
||||||
|
|
||||||
ClientType(int id,
|
|
||||||
@Nullable String deviceMake,
|
|
||||||
String deviceModel,
|
|
||||||
String clientVersion,
|
|
||||||
@Nullable String osName,
|
|
||||||
String osVersion,
|
|
||||||
Integer androidSdkVersion,
|
|
||||||
String userAgent,
|
|
||||||
boolean canLogin
|
|
||||||
) {
|
|
||||||
this.id = id;
|
|
||||||
this.deviceMake = deviceMake;
|
|
||||||
this.deviceModel = deviceModel;
|
|
||||||
this.clientVersion = clientVersion;
|
|
||||||
this.osName = osName;
|
|
||||||
this.osVersion = osVersion;
|
|
||||||
this.androidSdkVersion = androidSdkVersion;
|
|
||||||
this.userAgent = userAgent;
|
|
||||||
this.canLogin = canLogin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +1,14 @@
|
|||||||
package app.revanced.extension.shared.patches;
|
package app.revanced.extension.shared.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.patches.client.AppClient.ClientType;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class PatchStatus {
|
public class PatchStatus {
|
||||||
public static boolean HideFullscreenAdsDefaultBoolean() {
|
public static boolean HideFullscreenAdsDefaultBoolean() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ClientType SpoofStreamingDataDefaultClient() {
|
||||||
|
return ClientType.IOS;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
package app.revanced.extension.youtube.patches.misc.client;
|
package app.revanced.extension.shared.patches.client;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
import static app.revanced.extension.shared.utils.ResourceUtils.getString;
|
||||||
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
public class AppClient {
|
public class AppClient {
|
||||||
|
|
||||||
// ANDROID
|
|
||||||
private static final String OS_NAME_ANDROID = "Android";
|
|
||||||
|
|
||||||
// IOS
|
// IOS
|
||||||
/**
|
/**
|
||||||
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
|
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
|
||||||
@ -21,8 +17,7 @@ public class AppClient {
|
|||||||
* Store page of the YouTube app</a>, in the {@code What’s New} section.
|
* Store page of the YouTube app</a>, in the {@code What’s New} section.
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
private static final String CLIENT_VERSION_IOS = "19.47.7";
|
private static final String CLIENT_VERSION_IOS = "19.49.5";
|
||||||
private static final String DEVICE_MAKE_IOS = "Apple";
|
|
||||||
/**
|
/**
|
||||||
* The device machine id for the iPhone 16 Pro Max (iPhone17,2), used to get HDR with AV1 hardware decoding.
|
* The device machine id for the iPhone 16 Pro Max (iPhone17,2), used to get HDR with AV1 hardware decoding.
|
||||||
*
|
*
|
||||||
@ -31,11 +26,13 @@ public class AppClient {
|
|||||||
* information.
|
* information.
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
private static final String DEVICE_MODEL_IOS = "iPhone17,2"
|
private static final String DEVICE_MODEL_IOS = "iPhone17,2";
|
||||||
private static final String OS_NAME_IOS = "iOS";
|
/**
|
||||||
private static final String OS_VERSION_IOS = "18.1.1.22B91"
|
* The minimum supported OS version for the iOS YouTube client is iOS 14.0.
|
||||||
private static final String USER_AGENT_VERSION_IOS = "18_1_1"
|
* Using an invalid OS version will use the AVC codec.
|
||||||
|
*/
|
||||||
|
private static final String OS_VERSION_IOS = "18.1.1.22B91";
|
||||||
|
private static final String USER_AGENT_VERSION_IOS = "18_1_1";
|
||||||
private static final String USER_AGENT_IOS = "com.google.ios.youtube/" +
|
private static final String USER_AGENT_IOS = "com.google.ios.youtube/" +
|
||||||
CLIENT_VERSION_IOS +
|
CLIENT_VERSION_IOS +
|
||||||
"(" +
|
"(" +
|
||||||
@ -44,6 +41,25 @@ public class AppClient {
|
|||||||
USER_AGENT_VERSION_IOS +
|
USER_AGENT_VERSION_IOS +
|
||||||
" like Mac OS X)";
|
" like Mac OS X)";
|
||||||
|
|
||||||
|
// IOS_MUSIC
|
||||||
|
/**
|
||||||
|
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* It can be extracted by getting the latest release version of the app on
|
||||||
|
* <a href="https://apps.apple.com/us/app/youtube-music/id1017492454/">the App
|
||||||
|
* Store page of the YouTube app</a>, in the {@code What’s New} section.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
private static final String CLIENT_VERSION_IOS_MUSIC = "7.31.2";
|
||||||
|
private static final String USER_AGENT_IOS_MUSIC = "com.google.ios.youtubemusic/" +
|
||||||
|
CLIENT_VERSION_IOS +
|
||||||
|
"(" +
|
||||||
|
DEVICE_MODEL_IOS +
|
||||||
|
"; U; CPU iOS " +
|
||||||
|
USER_AGENT_VERSION_IOS +
|
||||||
|
" like Mac OS X)";
|
||||||
|
|
||||||
// ANDROID VR
|
// ANDROID VR
|
||||||
/**
|
/**
|
||||||
* The hardcoded client version of the Android VR app used for InnerTube requests with this client.
|
* The hardcoded client version of the Android VR app used for InnerTube requests with this client.
|
||||||
@ -54,7 +70,7 @@ public class AppClient {
|
|||||||
* Store page of the YouTube app</a>, in the {@code Additional details} section.
|
* Store page of the YouTube app</a>, in the {@code Additional details} section.
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
private static final String CLIENT_VERSION_ANDROID_VR = "1.60.19";
|
private static final String CLIENT_VERSION_ANDROID_VR = "1.61.47";
|
||||||
/**
|
/**
|
||||||
* The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client.
|
* The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client.
|
||||||
*
|
*
|
||||||
@ -69,7 +85,7 @@ public class AppClient {
|
|||||||
* The SDK version for Android 12 is 31,
|
* The SDK version for Android 12 is 31,
|
||||||
* but for some reason the build.props for the {@code Quest 3} state that the SDK version is 32.
|
* but for some reason the build.props for the {@code Quest 3} state that the SDK version is 32.
|
||||||
*/
|
*/
|
||||||
private static final int ANDROID_SDK_VERSION_ANDROID_VR = 32;
|
private static final String ANDROID_SDK_VERSION_ANDROID_VR = "32";
|
||||||
/**
|
/**
|
||||||
* Package name for YouTube VR (Google DayDream): com.google.android.apps.youtube.vr (Deprecated)
|
* Package name for YouTube VR (Google DayDream): com.google.android.apps.youtube.vr (Deprecated)
|
||||||
* Package name for YouTube VR (Meta Quests): com.google.android.apps.youtube.vr.oculus
|
* Package name for YouTube VR (Meta Quests): com.google.android.apps.youtube.vr.oculus
|
||||||
@ -82,7 +98,7 @@ public class AppClient {
|
|||||||
"; GB) gzip";
|
"; GB) gzip";
|
||||||
|
|
||||||
// ANDROID UNPLUGGED
|
// ANDROID UNPLUGGED
|
||||||
private static final String CLIENT_VERSION_ANDROID_UNPLUGGED = "8.47.0";
|
private static final String CLIENT_VERSION_ANDROID_UNPLUGGED = "8.49.0";
|
||||||
/**
|
/**
|
||||||
* The device machine id for the Chromecast with Google TV 4K.
|
* The device machine id for the Chromecast with Google TV 4K.
|
||||||
*
|
*
|
||||||
@ -93,7 +109,7 @@ public class AppClient {
|
|||||||
*/
|
*/
|
||||||
private static final String DEVICE_MODEL_ANDROID_UNPLUGGED = "Google TV Streamer";
|
private static final String DEVICE_MODEL_ANDROID_UNPLUGGED = "Google TV Streamer";
|
||||||
private static final String OS_VERSION_ANDROID_UNPLUGGED = "14";
|
private static final String OS_VERSION_ANDROID_UNPLUGGED = "14";
|
||||||
private static final int ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = 34;
|
private static final String ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = "34";
|
||||||
private static final String USER_AGENT_ANDROID_UNPLUGGED = "com.google.android.apps.youtube.unplugged/" +
|
private static final String USER_AGENT_ANDROID_UNPLUGGED = "com.google.android.apps.youtube.unplugged/" +
|
||||||
CLIENT_VERSION_ANDROID_UNPLUGGED +
|
CLIENT_VERSION_ANDROID_UNPLUGGED +
|
||||||
" (Linux; U; Android " +
|
" (Linux; U; Android " +
|
||||||
@ -105,61 +121,52 @@ public class AppClient {
|
|||||||
|
|
||||||
public enum ClientType {
|
public enum ClientType {
|
||||||
IOS(5,
|
IOS(5,
|
||||||
DEVICE_MAKE_IOS,
|
|
||||||
DEVICE_MODEL_IOS,
|
DEVICE_MODEL_IOS,
|
||||||
CLIENT_VERSION_IOS,
|
|
||||||
OS_NAME_IOS,
|
|
||||||
OS_VERSION_IOS,
|
OS_VERSION_IOS,
|
||||||
null,
|
|
||||||
USER_AGENT_IOS,
|
USER_AGENT_IOS,
|
||||||
|
null,
|
||||||
|
CLIENT_VERSION_IOS,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
ANDROID_VR(28,
|
ANDROID_VR(28,
|
||||||
null,
|
|
||||||
DEVICE_MODEL_ANDROID_VR,
|
DEVICE_MODEL_ANDROID_VR,
|
||||||
CLIENT_VERSION_ANDROID_VR,
|
|
||||||
OS_NAME_ANDROID,
|
|
||||||
OS_VERSION_ANDROID_VR,
|
OS_VERSION_ANDROID_VR,
|
||||||
ANDROID_SDK_VERSION_ANDROID_VR,
|
|
||||||
USER_AGENT_ANDROID_VR,
|
USER_AGENT_ANDROID_VR,
|
||||||
|
ANDROID_SDK_VERSION_ANDROID_VR,
|
||||||
|
CLIENT_VERSION_ANDROID_VR,
|
||||||
true
|
true
|
||||||
),
|
),
|
||||||
ANDROID_UNPLUGGED(29,
|
ANDROID_UNPLUGGED(29,
|
||||||
null,
|
|
||||||
DEVICE_MODEL_ANDROID_UNPLUGGED,
|
DEVICE_MODEL_ANDROID_UNPLUGGED,
|
||||||
CLIENT_VERSION_ANDROID_UNPLUGGED,
|
|
||||||
OS_NAME_ANDROID,
|
|
||||||
OS_VERSION_ANDROID_UNPLUGGED,
|
OS_VERSION_ANDROID_UNPLUGGED,
|
||||||
ANDROID_SDK_VERSION_ANDROID_UNPLUGGED,
|
|
||||||
USER_AGENT_ANDROID_UNPLUGGED,
|
USER_AGENT_ANDROID_UNPLUGGED,
|
||||||
|
ANDROID_SDK_VERSION_ANDROID_UNPLUGGED,
|
||||||
|
CLIENT_VERSION_ANDROID_UNPLUGGED,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
IOS_MUSIC(
|
||||||
|
26,
|
||||||
|
DEVICE_MODEL_IOS,
|
||||||
|
OS_VERSION_IOS,
|
||||||
|
USER_AGENT_IOS_MUSIC,
|
||||||
|
null,
|
||||||
|
CLIENT_VERSION_IOS_MUSIC,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
public final String friendlyName;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* YouTube
|
* YouTube
|
||||||
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
|
* <a href="https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients">client type</a>
|
||||||
*/
|
*/
|
||||||
public final int id;
|
public final int id;
|
||||||
|
|
||||||
/**
|
public final String clientName;
|
||||||
* Device manufacturer.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public final String deviceMake;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
|
* Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
|
||||||
*/
|
*/
|
||||||
public final String deviceModel;
|
public final String deviceModel;
|
||||||
|
|
||||||
/**
|
|
||||||
* Device OS name.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
public final String osName;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Device OS version.
|
* Device OS version.
|
||||||
*/
|
*/
|
||||||
@ -174,7 +181,8 @@ public class AppClient {
|
|||||||
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
|
* Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
|
||||||
* Field is null if not applicable.
|
* Field is null if not applicable.
|
||||||
*/
|
*/
|
||||||
public final Integer androidSdkVersion;
|
@Nullable
|
||||||
|
public final String androidSdkVersion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App version.
|
* App version.
|
||||||
@ -187,25 +195,25 @@ public class AppClient {
|
|||||||
public final boolean canLogin;
|
public final boolean canLogin;
|
||||||
|
|
||||||
ClientType(int id,
|
ClientType(int id,
|
||||||
@Nullable String deviceMake,
|
|
||||||
String deviceModel,
|
String deviceModel,
|
||||||
String clientVersion,
|
|
||||||
@Nullable String osName,
|
|
||||||
String osVersion,
|
String osVersion,
|
||||||
Integer androidSdkVersion,
|
|
||||||
String userAgent,
|
String userAgent,
|
||||||
|
@Nullable String androidSdkVersion,
|
||||||
|
String clientVersion,
|
||||||
boolean canLogin
|
boolean canLogin
|
||||||
) {
|
) {
|
||||||
this.friendlyName = str("revanced_spoof_streaming_data_type_entry_" + name().toLowerCase());
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.deviceMake = deviceMake;
|
this.clientName = name();
|
||||||
this.deviceModel = deviceModel;
|
this.deviceModel = deviceModel;
|
||||||
this.clientVersion = clientVersion;
|
this.clientVersion = clientVersion;
|
||||||
this.osName = osName;
|
|
||||||
this.osVersion = osVersion;
|
this.osVersion = osVersion;
|
||||||
this.androidSdkVersion = androidSdkVersion;
|
this.androidSdkVersion = androidSdkVersion;
|
||||||
this.userAgent = userAgent;
|
this.userAgent = userAgent;
|
||||||
this.canLogin = canLogin;
|
this.canLogin = canLogin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final String getFriendlyName() {
|
||||||
|
return getString("revanced_spoof_streaming_data_type_entry_" + name().toLowerCase());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package app.revanced.extension.youtube.patches.misc;
|
package app.revanced.extension.shared.patches.spoof;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@ -7,19 +7,15 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
|
||||||
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.misc.client.AppClient.ClientType;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.youtube.patches.misc.requests.StreamingDataRequest;
|
import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class SpoofStreamingDataPatch {
|
public class SpoofStreamingDataPatch {
|
||||||
private static final boolean SPOOF_STREAMING_DATA = Settings.SPOOF_STREAMING_DATA.get();
|
private static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_STREAMING_DATA.get();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any unreachable ip address. Used to intentionally fail requests.
|
* Any unreachable ip address. Used to intentionally fail requests.
|
||||||
*/
|
*/
|
||||||
@ -90,10 +86,19 @@ public class SpoofStreamingDataPatch {
|
|||||||
try {
|
try {
|
||||||
Uri uri = Uri.parse(url);
|
Uri uri = Uri.parse(url);
|
||||||
String path = uri.getPath();
|
String path = uri.getPath();
|
||||||
|
|
||||||
// 'heartbeat' has no video id and appears to be only after playback has started.
|
// 'heartbeat' has no video id and appears to be only after playback has started.
|
||||||
if (path != null && path.contains("player") && !path.contains("heartbeat")) {
|
// 'refresh' has no video id and appears to happen when waiting for a livestream to start.
|
||||||
String videoId = Objects.requireNonNull(uri.getQueryParameter("id"));
|
if (path != null && path.contains("player") && !path.contains("heartbeat")
|
||||||
StreamingDataRequest.fetchRequest(videoId, requestHeaders);
|
&& !path.contains("refresh")) {
|
||||||
|
String id = uri.getQueryParameter("id");
|
||||||
|
if (id == null) {
|
||||||
|
Logger.printException(() -> "Ignoring request that has no video id." +
|
||||||
|
" Url: " + url + " headers: " + requestHeaders);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamingDataRequest.fetchRequest(id, requestHeaders);
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "buildRequest failure", ex);
|
Logger.printException(() -> "buildRequest failure", ex);
|
||||||
@ -104,7 +109,7 @@ public class SpoofStreamingDataPatch {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
* Fix playback by replace the streaming data.
|
* Fix playback by replace the streaming data.
|
||||||
* Called after {@link #fetchStreams(String, Map)} .
|
* Called after {@link #fetchStreams(String, Map)}.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public static ByteBuffer getStreamingData(String videoId) {
|
public static ByteBuffer getStreamingData(String videoId) {
|
||||||
@ -117,9 +122,10 @@ public class SpoofStreamingDataPatch {
|
|||||||
// This is not a concern, since the fetch will always be finished
|
// This is not a concern, since the fetch will always be finished
|
||||||
// and never block the main thread.
|
// and never block the main thread.
|
||||||
// But if debugging, then still verify this is the situation.
|
// But if debugging, then still verify this is the situation.
|
||||||
if (Settings.ENABLE_DEBUG_LOGGING.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
|
if (BaseSettings.ENABLE_DEBUG_LOGGING.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
|
||||||
Logger.printException(() -> "Error: Blocking main thread");
|
Logger.printException(() -> "Error: Blocking main thread");
|
||||||
}
|
}
|
||||||
|
|
||||||
var stream = request.getStream();
|
var stream = request.getStream();
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
Logger.printDebug(() -> "Overriding video stream: " + videoId);
|
Logger.printDebug(() -> "Overriding video stream: " + videoId);
|
||||||
@ -164,7 +170,7 @@ public class SpoofStreamingDataPatch {
|
|||||||
*/
|
*/
|
||||||
public static String appendSpoofedClient(String videoFormat) {
|
public static String appendSpoofedClient(String videoFormat) {
|
||||||
try {
|
try {
|
||||||
if (SPOOF_STREAMING_DATA && Settings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get()
|
if (SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get()
|
||||||
&& !TextUtils.isEmpty(videoFormat)) {
|
&& !TextUtils.isEmpty(videoFormat)) {
|
||||||
// Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages
|
// Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages
|
||||||
return "\u202D" + videoFormat + String.format("\u2009(%s)", StreamingDataRequest.getLastSpoofedClientName()); // u202D = left to right override
|
return "\u202D" + videoFormat + String.format("\u2009(%s)", StreamingDataRequest.getLastSpoofedClientName()); // u202D = left to right override
|
||||||
@ -175,11 +181,4 @@ public class SpoofStreamingDataPatch {
|
|||||||
|
|
||||||
return videoFormat;
|
return videoFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class iOSAvailability implements Setting.Availability {
|
|
||||||
@Override
|
|
||||||
public boolean isAvailable() {
|
|
||||||
return Settings.SPOOF_STREAMING_DATA.get() && Settings.SPOOF_STREAMING_DATA_TYPE.get() == ClientType.IOS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,81 +1,69 @@
|
|||||||
package app.revanced.extension.youtube.patches.misc.requests;
|
package app.revanced.extension.shared.patches.spoof.requests;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.patches.client.AppClient.ClientType;
|
||||||
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;
|
||||||
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.misc.client.AppClient.ClientType;
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings({"ExtractMethodRecommender", "deprecation"})
|
||||||
public final class PlayerRoutes {
|
public final class PlayerRoutes {
|
||||||
/**
|
public static final Route.CompiledRoute GET_PLAYLIST_PAGE = new Route(
|
||||||
* The base URL of requests of non-web clients to the InnerTube internal API.
|
Route.Method.POST,
|
||||||
*/
|
"next" +
|
||||||
private static final String YOUTUBEI_V1_GAPIS_URL = "https://youtubei.googleapis.com/youtubei/v1/";
|
"?fields=contents.singleColumnWatchNextResults.playlist.playlist"
|
||||||
|
).compile();
|
||||||
static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
|
static final Route.CompiledRoute GET_STREAMING_DATA = new Route(
|
||||||
Route.Method.POST,
|
Route.Method.POST,
|
||||||
"player" +
|
"player" +
|
||||||
"?fields=streamingData" +
|
"?fields=streamingData" +
|
||||||
"&alt=proto"
|
"&alt=proto"
|
||||||
).compile();
|
).compile();
|
||||||
|
private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/";
|
||||||
static final Route.CompiledRoute GET_PLAYLIST_PAGE = new Route(
|
|
||||||
Route.Method.POST,
|
|
||||||
"next" +
|
|
||||||
"?fields=contents.singleColumnWatchNextResults.playlist.playlist"
|
|
||||||
).compile();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TCP connection and HTTP read timeout
|
* TCP connection and HTTP read timeout
|
||||||
*/
|
*/
|
||||||
private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds.
|
private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds.
|
||||||
|
|
||||||
|
private static final String LOCALE_LANGUAGE = Utils.getContext().getResources()
|
||||||
|
.getConfiguration().locale.getLanguage();
|
||||||
|
|
||||||
private PlayerRoutes() {
|
private PlayerRoutes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static String createInnertubeBody(ClientType clientType, String videoId) {
|
public static String createInnertubeBody(ClientType clientType) {
|
||||||
return createInnertubeBody(clientType, videoId, null);
|
return createInnertubeBody(clientType, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
static String createInnertubeBody(ClientType clientType, String videoId, String playlistId) {
|
public static String createInnertubeBody(ClientType clientType, boolean playlistId) {
|
||||||
JSONObject innerTubeBody = new JSONObject();
|
JSONObject innerTubeBody = new JSONObject();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
JSONObject context = new JSONObject();
|
|
||||||
|
|
||||||
JSONObject client = new JSONObject();
|
JSONObject client = new JSONObject();
|
||||||
client.put("clientName", clientType.name());
|
client.put("clientName", clientType.clientName);
|
||||||
client.put("clientVersion", clientType.clientVersion);
|
client.put("clientVersion", clientType.clientVersion);
|
||||||
client.put("deviceModel", clientType.deviceModel);
|
client.put("deviceModel", clientType.deviceModel);
|
||||||
client.put("osVersion", clientType.osVersion);
|
client.put("osVersion", clientType.osVersion);
|
||||||
if (clientType.deviceMake != null) {
|
|
||||||
client.put("deviceMake", clientType.deviceMake);
|
|
||||||
}
|
|
||||||
if (clientType.osName != null) {
|
|
||||||
client.put("osName", clientType.osName);
|
|
||||||
}
|
|
||||||
if (clientType.androidSdkVersion != null) {
|
if (clientType.androidSdkVersion != null) {
|
||||||
client.put("androidSdkVersion", clientType.androidSdkVersion.toString());
|
client.put("androidSdkVersion", clientType.androidSdkVersion);
|
||||||
}
|
}
|
||||||
String languageCode = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().locale.getLanguage();
|
client.put("hl", LOCALE_LANGUAGE);
|
||||||
client.put("hl", languageCode);
|
|
||||||
|
|
||||||
|
JSONObject context = new JSONObject();
|
||||||
context.put("client", client);
|
context.put("client", client);
|
||||||
|
|
||||||
innerTubeBody.put("context", context);
|
innerTubeBody.put("context", context);
|
||||||
innerTubeBody.put("contentCheckOk", true);
|
innerTubeBody.put("contentCheckOk", true);
|
||||||
innerTubeBody.put("racyCheckOk", true);
|
innerTubeBody.put("racyCheckOk", true);
|
||||||
innerTubeBody.put("videoId", videoId);
|
innerTubeBody.put("videoId", "%s");
|
||||||
if (playlistId != null) {
|
if (playlistId) {
|
||||||
innerTubeBody.put("playlistId", playlistId);
|
innerTubeBody.put("playlistId", "%s");
|
||||||
}
|
}
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
Logger.printException(() -> "Failed to create innerTubeBody", e);
|
Logger.printException(() -> "Failed to create innerTubeBody", e);
|
||||||
@ -87,13 +75,11 @@ public final class PlayerRoutes {
|
|||||||
/**
|
/**
|
||||||
* @noinspection SameParameterValue
|
* @noinspection SameParameterValue
|
||||||
*/
|
*/
|
||||||
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
|
public static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
|
||||||
var connection = Requester.getConnectionFromCompiledRoute(YOUTUBEI_V1_GAPIS_URL, route);
|
var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
|
||||||
|
|
||||||
connection.setRequestProperty("Content-Type", "application/json");
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
connection.setRequestProperty("User-Agent", clientType.userAgent);
|
connection.setRequestProperty("User-Agent", clientType.userAgent);
|
||||||
connection.setRequestProperty("X-YouTube-Client-Name", clientType.id);
|
|
||||||
connection.setRequestProperty("X-YouTube-Client-Version", clientType.clientVersion);
|
|
||||||
|
|
||||||
connection.setUseCaches(false);
|
connection.setUseCaches(false);
|
||||||
connection.setDoOutput(true);
|
connection.setDoOutput(true);
|
||||||
@ -102,4 +88,4 @@ public final class PlayerRoutes {
|
|||||||
connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
|
connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS);
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,7 @@
|
|||||||
package app.revanced.extension.youtube.patches.misc.requests;
|
package app.revanced.extension.shared.patches.spoof.requests;
|
||||||
|
|
||||||
import static app.revanced.extension.youtube.patches.misc.requests.PlayerRoutes.GET_STREAMING_DATA;
|
import static app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
|
||||||
|
|
||||||
import androidx.annotation.GuardedBy;
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
@ -11,60 +10,57 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Collections;
|
import java.util.*;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
||||||
|
import app.revanced.extension.shared.patches.client.AppClient.ClientType;
|
||||||
|
|
||||||
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.misc.client.AppClient.ClientType;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Video streaming data. Fetching is tied to the behavior YT uses,
|
||||||
|
* where this class fetches the streams only when YT fetches.
|
||||||
|
* <p>
|
||||||
|
* Effectively the cache expiration of these fetches is the same as the stock app,
|
||||||
|
* since the stock app would not use expired streams and therefor
|
||||||
|
* the extension replace stream hook is called only if YT
|
||||||
|
* did use its own client streams.
|
||||||
|
*/
|
||||||
public class StreamingDataRequest {
|
public class StreamingDataRequest {
|
||||||
private static final ClientType[] ALL_CLIENT_TYPES = ClientType.values();
|
|
||||||
private static final ClientType[] CLIENT_ORDER_TO_USE;
|
private static final ClientType[] CLIENT_ORDER_TO_USE;
|
||||||
|
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
static {
|
private static final String[] REQUEST_HEADER_KEYS = {
|
||||||
ClientType preferredClient = Settings.SPOOF_STREAMING_DATA_TYPE.get();
|
AUTHORIZATION_HEADER, // Available only to logged-in users.
|
||||||
CLIENT_ORDER_TO_USE = new ClientType[ALL_CLIENT_TYPES.length];
|
"X-GOOG-API-FORMAT-VERSION",
|
||||||
|
"X-Goog-Visitor-Id"
|
||||||
CLIENT_ORDER_TO_USE[0] = preferredClient;
|
};
|
||||||
|
private static final ByteArrayFilterGroup liveStreams =
|
||||||
int i = 1;
|
new ByteArrayFilterGroup(
|
||||||
for (ClientType c : ALL_CLIENT_TYPES) {
|
BaseSettings.SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK,
|
||||||
if (c != preferredClient) {
|
"yt_live_broadcast",
|
||||||
CLIENT_ORDER_TO_USE[i++] = c;
|
"yt_premiere_broadcast"
|
||||||
}
|
);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ClientType lastSpoofedClientType;
|
private static ClientType lastSpoofedClientType;
|
||||||
|
|
||||||
public static String getLastSpoofedClientName() {
|
|
||||||
return lastSpoofedClientType == null
|
|
||||||
? "Unknown"
|
|
||||||
: lastSpoofedClientType.friendlyName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TCP connection and HTTP read timeout.
|
* TCP connection and HTTP read timeout.
|
||||||
*/
|
*/
|
||||||
private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000;
|
private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS}
|
* Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS}
|
||||||
*/
|
*/
|
||||||
private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000;
|
private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000;
|
||||||
|
|
||||||
@GuardedBy("itself")
|
|
||||||
private static final Map<String, StreamingDataRequest> cache = Collections.synchronizedMap(
|
private static final Map<String, StreamingDataRequest> cache = Collections.synchronizedMap(
|
||||||
new LinkedHashMap<>(100) {
|
new LinkedHashMap<>(100) {
|
||||||
/**
|
/**
|
||||||
@ -82,12 +78,43 @@ public class StreamingDataRequest {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
public static void fetchRequest(@NonNull String videoId, Map<String, String> fetchHeaders) {
|
public static String getLastSpoofedClientName() {
|
||||||
|
return lastSpoofedClientType == null
|
||||||
|
? "Unknown"
|
||||||
|
: lastSpoofedClientType.getFriendlyName();
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
ClientType[] allClientTypes = ClientType.values();
|
||||||
|
ClientType preferredClient = BaseSettings.SPOOF_STREAMING_DATA_TYPE.get();
|
||||||
|
|
||||||
|
CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length];
|
||||||
|
CLIENT_ORDER_TO_USE[0] = preferredClient;
|
||||||
|
|
||||||
|
int i = 1;
|
||||||
|
for (ClientType c : allClientTypes) {
|
||||||
|
if (c != preferredClient) {
|
||||||
|
CLIENT_ORDER_TO_USE[i++] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String videoId;
|
||||||
|
private final Future<ByteBuffer> future;
|
||||||
|
|
||||||
|
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
|
||||||
|
Objects.requireNonNull(playerHeaders);
|
||||||
|
this.videoId = videoId;
|
||||||
|
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void fetchRequest(String videoId, Map<String, String> fetchHeaders) {
|
||||||
|
// Always fetch, even if there is an existing request for the same video.
|
||||||
cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders));
|
cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static StreamingDataRequest getRequestForVideoId(@Nullable String videoId) {
|
public static StreamingDataRequest getRequestForVideoId(String videoId) {
|
||||||
return cache.get(videoId);
|
return cache.get(videoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,42 +122,6 @@ public class StreamingDataRequest {
|
|||||||
Logger.printInfo(() -> toastMessage, ex);
|
Logger.printInfo(() -> toastMessage, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Available only to logged in users.
|
|
||||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
|
||||||
|
|
||||||
private static final String[] REQUEST_HEADER_KEYS = {
|
|
||||||
AUTHORIZATION_HEADER,
|
|
||||||
"X-GOOG-API-FORMAT-VERSION",
|
|
||||||
"X-Goog-Visitor-Id"
|
|
||||||
};
|
|
||||||
|
|
||||||
private static void writeInnerTubeBody(HttpURLConnection connection, ClientType clientType,
|
|
||||||
String videoId, Map<String, String> playerHeaders) {
|
|
||||||
try {
|
|
||||||
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
|
||||||
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
|
||||||
|
|
||||||
if (playerHeaders != null) {
|
|
||||||
for (String key : REQUEST_HEADER_KEYS) {
|
|
||||||
if (!clientType.canLogin && key.equals(AUTHORIZATION_HEADER)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String value = playerHeaders.get(key);
|
|
||||||
if (value != null) {
|
|
||||||
connection.setRequestProperty(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String innerTubeBody = PlayerRoutes.createInnertubeBody(clientType, videoId);
|
|
||||||
byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
|
|
||||||
connection.setFixedLengthStreamingMode(requestBody.length);
|
|
||||||
connection.getOutputStream().write(requestBody);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
handleConnectionError("Network error", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static HttpURLConnection send(ClientType clientType, String videoId,
|
private static HttpURLConnection send(ClientType clientType, String videoId,
|
||||||
Map<String, String> playerHeaders) {
|
Map<String, String> playerHeaders) {
|
||||||
@ -139,19 +130,44 @@ public class StreamingDataRequest {
|
|||||||
Objects.requireNonNull(playerHeaders);
|
Objects.requireNonNull(playerHeaders);
|
||||||
|
|
||||||
final long startTime = System.currentTimeMillis();
|
final long startTime = System.currentTimeMillis();
|
||||||
String clientTypeName = clientType.name();
|
Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType);
|
||||||
Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
|
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType);
|
||||||
writeInnerTubeBody(connection, clientType, videoId, playerHeaders);
|
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||||
|
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);
|
||||||
|
|
||||||
|
for (String key : REQUEST_HEADER_KEYS) {
|
||||||
|
String value = playerHeaders.get(key);
|
||||||
|
if (value != null) {
|
||||||
|
if (key.equals(AUTHORIZATION_HEADER)) {
|
||||||
|
if (!clientType.canLogin) {
|
||||||
|
Logger.printDebug(() -> "Not including request header: " + key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.setRequestProperty(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId);
|
||||||
|
byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8);
|
||||||
|
connection.setFixedLengthStreamingMode(requestBody.length);
|
||||||
|
connection.getOutputStream().write(requestBody);
|
||||||
|
|
||||||
final int responseCode = connection.getResponseCode();
|
final int responseCode = connection.getResponseCode();
|
||||||
if (responseCode == 200) return connection;
|
if (responseCode == 200) return connection;
|
||||||
|
|
||||||
handleConnectionError(clientTypeName + " not available with response code: "
|
// This situation likely means the patches are outdated.
|
||||||
+ responseCode + " message: " + connection.getResponseMessage(),
|
// Use a toast message that suggests updating.
|
||||||
|
handleConnectionError("Playback error (App is outdated?) " + clientType + ": "
|
||||||
|
+ responseCode + " response: " + connection.getResponseMessage(),
|
||||||
null);
|
null);
|
||||||
|
} catch (SocketTimeoutException ex) {
|
||||||
|
handleConnectionError("Connection timeout", ex);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
handleConnectionError("Network error", ex);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "send failed", ex);
|
Logger.printException(() -> "send failed", ex);
|
||||||
} finally {
|
} finally {
|
||||||
@ -161,43 +177,39 @@ public class StreamingDataRequest {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final ByteArrayFilterGroup liveStreams =
|
private static ByteBuffer fetch(String videoId, Map<String, String> playerHeaders) {
|
||||||
new ByteArrayFilterGroup(
|
|
||||||
Settings.SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK,
|
|
||||||
"yt_live_broadcast",
|
|
||||||
"yt_premiere_broadcast"
|
|
||||||
);
|
|
||||||
|
|
||||||
private static ByteBuffer fetch(@NonNull String videoId, Map<String, String> playerHeaders) {
|
|
||||||
lastSpoofedClientType = null;
|
lastSpoofedClientType = null;
|
||||||
|
|
||||||
// Retry with different client if empty response body is received.
|
// Retry with different client if empty response body is received.
|
||||||
for (ClientType clientType : CLIENT_ORDER_TO_USE) {
|
for (ClientType clientType : CLIENT_ORDER_TO_USE) {
|
||||||
HttpURLConnection connection = send(clientType, videoId, playerHeaders);
|
HttpURLConnection connection = send(clientType, videoId, playerHeaders);
|
||||||
|
if (connection != null) {
|
||||||
|
try {
|
||||||
|
// gzip encoding doesn't response with content length (-1),
|
||||||
|
// but empty response body does.
|
||||||
|
if (connection.getContentLength() == 0) {
|
||||||
|
Logger.printDebug(() -> "Received empty response" + "\nClient: " + clientType + "\nVideo: " + videoId);
|
||||||
|
} else {
|
||||||
|
try (InputStream inputStream = new BufferedInputStream(connection.getInputStream());
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||||
|
|
||||||
// gzip encoding doesn't response with content length (-1),
|
byte[] buffer = new byte[4096];
|
||||||
// but empty response body does.
|
int bytesRead;
|
||||||
if (connection == null || connection.getContentLength() == 0)
|
while ((bytesRead = inputStream.read(buffer)) >= 0) {
|
||||||
continue;
|
baos.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
if (clientType == ClientType.IOS && liveStreams.check(buffer).isFiltered()) {
|
||||||
|
Logger.printDebug(() -> "Ignore IOS spoofing as it is a livestream (video: " + videoId + ")");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
lastSpoofedClientType = clientType;
|
||||||
|
|
||||||
try (
|
return ByteBuffer.wrap(baos.toByteArray());
|
||||||
InputStream inputStream = new BufferedInputStream(connection.getInputStream());
|
}
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream()
|
}
|
||||||
) {
|
} catch (IOException ex) {
|
||||||
byte[] buffer = new byte[2048];
|
Logger.printException(() -> "Fetch failed while processing response data", ex);
|
||||||
int bytesRead;
|
|
||||||
while ((bytesRead = inputStream.read(buffer)) >= 0) {
|
|
||||||
baos.write(buffer, 0, bytesRead);
|
|
||||||
}
|
}
|
||||||
if (clientType == ClientType.IOS && liveStreams.check(buffer).isFiltered()) {
|
|
||||||
Logger.printDebug(() -> "Ignore IOS spoofing as it is a livestream (video: " + videoId + ")");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
lastSpoofedClientType = clientType;
|
|
||||||
|
|
||||||
return ByteBuffer.wrap(baos.toByteArray());
|
|
||||||
} catch (IOException ex) {
|
|
||||||
Logger.printException(() -> "Fetch failed while processing response data", ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,15 +217,6 @@ public class StreamingDataRequest {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final String videoId;
|
|
||||||
private final Future<ByteBuffer> future;
|
|
||||||
|
|
||||||
private StreamingDataRequest(String videoId, Map<String, String> playerHeaders) {
|
|
||||||
Objects.requireNonNull(playerHeaders);
|
|
||||||
this.videoId = videoId;
|
|
||||||
this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean fetchCompleted() {
|
public boolean fetchCompleted() {
|
||||||
return future.isDone();
|
return future.isDone();
|
||||||
}
|
}
|
||||||
@ -239,4 +242,4 @@ public class StreamingDataRequest {
|
|||||||
public String toString() {
|
public String toString() {
|
||||||
return "StreamingDataRequest{" + "videoId='" + videoId + '\'' + '}';
|
return "StreamingDataRequest{" + "videoId='" + videoId + '\'' + '}';
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -11,9 +11,11 @@ import java.io.InputStreamReader;
|
|||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.utils.PackageUtils;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class Requester {
|
public class Requester {
|
||||||
public Requester() {
|
private Requester() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException {
|
public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException {
|
||||||
@ -27,7 +29,9 @@ public class Requester {
|
|||||||
// The calling code must set a length if using a request body.
|
// The calling code must set a length if using a request body.
|
||||||
connection.setFixedLengthStreamingMode(0);
|
connection.setFixedLengthStreamingMode(0);
|
||||||
connection.setRequestMethod(route.getMethod().name());
|
connection.setRequestMethod(route.getMethod().name());
|
||||||
connection.setRequestProperty("User-Agent", System.getProperty("http.agent") + ";");
|
String agentString = System.getProperty("http.agent")
|
||||||
|
+ "; RVX/" + PackageUtils.getAppVersionName();
|
||||||
|
connection.setRequestProperty("User-Agent", agentString);
|
||||||
|
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
@ -2,10 +2,10 @@ package app.revanced.extension.shared.requests;
|
|||||||
|
|
||||||
public class Route {
|
public class Route {
|
||||||
private final String route;
|
private final String route;
|
||||||
private final Route.Method method;
|
private final Method method;
|
||||||
private final int paramCount;
|
private final int paramCount;
|
||||||
|
|
||||||
public Route(Route.Method method, String route) {
|
public Route(Method method, String route) {
|
||||||
this.method = method;
|
this.method = method;
|
||||||
this.route = route;
|
this.route = route;
|
||||||
this.paramCount = countMatches(route, '{');
|
this.paramCount = countMatches(route, '{');
|
||||||
@ -14,11 +14,11 @@ public class Route {
|
|||||||
throw new IllegalArgumentException("Not enough parameters");
|
throw new IllegalArgumentException("Not enough parameters");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Route.Method getMethod() {
|
public Method getMethod() {
|
||||||
return method;
|
return method;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Route.CompiledRoute compile(String... params) {
|
public CompiledRoute compile(String... params) {
|
||||||
if (params.length != paramCount)
|
if (params.length != paramCount)
|
||||||
throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " +
|
throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " +
|
||||||
"Expected: " + paramCount + ", provided: " + params.length);
|
"Expected: " + paramCount + ", provided: " + params.length);
|
||||||
@ -29,21 +29,7 @@ public class Route {
|
|||||||
int paramEnd = compiledRoute.indexOf("}");
|
int paramEnd = compiledRoute.indexOf("}");
|
||||||
compiledRoute.replace(paramStart, paramEnd + 1, params[i]);
|
compiledRoute.replace(paramStart, paramEnd + 1, params[i]);
|
||||||
}
|
}
|
||||||
return new Route.CompiledRoute(this, compiledRoute.toString());
|
return new CompiledRoute(this, compiledRoute.toString());
|
||||||
}
|
|
||||||
|
|
||||||
private int countMatches(CharSequence seq, char c) {
|
|
||||||
int count = 0;
|
|
||||||
for (int i = 0; i < seq.length(); i++) {
|
|
||||||
if (seq.charAt(i) == c)
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Method {
|
|
||||||
GET,
|
|
||||||
POST
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class CompiledRoute {
|
public static class CompiledRoute {
|
||||||
@ -59,8 +45,22 @@ public class Route {
|
|||||||
return compiledRoute;
|
return compiledRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Route.Method getMethod() {
|
public Method getMethod() {
|
||||||
return baseRoute.method;
|
return baseRoute.method;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int countMatches(CharSequence seq, char c) {
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 0; i < seq.length(); i++) {
|
||||||
|
if (seq.charAt(i) == c)
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Method {
|
||||||
|
GET,
|
||||||
|
POST
|
||||||
|
}
|
||||||
}
|
}
|
@ -4,8 +4,10 @@ import static java.lang.Boolean.FALSE;
|
|||||||
import static java.lang.Boolean.TRUE;
|
import static java.lang.Boolean.TRUE;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.patches.PatchStatus.HideFullscreenAdsDefaultBoolean;
|
import static app.revanced.extension.shared.patches.PatchStatus.HideFullscreenAdsDefaultBoolean;
|
||||||
|
import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingDataDefaultClient;
|
||||||
|
|
||||||
import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat;
|
import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat;
|
||||||
|
import app.revanced.extension.shared.patches.client.AppClient.ClientType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings shared across multiple apps.
|
* Settings shared across multiple apps.
|
||||||
@ -35,6 +37,11 @@ public class BaseSettings {
|
|||||||
public static final EnumSetting<DisplayFormat> RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT = new EnumSetting<>("revanced_return_youtube_username_display_format", DisplayFormat.USERNAME_ONLY, true);
|
public static final EnumSetting<DisplayFormat> RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT = new EnumSetting<>("revanced_return_youtube_username_display_format", DisplayFormat.USERNAME_ONLY, true);
|
||||||
public static final StringSetting RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY = new StringSetting("revanced_return_youtube_username_youtube_data_api_v3_developer_key", "", true, false);
|
public static final StringSetting RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY = new StringSetting("revanced_return_youtube_username_youtube_data_api_v3_developer_key", "", true, false);
|
||||||
|
|
||||||
|
public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", TRUE, true, "revanced_spoof_streaming_data_user_dialog_message");
|
||||||
|
public static final EnumSetting<ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", SpoofStreamingDataDefaultClient(), true);
|
||||||
|
public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK = new BooleanSetting("revanced_spoof_streaming_data_ios_skip_livestream_playback", TRUE, true);
|
||||||
|
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @noinspection DeprecatedIsStillUsed
|
* @noinspection DeprecatedIsStillUsed
|
||||||
*/
|
*/
|
||||||
|
@ -9,8 +9,8 @@ import org.apache.commons.lang3.BooleanUtils;
|
|||||||
|
|
||||||
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.misc.requests.PlaylistRequest;
|
|
||||||
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
||||||
|
import app.revanced.extension.youtube.patches.video.requests.PlaylistRequest;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||||
import app.revanced.extension.youtube.whitelist.Whitelist;
|
import app.revanced.extension.youtube.whitelist.Whitelist;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package app.revanced.extension.youtube.patches.misc.requests;
|
package app.revanced.extension.youtube.patches.video.requests;
|
||||||
|
|
||||||
import static app.revanced.extension.youtube.patches.misc.requests.PlayerRoutes.GET_PLAYLIST_PAGE;
|
import static app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_PLAYLIST_PAGE;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ import java.net.HttpURLConnection;
|
|||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
@ -23,10 +24,11 @@ import java.util.concurrent.Future;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.patches.client.AppClient.ClientType;
|
||||||
|
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes;
|
||||||
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.misc.client.AppClient.ClientType;
|
|
||||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||||
|
|
||||||
public class PlaylistRequest {
|
public class PlaylistRequest {
|
||||||
@ -82,8 +84,9 @@ public class PlaylistRequest {
|
|||||||
try {
|
try {
|
||||||
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_PLAYLIST_PAGE, clientType);
|
HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_PLAYLIST_PAGE, clientType);
|
||||||
|
|
||||||
String innerTubeBody = PlayerRoutes.createInnertubeBody(
|
String innerTubeBody = String.format(
|
||||||
clientType,
|
Locale.ENGLISH,
|
||||||
|
PlayerRoutes.createInnertubeBody(clientType, true),
|
||||||
videoId,
|
videoId,
|
||||||
"RD" + videoId
|
"RD" + videoId
|
||||||
);
|
);
|
@ -37,9 +37,7 @@ import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch.Start
|
|||||||
import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch.FormFactor;
|
import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch.FormFactor;
|
||||||
import app.revanced.extension.youtube.patches.general.MiniplayerPatch;
|
import app.revanced.extension.youtube.patches.general.MiniplayerPatch;
|
||||||
import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch;
|
import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch;
|
||||||
import app.revanced.extension.youtube.patches.misc.SpoofStreamingDataPatch;
|
|
||||||
import app.revanced.extension.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType;
|
import app.revanced.extension.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType;
|
||||||
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
|
|
||||||
import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
|
import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
|
||||||
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
||||||
import app.revanced.extension.youtube.shared.PlaylistIdPrefix;
|
import app.revanced.extension.youtube.shared.PlaylistIdPrefix;
|
||||||
@ -563,11 +561,6 @@ public class Settings extends BaseSettings {
|
|||||||
public static final EnumSetting<WatchHistoryType> WATCH_HISTORY_TYPE = new EnumSetting<>("revanced_watch_history_type", WatchHistoryType.REPLACE);
|
public static final EnumSetting<WatchHistoryType> WATCH_HISTORY_TYPE = new EnumSetting<>("revanced_watch_history_type", WatchHistoryType.REPLACE);
|
||||||
|
|
||||||
// PreferenceScreen: Miscellaneous - Spoof streaming data
|
// PreferenceScreen: Miscellaneous - Spoof streaming data
|
||||||
// The order of the settings should not be changed otherwise the app may crash
|
|
||||||
public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", TRUE, true, "revanced_spoof_streaming_data_user_dialog_message");
|
|
||||||
public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK = new BooleanSetting("revanced_spoof_streaming_data_ios_skip_livestream_playback", TRUE, true, new SpoofStreamingDataPatch.iOSAvailability());
|
|
||||||
public static final EnumSetting<ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.IOS, true, parent(SPOOF_STREAMING_DATA));
|
|
||||||
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_STREAMING_DATA));
|
|
||||||
|
|
||||||
// PreferenceScreen: Return YouTube Dislike
|
// PreferenceScreen: Return YouTube Dislike
|
||||||
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("ryd_enabled", TRUE);
|
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("ryd_enabled", TRUE);
|
||||||
|
@ -8,9 +8,9 @@ import android.preference.Preference;
|
|||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.patches.client.AppClient.ClientType;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.utils.Utils;
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings({"deprecation", "unused"})
|
@SuppressWarnings({"deprecation", "unused"})
|
||||||
@ -74,5 +74,6 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
|
|||||||
|
|
||||||
setSummary(str(summaryTextKey));
|
setSummary(str(summaryTextKey));
|
||||||
setEnabled(Settings.SPOOF_STREAMING_DATA.get());
|
setEnabled(Settings.SPOOF_STREAMING_DATA.get());
|
||||||
|
setSelectable(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -46,6 +46,7 @@ private const val CLIENT_INFO_CLASS_DESCRIPTOR =
|
|||||||
val spoofClientPatch = bytecodePatch(
|
val spoofClientPatch = bytecodePatch(
|
||||||
SPOOF_CLIENT.title,
|
SPOOF_CLIENT.title,
|
||||||
SPOOF_CLIENT.summary,
|
SPOOF_CLIENT.summary,
|
||||||
|
false,
|
||||||
) {
|
) {
|
||||||
dependsOn(settingsPatch)
|
dependsOn(settingsPatch)
|
||||||
|
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
package app.revanced.patches.music.utils.fix.streamingdata
|
||||||
|
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||||
|
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||||
|
import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME
|
||||||
|
import app.revanced.patches.music.utils.patch.PatchList.SPOOF_STREAMING_DATA
|
||||||
|
import app.revanced.patches.music.utils.settings.CategoryType
|
||||||
|
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
|
||||||
|
import app.revanced.patches.music.utils.settings.addSwitchPreference
|
||||||
|
import app.revanced.patches.music.utils.settings.settingsPatch
|
||||||
|
import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
|
||||||
|
import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch
|
||||||
|
import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch
|
||||||
|
import app.revanced.util.findMethodOrThrow
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||||
|
|
||||||
|
private const val DEFAULT_CLIENT_TYPE = "ANDROID_VR"
|
||||||
|
|
||||||
|
val spoofStreamingDataPatch = baseSpoofStreamingDataPatch(
|
||||||
|
{
|
||||||
|
compatibleWith(COMPATIBLE_PACKAGE)
|
||||||
|
|
||||||
|
dependsOn(
|
||||||
|
baseSpoofUserAgentPatch(YOUTUBE_MUSIC_PACKAGE_NAME),
|
||||||
|
settingsPatch,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
findMethodOrThrow("$PATCHES_PATH/PatchStatus;") {
|
||||||
|
name == "SpoofStreamingDataDefaultClient"
|
||||||
|
}.apply {
|
||||||
|
val register = getInstruction<OneRegisterInstruction>(0).registerA
|
||||||
|
val type = (getInstruction<ReferenceInstruction>(0).reference as FieldReference).type
|
||||||
|
replaceInstruction(
|
||||||
|
0,
|
||||||
|
"sget-object v$register, $type->$DEFAULT_CLIENT_TYPE:$type"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addSwitchPreference(
|
||||||
|
CategoryType.MISC,
|
||||||
|
"revanced_spoof_streaming_data",
|
||||||
|
"true"
|
||||||
|
)
|
||||||
|
addSwitchPreference(
|
||||||
|
CategoryType.MISC,
|
||||||
|
"revanced_spoof_streaming_data_stats_for_nerds",
|
||||||
|
"true",
|
||||||
|
"revanced_spoof_streaming_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
updatePatchStatus(SPOOF_STREAMING_DATA)
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
@ -143,7 +143,11 @@ internal enum class PatchList(
|
|||||||
),
|
),
|
||||||
SPOOF_CLIENT(
|
SPOOF_CLIENT(
|
||||||
"Spoof client",
|
"Spoof client",
|
||||||
"Adds options to spoof the client to allow track playback."
|
"Adds options to spoof the client to allow playback."
|
||||||
|
),
|
||||||
|
SPOOF_STREAMING_DATA(
|
||||||
|
"Spoof streaming data",
|
||||||
|
"Adds options to spoof the streaming data to allow playback."
|
||||||
),
|
),
|
||||||
TRANSLATIONS_FOR_YOUTUBE_MUSIC(
|
TRANSLATIONS_FOR_YOUTUBE_MUSIC(
|
||||||
"Translations for YouTube Music",
|
"Translations for YouTube Music",
|
||||||
|
@ -6,6 +6,7 @@ internal object Constants {
|
|||||||
const val PATCHES_PATH = "$EXTENSION_PATH/patches"
|
const val PATCHES_PATH = "$EXTENSION_PATH/patches"
|
||||||
const val COMPONENTS_PATH = "$PATCHES_PATH/components"
|
const val COMPONENTS_PATH = "$PATCHES_PATH/components"
|
||||||
const val SPANS_PATH = "$PATCHES_PATH/spans"
|
const val SPANS_PATH = "$PATCHES_PATH/spans"
|
||||||
|
const val SPOOF_PATH = "$PATCHES_PATH/spoof"
|
||||||
|
|
||||||
const val EXTENSION_UTILS_PATH = "$EXTENSION_PATH/utils"
|
const val EXTENSION_UTILS_PATH = "$EXTENSION_PATH/utils"
|
||||||
const val EXTENSION_SETTING_CLASS_DESCRIPTOR = "$EXTENSION_PATH/settings/Setting;"
|
const val EXTENSION_SETTING_CLASS_DESCRIPTOR = "$EXTENSION_PATH/settings/Setting;"
|
||||||
|
@ -0,0 +1,215 @@
|
|||||||
|
package app.revanced.patches.shared.spoof.streamingdata
|
||||||
|
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
|
import app.revanced.patcher.extensions.InstructionExtensions.instructions
|
||||||
|
import app.revanced.patcher.patch.BytecodePatchBuilder
|
||||||
|
import app.revanced.patcher.patch.BytecodePatchContext
|
||||||
|
import app.revanced.patcher.patch.PatchException
|
||||||
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
import app.revanced.patcher.util.smali.ExternalLabel
|
||||||
|
import app.revanced.patches.shared.extension.Constants.SPOOF_PATH
|
||||||
|
import app.revanced.util.findInstructionIndicesReversedOrThrow
|
||||||
|
import app.revanced.util.fingerprint.definingClassOrThrow
|
||||||
|
import app.revanced.util.fingerprint.matchOrThrow
|
||||||
|
import app.revanced.util.fingerprint.methodOrThrow
|
||||||
|
import app.revanced.util.getReference
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||||
|
|
||||||
|
const val EXTENSION_CLASS_DESCRIPTOR =
|
||||||
|
"$SPOOF_PATH/SpoofStreamingDataPatch;"
|
||||||
|
|
||||||
|
fun baseSpoofStreamingDataPatch(
|
||||||
|
block: BytecodePatchBuilder.() -> Unit = {},
|
||||||
|
executeBlock: BytecodePatchContext.() -> Unit = {},
|
||||||
|
) = bytecodePatch(
|
||||||
|
name = "Spoof streaming data",
|
||||||
|
description = "Adds options to spoof the streaming data to allow playback."
|
||||||
|
) {
|
||||||
|
block()
|
||||||
|
|
||||||
|
execute {
|
||||||
|
// region Block /initplayback requests to fall back to /get_watch requests.
|
||||||
|
|
||||||
|
buildInitPlaybackRequestFingerprint.matchOrThrow().let {
|
||||||
|
it.method.apply {
|
||||||
|
val moveUriStringIndex = it.patternMatch!!.startIndex
|
||||||
|
val targetRegister =
|
||||||
|
getInstruction<OneRegisterInstruction>(moveUriStringIndex).registerA
|
||||||
|
|
||||||
|
addInstructions(
|
||||||
|
moveUriStringIndex + 1,
|
||||||
|
"""
|
||||||
|
invoke-static { v$targetRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockInitPlaybackRequest(Ljava/lang/String;)Ljava/lang/String;
|
||||||
|
move-result-object v$targetRegister
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Block /get_watch requests to fall back to /player requests.
|
||||||
|
|
||||||
|
buildPlayerRequestURIFingerprint.methodOrThrow().apply {
|
||||||
|
val invokeToStringIndex = indexOfToStringInstruction(this)
|
||||||
|
val uriRegister =
|
||||||
|
getInstruction<FiveRegisterInstruction>(invokeToStringIndex).registerC
|
||||||
|
|
||||||
|
addInstructions(
|
||||||
|
invokeToStringIndex,
|
||||||
|
"""
|
||||||
|
invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri;
|
||||||
|
move-result-object v$uriRegister
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Get replacement streams at player requests.
|
||||||
|
|
||||||
|
buildRequestFingerprint.methodOrThrow().apply {
|
||||||
|
val newRequestBuilderIndex = indexOfNewUrlRequestBuilderInstruction(this)
|
||||||
|
val urlRegister =
|
||||||
|
getInstruction<FiveRegisterInstruction>(newRequestBuilderIndex).registerD
|
||||||
|
|
||||||
|
val entrySetIndex = indexOfEntrySetInstruction(this)
|
||||||
|
val mapRegister = if (entrySetIndex < 0)
|
||||||
|
urlRegister + 1
|
||||||
|
else
|
||||||
|
getInstruction<FiveRegisterInstruction>(entrySetIndex).registerC
|
||||||
|
|
||||||
|
var smaliInstructions =
|
||||||
|
"invoke-static { v$urlRegister, v$mapRegister }, " +
|
||||||
|
"$EXTENSION_CLASS_DESCRIPTOR->" +
|
||||||
|
"fetchStreams(Ljava/lang/String;Ljava/util/Map;)V"
|
||||||
|
|
||||||
|
if (entrySetIndex < 0) smaliInstructions = """
|
||||||
|
move-object/from16 v$mapRegister, p1
|
||||||
|
|
||||||
|
""" + smaliInstructions
|
||||||
|
|
||||||
|
// Copy request headers for streaming data fetch.
|
||||||
|
addInstructions(newRequestBuilderIndex + 2, smaliInstructions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Replace the streaming data.
|
||||||
|
|
||||||
|
createStreamingDataFingerprint.matchOrThrow(createStreamingDataParentFingerprint).let { result ->
|
||||||
|
result.method.apply {
|
||||||
|
val setStreamingDataIndex = result.patternMatch!!.startIndex
|
||||||
|
val setStreamingDataField =
|
||||||
|
getInstruction(setStreamingDataIndex).getReference<FieldReference>().toString()
|
||||||
|
|
||||||
|
val playerProtoClass =
|
||||||
|
getInstruction(setStreamingDataIndex + 1).getReference<FieldReference>()!!.definingClass
|
||||||
|
val protobufClass =
|
||||||
|
protobufClassParseByteBufferFingerprint.definingClassOrThrow()
|
||||||
|
|
||||||
|
val getStreamingDataField = instructions.find { instruction ->
|
||||||
|
instruction.opcode == Opcode.IGET_OBJECT &&
|
||||||
|
instruction.getReference<FieldReference>()?.definingClass == playerProtoClass
|
||||||
|
}?.getReference<FieldReference>()
|
||||||
|
?: throw PatchException("Could not find getStreamingDataField")
|
||||||
|
|
||||||
|
val videoDetailsIndex = result.patternMatch!!.endIndex
|
||||||
|
val videoDetailsClass =
|
||||||
|
getInstruction(videoDetailsIndex).getReference<FieldReference>()!!.type
|
||||||
|
|
||||||
|
val insertIndex = videoDetailsIndex + 1
|
||||||
|
val videoDetailsRegister =
|
||||||
|
getInstruction<TwoRegisterInstruction>(videoDetailsIndex).registerA
|
||||||
|
|
||||||
|
val overrideRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerA
|
||||||
|
val freeRegister = implementation!!.registerCount - parameters.size - 2
|
||||||
|
|
||||||
|
addInstructionsWithLabels(
|
||||||
|
insertIndex,
|
||||||
|
"""
|
||||||
|
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z
|
||||||
|
move-result v$freeRegister
|
||||||
|
if-eqz v$freeRegister, :disabled
|
||||||
|
|
||||||
|
# Get video id.
|
||||||
|
# From YouTube 17.34.36 to YouTube 19.16.39, the field names and field types are the same.
|
||||||
|
iget-object v$freeRegister, v$videoDetailsRegister, $videoDetailsClass->c:Ljava/lang/String;
|
||||||
|
if-eqz v$freeRegister, :disabled
|
||||||
|
|
||||||
|
# Get streaming data.
|
||||||
|
invoke-static { v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
|
||||||
|
move-result-object v$freeRegister
|
||||||
|
if-eqz v$freeRegister, :disabled
|
||||||
|
|
||||||
|
# Parse streaming data.
|
||||||
|
sget-object v$overrideRegister, $playerProtoClass->a:$playerProtoClass
|
||||||
|
invoke-static { v$overrideRegister, v$freeRegister }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
|
||||||
|
move-result-object v$freeRegister
|
||||||
|
check-cast v$freeRegister, $playerProtoClass
|
||||||
|
|
||||||
|
# Set streaming data.
|
||||||
|
iget-object v$freeRegister, v$freeRegister, $getStreamingDataField
|
||||||
|
if-eqz v$freeRegister, :disabled
|
||||||
|
iput-object v$freeRegister, p0, $setStreamingDataField
|
||||||
|
|
||||||
|
""",
|
||||||
|
ExternalLabel("disabled", getInstruction(insertIndex))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Remove /videoplayback request body to fix playback.
|
||||||
|
// This is needed when using iOS client as streaming data source.
|
||||||
|
|
||||||
|
buildMediaDataSourceFingerprint.methodOrThrow().apply {
|
||||||
|
val targetIndex = instructions.lastIndex
|
||||||
|
|
||||||
|
addInstructions(
|
||||||
|
targetIndex,
|
||||||
|
"""
|
||||||
|
# Field a: Stream uri.
|
||||||
|
# Field c: Http method.
|
||||||
|
# Field d: Post data.
|
||||||
|
move-object/from16 v0, p0
|
||||||
|
iget-object v1, v0, $definingClass->a:Landroid/net/Uri;
|
||||||
|
iget v2, v0, $definingClass->c:I
|
||||||
|
iget-object v3, v0, $definingClass->d:[B
|
||||||
|
invoke-static { v1, v2, v3 }, $EXTENSION_CLASS_DESCRIPTOR->removeVideoPlaybackPostBody(Landroid/net/Uri;I[B)[B
|
||||||
|
move-result-object v1
|
||||||
|
iput-object v1, v0, $definingClass->d:[B
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Append spoof info.
|
||||||
|
|
||||||
|
nerdsStatsVideoFormatBuilderFingerprint.methodOrThrow().apply {
|
||||||
|
findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index ->
|
||||||
|
val register = getInstruction<OneRegisterInstruction>(index).registerA
|
||||||
|
|
||||||
|
addInstructions(
|
||||||
|
index, """
|
||||||
|
invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->appendSpoofedClient(Ljava/lang/String;)Ljava/lang/String;
|
||||||
|
move-result-object v$register
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
executeBlock()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package app.revanced.patches.youtube.utils.fix.streamingdata
|
package app.revanced.patches.shared.spoof.streamingdata
|
||||||
|
|
||||||
import app.revanced.util.fingerprint.legacyFingerprint
|
import app.revanced.util.fingerprint.legacyFingerprint
|
||||||
import app.revanced.util.getReference
|
import app.revanced.util.getReference
|
||||||
@ -10,38 +10,6 @@ import com.android.tools.smali.dexlib2.iface.Method
|
|||||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||||
|
|
||||||
internal val buildBrowseRequestFingerprint = legacyFingerprint(
|
|
||||||
name = "buildBrowseRequestFingerprint",
|
|
||||||
customFingerprint = { method, _ ->
|
|
||||||
method.implementation != null &&
|
|
||||||
indexOfRequestFinishedListenerInstruction(method) >= 0 &&
|
|
||||||
!method.definingClass.startsWith("Lorg/") &&
|
|
||||||
indexOfNewUrlRequestBuilderInstruction(method) >= 0 &&
|
|
||||||
// YouTube 17.34.36 ~ YouTube 18.35.36
|
|
||||||
(indexOfEntrySetInstruction(method) >= 0 ||
|
|
||||||
// YouTube 18.36.39 ~
|
|
||||||
method.parameters[1].type == "Ljava/util/Map;")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
internal fun indexOfRequestFinishedListenerInstruction(method: Method) =
|
|
||||||
method.indexOfFirstInstruction {
|
|
||||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
|
||||||
getReference<MethodReference>()?.name == "setRequestFinishedListener"
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun indexOfNewUrlRequestBuilderInstruction(method: Method) =
|
|
||||||
method.indexOfFirstInstruction {
|
|
||||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
|
||||||
getReference<MethodReference>().toString() == "Lorg/chromium/net/CronetEngine;->newUrlRequestBuilder(Ljava/lang/String;Lorg/chromium/net/UrlRequest${'$'}Callback;Ljava/util/concurrent/Executor;)Lorg/chromium/net/UrlRequest${'$'}Builder;"
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun indexOfEntrySetInstruction(method: Method) =
|
|
||||||
method.indexOfFirstInstruction {
|
|
||||||
opcode == Opcode.INVOKE_INTERFACE &&
|
|
||||||
getReference<MethodReference>().toString() == "Ljava/util/Map;->entrySet()Ljava/util/Set;"
|
|
||||||
}
|
|
||||||
|
|
||||||
internal val buildInitPlaybackRequestFingerprint = legacyFingerprint(
|
internal val buildInitPlaybackRequestFingerprint = legacyFingerprint(
|
||||||
name = "buildInitPlaybackRequestFingerprint",
|
name = "buildInitPlaybackRequestFingerprint",
|
||||||
returnType = "Lorg/chromium/net/UrlRequest\$Builder;",
|
returnType = "Lorg/chromium/net/UrlRequest\$Builder;",
|
||||||
@ -91,6 +59,38 @@ internal fun indexOfToStringInstruction(method: Method) =
|
|||||||
getReference<MethodReference>().toString() == "Landroid/net/Uri;->toString()Ljava/lang/String;"
|
getReference<MethodReference>().toString() == "Landroid/net/Uri;->toString()Ljava/lang/String;"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal val buildRequestFingerprint = legacyFingerprint(
|
||||||
|
name = "buildRequestFingerprint",
|
||||||
|
customFingerprint = { method, _ ->
|
||||||
|
method.implementation != null &&
|
||||||
|
indexOfRequestFinishedListenerInstruction(method) >= 0 &&
|
||||||
|
!method.definingClass.startsWith("Lorg/") &&
|
||||||
|
indexOfNewUrlRequestBuilderInstruction(method) >= 0 &&
|
||||||
|
// Earlier targets
|
||||||
|
(indexOfEntrySetInstruction(method) >= 0 ||
|
||||||
|
// Later targets
|
||||||
|
method.parameters[1].type == "Ljava/util/Map;")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun indexOfRequestFinishedListenerInstruction(method: Method) =
|
||||||
|
method.indexOfFirstInstruction {
|
||||||
|
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||||
|
getReference<MethodReference>()?.name == "setRequestFinishedListener"
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun indexOfNewUrlRequestBuilderInstruction(method: Method) =
|
||||||
|
method.indexOfFirstInstruction {
|
||||||
|
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||||
|
getReference<MethodReference>().toString() == "Lorg/chromium/net/CronetEngine;->newUrlRequestBuilder(Ljava/lang/String;Lorg/chromium/net/UrlRequest${'$'}Callback;Ljava/util/concurrent/Executor;)Lorg/chromium/net/UrlRequest${'$'}Builder;"
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun indexOfEntrySetInstruction(method: Method) =
|
||||||
|
method.indexOfFirstInstruction {
|
||||||
|
opcode == Opcode.INVOKE_INTERFACE &&
|
||||||
|
getReference<MethodReference>().toString() == "Ljava/util/Map;->entrySet()Ljava/util/Set;"
|
||||||
|
}
|
||||||
|
|
||||||
internal val createStreamingDataFingerprint = legacyFingerprint(
|
internal val createStreamingDataFingerprint = legacyFingerprint(
|
||||||
name = "createStreamingDataFingerprint",
|
name = "createStreamingDataFingerprint",
|
||||||
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
|
accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
|
||||||
@ -103,19 +103,21 @@ internal val createStreamingDataFingerprint = legacyFingerprint(
|
|||||||
Opcode.SGET_OBJECT,
|
Opcode.SGET_OBJECT,
|
||||||
Opcode.IPUT_OBJECT
|
Opcode.IPUT_OBJECT
|
||||||
),
|
),
|
||||||
customFingerprint = { method, _ ->
|
)
|
||||||
method.indexOfFirstInstruction {
|
|
||||||
opcode == Opcode.SGET_OBJECT &&
|
internal val createStreamingDataParentFingerprint = legacyFingerprint(
|
||||||
getReference<FieldReference>()?.name == "playerThreedRenderer"
|
name = "createStreamingDataParentFingerprint",
|
||||||
} >= 0
|
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
|
||||||
},
|
returnType = "L",
|
||||||
|
parameters = emptyList(),
|
||||||
|
strings = listOf("Invalid playback type; streaming data is not playable"),
|
||||||
)
|
)
|
||||||
|
|
||||||
internal val nerdsStatsVideoFormatBuilderFingerprint = legacyFingerprint(
|
internal val nerdsStatsVideoFormatBuilderFingerprint = legacyFingerprint(
|
||||||
name = "nerdsStatsVideoFormatBuilderFingerprint",
|
name = "nerdsStatsVideoFormatBuilderFingerprint",
|
||||||
returnType = "Ljava/lang/String;",
|
returnType = "Ljava/lang/String;",
|
||||||
accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC,
|
accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC,
|
||||||
parameters = listOf("Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;"),
|
parameters = listOf("L"),
|
||||||
strings = listOf("codecs=\""),
|
strings = listOf("codecs=\""),
|
||||||
)
|
)
|
||||||
|
|
@ -1,222 +1,23 @@
|
|||||||
package app.revanced.patches.youtube.utils.fix.streamingdata
|
package app.revanced.patches.youtube.utils.fix.streamingdata
|
||||||
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.instructions
|
|
||||||
import app.revanced.patcher.patch.PatchException
|
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
|
||||||
import app.revanced.patcher.util.smali.ExternalLabel
|
|
||||||
import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch
|
import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch
|
||||||
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||||
import app.revanced.patches.youtube.utils.extension.Constants.MISC_PATH
|
import app.revanced.patches.youtube.utils.compatibility.Constants.YOUTUBE_PACKAGE_NAME
|
||||||
import app.revanced.patches.youtube.utils.patch.PatchList.SPOOF_STREAMING_DATA
|
import app.revanced.patches.youtube.utils.patch.PatchList.SPOOF_STREAMING_DATA
|
||||||
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
|
import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
|
||||||
import app.revanced.patches.youtube.utils.settings.settingsPatch
|
import app.revanced.patches.youtube.utils.settings.settingsPatch
|
||||||
import app.revanced.util.findInstructionIndicesReversedOrThrow
|
|
||||||
import app.revanced.util.fingerprint.definingClassOrThrow
|
|
||||||
import app.revanced.util.fingerprint.matchOrThrow
|
|
||||||
import app.revanced.util.fingerprint.methodOrThrow
|
|
||||||
import app.revanced.util.getReference
|
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
|
||||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
|
||||||
|
|
||||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
val spoofStreamingDataPatch = baseSpoofStreamingDataPatch(
|
||||||
"$MISC_PATH/SpoofStreamingDataPatch;"
|
{
|
||||||
|
compatibleWith(COMPATIBLE_PACKAGE)
|
||||||
val spoofStreamingDataPatch = bytecodePatch(
|
|
||||||
SPOOF_STREAMING_DATA.title,
|
|
||||||
SPOOF_STREAMING_DATA.summary,
|
|
||||||
) {
|
|
||||||
compatibleWith(COMPATIBLE_PACKAGE)
|
|
||||||
|
|
||||||
dependsOn(
|
|
||||||
baseSpoofUserAgentPatch("com.google.android.youtube"),
|
|
||||||
settingsPatch
|
|
||||||
)
|
|
||||||
|
|
||||||
execute {
|
|
||||||
// region Block /get_watch requests to fall back to /player requests.
|
|
||||||
|
|
||||||
buildPlayerRequestURIFingerprint.methodOrThrow().apply {
|
|
||||||
val invokeToStringIndex = indexOfToStringInstruction(this)
|
|
||||||
val uriRegister =
|
|
||||||
getInstruction<FiveRegisterInstruction>(invokeToStringIndex).registerC
|
|
||||||
|
|
||||||
addInstructions(
|
|
||||||
invokeToStringIndex,
|
|
||||||
"""
|
|
||||||
invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri;
|
|
||||||
move-result-object v$uriRegister
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Block /initplayback requests to fall back to /get_watch requests.
|
|
||||||
|
|
||||||
buildInitPlaybackRequestFingerprint.matchOrThrow().let {
|
|
||||||
it.method.apply {
|
|
||||||
val moveUriStringIndex = it.patternMatch!!.startIndex
|
|
||||||
val targetRegister =
|
|
||||||
getInstruction<OneRegisterInstruction>(moveUriStringIndex).registerA
|
|
||||||
|
|
||||||
addInstructions(
|
|
||||||
moveUriStringIndex + 1,
|
|
||||||
"""
|
|
||||||
invoke-static { v$targetRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockInitPlaybackRequest(Ljava/lang/String;)Ljava/lang/String;
|
|
||||||
move-result-object v$targetRegister
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Fetch replacement streams.
|
|
||||||
|
|
||||||
buildBrowseRequestFingerprint.methodOrThrow().apply {
|
|
||||||
val newRequestBuilderIndex = indexOfNewUrlRequestBuilderInstruction(this)
|
|
||||||
val urlRegister =
|
|
||||||
getInstruction<FiveRegisterInstruction>(newRequestBuilderIndex).registerD
|
|
||||||
|
|
||||||
val entrySetIndex = indexOfEntrySetInstruction(this)
|
|
||||||
val mapRegister = if (entrySetIndex < 0)
|
|
||||||
urlRegister + 1
|
|
||||||
else
|
|
||||||
getInstruction<FiveRegisterInstruction>(entrySetIndex).registerC
|
|
||||||
|
|
||||||
var smaliInstructions =
|
|
||||||
"invoke-static { v$urlRegister, v$mapRegister }, " +
|
|
||||||
"$EXTENSION_CLASS_DESCRIPTOR->" +
|
|
||||||
"fetchStreams(Ljava/lang/String;Ljava/util/Map;)V"
|
|
||||||
|
|
||||||
if (entrySetIndex < 0) smaliInstructions = """
|
|
||||||
move-object/from16 v$mapRegister, p1
|
|
||||||
|
|
||||||
""" + smaliInstructions
|
|
||||||
|
|
||||||
// Copy request headers for streaming data fetch.
|
|
||||||
addInstructions(newRequestBuilderIndex + 2, smaliInstructions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Replace the streaming data.
|
|
||||||
|
|
||||||
createStreamingDataFingerprint.matchOrThrow().let { result ->
|
|
||||||
result.method.apply {
|
|
||||||
val setStreamingDataIndex = result.patternMatch!!.startIndex
|
|
||||||
val setStreamingDataField =
|
|
||||||
getInstruction(setStreamingDataIndex).getReference<FieldReference>().toString()
|
|
||||||
|
|
||||||
val playerProtoClass =
|
|
||||||
getInstruction(setStreamingDataIndex + 1).getReference<FieldReference>()!!.definingClass
|
|
||||||
val protobufClass =
|
|
||||||
protobufClassParseByteBufferFingerprint.definingClassOrThrow()
|
|
||||||
|
|
||||||
val getStreamingDataField = instructions.find { instruction ->
|
|
||||||
instruction.opcode == Opcode.IGET_OBJECT &&
|
|
||||||
instruction.getReference<FieldReference>()?.definingClass == playerProtoClass
|
|
||||||
}?.getReference<FieldReference>()
|
|
||||||
?: throw PatchException("Could not find getStreamingDataField")
|
|
||||||
|
|
||||||
val videoDetailsIndex = result.patternMatch!!.endIndex
|
|
||||||
val videoDetailsClass =
|
|
||||||
getInstruction(videoDetailsIndex).getReference<FieldReference>()!!.type
|
|
||||||
|
|
||||||
val insertIndex = videoDetailsIndex + 1
|
|
||||||
val videoDetailsRegister =
|
|
||||||
getInstruction<TwoRegisterInstruction>(videoDetailsIndex).registerA
|
|
||||||
|
|
||||||
val overrideRegister = getInstruction<TwoRegisterInstruction>(insertIndex).registerA
|
|
||||||
val freeRegister = implementation!!.registerCount - parameters.size - 2
|
|
||||||
|
|
||||||
addInstructionsWithLabels(
|
|
||||||
insertIndex,
|
|
||||||
"""
|
|
||||||
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z
|
|
||||||
move-result v$freeRegister
|
|
||||||
if-eqz v$freeRegister, :disabled
|
|
||||||
|
|
||||||
# Get video id.
|
|
||||||
# From YouTube 17.34.36 to YouTube 19.16.39, the field names and field types are the same.
|
|
||||||
iget-object v$freeRegister, v$videoDetailsRegister, $videoDetailsClass->c:Ljava/lang/String;
|
|
||||||
if-eqz v$freeRegister, :disabled
|
|
||||||
|
|
||||||
# Get streaming data.
|
|
||||||
invoke-static { v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer;
|
|
||||||
move-result-object v$freeRegister
|
|
||||||
if-eqz v$freeRegister, :disabled
|
|
||||||
|
|
||||||
# Parse streaming data.
|
|
||||||
sget-object v$overrideRegister, $playerProtoClass->a:$playerProtoClass
|
|
||||||
invoke-static { v$overrideRegister, v$freeRegister }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass
|
|
||||||
move-result-object v$freeRegister
|
|
||||||
check-cast v$freeRegister, $playerProtoClass
|
|
||||||
|
|
||||||
# Set streaming data.
|
|
||||||
iget-object v$freeRegister, v$freeRegister, $getStreamingDataField
|
|
||||||
if-eqz v$freeRegister, :disabled
|
|
||||||
iput-object v$freeRegister, p0, $setStreamingDataField
|
|
||||||
|
|
||||||
""",
|
|
||||||
ExternalLabel("disabled", getInstruction(insertIndex))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Remove /videoplayback request body to fix playback.
|
|
||||||
// This is needed when using iOS client as streaming data source.
|
|
||||||
|
|
||||||
buildMediaDataSourceFingerprint.methodOrThrow().apply {
|
|
||||||
val targetIndex = instructions.lastIndex
|
|
||||||
|
|
||||||
addInstructions(
|
|
||||||
targetIndex,
|
|
||||||
"""
|
|
||||||
# Field a: Stream uri.
|
|
||||||
# Field c: Http method.
|
|
||||||
# Field d: Post data.
|
|
||||||
# From YouTube 17.34.36 to YouTube 19.16.39, the field names and field types are the same.
|
|
||||||
move-object/from16 v0, p0
|
|
||||||
iget-object v1, v0, $definingClass->a:Landroid/net/Uri;
|
|
||||||
iget v2, v0, $definingClass->c:I
|
|
||||||
iget-object v3, v0, $definingClass->d:[B
|
|
||||||
invoke-static { v1, v2, v3 }, $EXTENSION_CLASS_DESCRIPTOR->removeVideoPlaybackPostBody(Landroid/net/Uri;I[B)[B
|
|
||||||
move-result-object v1
|
|
||||||
iput-object v1, v0, $definingClass->d:[B
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Append spoof info.
|
|
||||||
|
|
||||||
nerdsStatsVideoFormatBuilderFingerprint.methodOrThrow().apply {
|
|
||||||
findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index ->
|
|
||||||
val register = getInstruction<OneRegisterInstruction>(index).registerA
|
|
||||||
|
|
||||||
addInstructions(
|
|
||||||
index, """
|
|
||||||
invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->appendSpoofedClient(Ljava/lang/String;)Ljava/lang/String;
|
|
||||||
move-result-object v$register
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region add settings
|
|
||||||
|
|
||||||
|
dependsOn(
|
||||||
|
baseSpoofUserAgentPatch(YOUTUBE_PACKAGE_NAME),
|
||||||
|
settingsPatch
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
addPreference(
|
addPreference(
|
||||||
arrayOf(
|
arrayOf(
|
||||||
"SETTINGS: SPOOF_STREAMING_DATA"
|
"SETTINGS: SPOOF_STREAMING_DATA"
|
||||||
@ -224,7 +25,5 @@ val spoofStreamingDataPatch = bytecodePatch(
|
|||||||
SPOOF_STREAMING_DATA
|
SPOOF_STREAMING_DATA
|
||||||
)
|
)
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
@ -223,7 +223,7 @@ internal enum class PatchList(
|
|||||||
),
|
),
|
||||||
SPOOF_STREAMING_DATA(
|
SPOOF_STREAMING_DATA(
|
||||||
"Spoof streaming data",
|
"Spoof streaming data",
|
||||||
"Adds options to spoof the streaming data to allow video playback."
|
"Adds options to spoof the streaming data to allow playback."
|
||||||
),
|
),
|
||||||
SWIPE_CONTROLS(
|
SWIPE_CONTROLS(
|
||||||
"Swipe controls",
|
"Swipe controls",
|
||||||
|
@ -55,6 +55,10 @@ internal fun Pair<String, Fingerprint>.matchOrNull(parentFingerprint: Pair<Strin
|
|||||||
second.matchOrNull(parentClassDef)
|
second.matchOrNull(parentClassDef)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context(BytecodePatchContext)
|
||||||
|
internal fun Pair<String, Fingerprint>.methodOrNull(): MutableMethod? =
|
||||||
|
matchOrNull()?.method
|
||||||
|
|
||||||
context(BytecodePatchContext)
|
context(BytecodePatchContext)
|
||||||
internal fun Pair<String, Fingerprint>.methodOrThrow(): MutableMethod =
|
internal fun Pair<String, Fingerprint>.methodOrThrow(): MutableMethod =
|
||||||
second.methodOrNull ?: throw first.exception
|
second.methodOrNull ?: throw first.exception
|
||||||
@ -63,6 +67,14 @@ context(BytecodePatchContext)
|
|||||||
internal fun Pair<String, Fingerprint>.methodOrThrow(parentFingerprint: Pair<String, Fingerprint>): MutableMethod =
|
internal fun Pair<String, Fingerprint>.methodOrThrow(parentFingerprint: Pair<String, Fingerprint>): MutableMethod =
|
||||||
matchOrThrow(parentFingerprint).method
|
matchOrThrow(parentFingerprint).method
|
||||||
|
|
||||||
|
context(BytecodePatchContext)
|
||||||
|
internal fun Pair<String, Fingerprint>.originalMethodOrThrow(): Method =
|
||||||
|
second.originalMethodOrNull ?: throw first.exception
|
||||||
|
|
||||||
|
context(BytecodePatchContext)
|
||||||
|
internal fun Pair<String, Fingerprint>.originalMethodOrThrow(parentFingerprint: Pair<String, Fingerprint>): Method =
|
||||||
|
matchOrThrow(parentFingerprint).originalMethod
|
||||||
|
|
||||||
context(BytecodePatchContext)
|
context(BytecodePatchContext)
|
||||||
internal fun Pair<String, Fingerprint>.mutableClassOrThrow(): MutableClass =
|
internal fun Pair<String, Fingerprint>.mutableClassOrThrow(): MutableClass =
|
||||||
second.classDefOrNull ?: throw first.exception
|
second.classDefOrNull ?: throw first.exception
|
||||||
|
@ -450,7 +450,16 @@ Tap on the continue button and disable battery optimizations."</string>
|
|||||||
Limitations:
|
Limitations:
|
||||||
• OPUS audio codec may not be supported.
|
• OPUS audio codec may not be supported.
|
||||||
• Seekbar thumbnail may not be present.
|
• Seekbar thumbnail may not be present.
|
||||||
• Watch history does not work with a brand account.</string>
|
• Watch history does not work with a brand account."</string>
|
||||||
|
|
||||||
|
<string name="revanced_spoof_streaming_data_title">Spoof streaming data</string>
|
||||||
|
<string name="revanced_spoof_streaming_data_summary">Spoof the streaming data to prevent playback issues.</string>
|
||||||
|
<string name="revanced_spoof_streaming_data_stats_for_nerds_title">Show in Stats for nerds</string>
|
||||||
|
<string name="revanced_spoof_streaming_data_stats_for_nerds_summary">Shows the client used to fetch streaming data in Stats for nerds.</string>
|
||||||
|
<string name="revanced_spoof_streaming_data_type_entry_ios">iOS</string>
|
||||||
|
<string name="revanced_spoof_streaming_data_type_entry_ios_music">iOS Music</string>
|
||||||
|
<string name="revanced_spoof_streaming_data_type_entry_android_unplugged">Android TV</string>
|
||||||
|
<string name="revanced_spoof_streaming_data_type_entry_android_vr">Android VR</string>
|
||||||
|
|
||||||
<string name="revanced_sanitize_sharing_links_title">Sanitize sharing links</string>
|
<string name="revanced_sanitize_sharing_links_title">Sanitize sharing links</string>
|
||||||
<string name="revanced_sanitize_sharing_links_summary">Removes tracking query parameters from URLs when sharing links.</string>
|
<string name="revanced_sanitize_sharing_links_summary">Removes tracking query parameters from URLs when sharing links.</string>
|
||||||
|
@ -1895,6 +1895,7 @@ Tap on the continue button and disable battery optimizations."</string>
|
|||||||
<string name="revanced_spoof_streaming_data_user_dialog_message">Turning off this setting may cause video playback issues.</string>
|
<string name="revanced_spoof_streaming_data_user_dialog_message">Turning off this setting may cause video playback issues.</string>
|
||||||
<string name="revanced_spoof_streaming_data_type_title">Default client</string>
|
<string name="revanced_spoof_streaming_data_type_title">Default client</string>
|
||||||
<string name="revanced_spoof_streaming_data_type_entry_ios">iOS</string>
|
<string name="revanced_spoof_streaming_data_type_entry_ios">iOS</string>
|
||||||
|
<string name="revanced_spoof_streaming_data_type_entry_ios_music">iOS Music</string>
|
||||||
<string name="revanced_spoof_streaming_data_type_entry_android_unplugged">Android TV</string>
|
<string name="revanced_spoof_streaming_data_type_entry_android_unplugged">Android TV</string>
|
||||||
<string name="revanced_spoof_streaming_data_type_entry_android_vr">Android VR</string>
|
<string name="revanced_spoof_streaming_data_type_entry_android_vr">Android VR</string>
|
||||||
<string name="revanced_spoof_streaming_data_side_effects_title">Spoofing side effects</string>
|
<string name="revanced_spoof_streaming_data_side_effects_title">Spoofing side effects</string>
|
||||||
|
@ -778,10 +778,10 @@
|
|||||||
<!-- SETTINGS: SPOOF_STREAMING_DATA
|
<!-- SETTINGS: SPOOF_STREAMING_DATA
|
||||||
<PreferenceScreen android:title="@string/revanced_preference_screen_spoof_streaming_data_title" android:key="revanced_preference_screen_spoof_streaming_data" android:summary="@string/revanced_preference_screen_spoof_streaming_data_summary">
|
<PreferenceScreen android:title="@string/revanced_preference_screen_spoof_streaming_data_title" android:key="revanced_preference_screen_spoof_streaming_data" android:summary="@string/revanced_preference_screen_spoof_streaming_data_summary">
|
||||||
<SwitchPreference android:title="@string/revanced_spoof_streaming_data_title" android:key="revanced_spoof_streaming_data" android:summaryOn="@string/revanced_spoof_streaming_data_summary_on" android:summaryOff="@string/revanced_spoof_streaming_data_summary_off" />
|
<SwitchPreference android:title="@string/revanced_spoof_streaming_data_title" android:key="revanced_spoof_streaming_data" android:summaryOn="@string/revanced_spoof_streaming_data_summary_on" android:summaryOff="@string/revanced_spoof_streaming_data_summary_off" />
|
||||||
<ListPreference android:entries="@array/revanced_spoof_streaming_data_type_entries" android:title="@string/revanced_spoof_streaming_data_type_title" android:key="revanced_spoof_streaming_data_type" android:entryValues="@array/revanced_spoof_streaming_data_type_entry_values" />
|
<ListPreference android:entries="@array/revanced_spoof_streaming_data_type_entries" android:title="@string/revanced_spoof_streaming_data_type_title" android:key="revanced_spoof_streaming_data_type" android:entryValues="@array/revanced_spoof_streaming_data_type_entry_values" android:dependency="revanced_spoof_streaming_data" />
|
||||||
<app.revanced.extension.youtube.settings.preference.SpoofStreamingDataSideEffectsPreference android:title="@string/revanced_spoof_streaming_data_side_effects_title" />
|
<app.revanced.extension.youtube.settings.preference.SpoofStreamingDataSideEffectsPreference android:title="@string/revanced_spoof_streaming_data_side_effects_title" />
|
||||||
<SwitchPreference android:title="@string/revanced_spoof_streaming_data_ios_skip_livestream_playback_title" android:key="revanced_spoof_streaming_data_ios_skip_livestream_playback" android:summaryOn="@string/revanced_spoof_streaming_data_ios_skip_livestream_playback_summary_on" android:summaryOff="@string/revanced_spoof_streaming_data_ios_skip_livestream_playback_summary_off" />
|
<SwitchPreference android:title="@string/revanced_spoof_streaming_data_ios_skip_livestream_playback_title" android:key="revanced_spoof_streaming_data_ios_skip_livestream_playback" android:summaryOn="@string/revanced_spoof_streaming_data_ios_skip_livestream_playback_summary_on" android:summaryOff="@string/revanced_spoof_streaming_data_ios_skip_livestream_playback_summary_off" android:dependency="revanced_spoof_streaming_data" />
|
||||||
<SwitchPreference android:title="@string/revanced_spoof_streaming_data_stats_for_nerds_title" android:key="revanced_spoof_streaming_data_stats_for_nerds" android:summaryOn="@string/revanced_spoof_streaming_data_stats_for_nerds_summary_on" android:summaryOff="@string/revanced_spoof_streaming_data_stats_for_nerds_summary_off" />
|
<SwitchPreference android:title="@string/revanced_spoof_streaming_data_stats_for_nerds_title" android:key="revanced_spoof_streaming_data_stats_for_nerds" android:summaryOn="@string/revanced_spoof_streaming_data_stats_for_nerds_summary_on" android:summaryOff="@string/revanced_spoof_streaming_data_stats_for_nerds_summary_off" android:dependency="revanced_spoof_streaming_data" />
|
||||||
</PreferenceScreen>SETTINGS: SPOOF_STREAMING_DATA -->
|
</PreferenceScreen>SETTINGS: SPOOF_STREAMING_DATA -->
|
||||||
|
|
||||||
<!-- SETTINGS: WATCH_HISTORY
|
<!-- SETTINGS: WATCH_HISTORY
|
||||||
|
Loading…
x
Reference in New Issue
Block a user