fix(YouTube - Spoof streaming data): Change the default client to Android VR, add iOS TV to the selectable default clients, and add the Use Android clients only setting https://github.com/inotia00/ReVanced_Extended/issues/2593

This commit is contained in:
inotia00
2024-12-22 16:33:29 +09:00
parent 480d47587d
commit 89d038fdb8
18 changed files with 416 additions and 264 deletions

View File

@ -1,80 +0,0 @@
package app.revanced.extension.shared.patches;
import static app.revanced.extension.shared.patches.PatchStatus.SpoofClient;
import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingData;
import android.net.Uri;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused")
public class BlockRequestPatch {
/**
* Used in YouTube and YouTube Music.
*/
public static final boolean SPOOF_STREAMING_DATA = BaseSettings.SPOOF_STREAMING_DATA.get();
/**
* Used in YouTube Music.
* Disabled by default.
*/
public static final boolean SPOOF_CLIENT = BaseSettings.SPOOF_CLIENT.get();
private static final boolean BLOCK_REQUEST = (SpoofStreamingData() && SPOOF_STREAMING_DATA) || (SpoofClient() && SPOOF_CLIENT);
/**
* Any unreachable ip address. Used to intentionally fail requests.
*/
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
/**
* Injection point.
* Blocks /get_watch requests by returning an unreachable URI.
*
* @param playerRequestUri The URI of the player request.
* @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
*/
public static Uri blockGetWatchRequest(Uri playerRequestUri) {
if (BLOCK_REQUEST) {
try {
String path = playerRequestUri.getPath();
if (path != null && path.contains("get_watch")) {
Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri");
return UNREACHABLE_HOST_URI;
}
} catch (Exception ex) {
Logger.printException(() -> "blockGetWatchRequest failure", ex);
}
}
return playerRequestUri;
}
/**
* Injection point.
* <p>
* Blocks /initplayback requests.
*/
public static String blockInitPlaybackRequest(String originalUrlString) {
if (BLOCK_REQUEST) {
try {
var originalUri = Uri.parse(originalUrlString);
String path = originalUri.getPath();
if (path != null && path.contains("initplayback")) {
Logger.printDebug(() -> "Blocking 'initplayback' by clearing query");
return originalUri.buildUpon().clearQuery().build().toString();
}
} catch (Exception ex) {
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
}
}
return originalUrlString;
}
}

View File

@ -1,19 +1,18 @@
package app.revanced.extension.shared.patches;
import app.revanced.extension.shared.patches.client.AppClient.ClientType;
@SuppressWarnings("unused")
public class PatchStatus {
public static boolean HideFullscreenAdsDefaultBoolean() {
return false;
}
public static ClientType SpoofStreamingDataDefaultClient() {
return ClientType.IOS;
}
public static boolean SpoofStreamingData() {
// Replace this with true If the Spoof streaming data patch succeeds
return false;
}
public static boolean SpoofStreamingDataAndroidOnlyDefaultBoolean() {
// Replace this with true If the Spoof streaming data patch succeeds in YouTube
return false;
}
}

View File

@ -6,8 +6,15 @@ import android.os.Build;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.settings.BaseSettings;
public class AppClient {
// IOS
/**
* Video not playable: Paid / Movie / Private / Age-restricted
* Note: Audio track available
*/
private static final String PACKAGE_NAME_IOS = "com.google.ios.youtube";
/**
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
*
@ -29,15 +36,35 @@ public class AppClient {
private static final String DEVICE_MODEL_IOS = "iPhone16,2";
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.youtube/" +
CLIENT_VERSION_IOS +
"(" +
DEVICE_MODEL_IOS +
"; U; CPU iOS " +
USER_AGENT_VERSION_IOS +
" like Mac OS X)";
private static final String USER_AGENT_IOS =
iOSUserAgent(PACKAGE_NAME_IOS, CLIENT_VERSION_IOS);
// IOS UNPLUGGED
/**
* Video not playable: Paid / Movie
* Note: Audio track available
*/
private static final String PACKAGE_NAME_IOS_UNPLUGGED = "com.google.ios.youtubeunplugged";
/**
* 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-tv/id1193350206/">the App
* Store page of the YouTube TV app</a>, in the {@code Whats New} section.
* </p>
*/
private static final String CLIENT_VERSION_IOS_UNPLUGGED = "8.33";
private static final String USER_AGENT_IOS_UNPLUGGED =
iOSUserAgent(PACKAGE_NAME_IOS_UNPLUGGED, CLIENT_VERSION_IOS_UNPLUGGED);
// IOS MUSIC
/**
* Video not playable: All videos that can't be played on YouTube Music
*/
private static final String PACKAGE_NAME_IOS_MUSIC = "com.google.ios.youtubemusic";
/**
* The hardcoded client version of the iOS app used for InnerTube requests with this client.
*
@ -47,16 +74,21 @@ public class AppClient {
* Store page of the YouTube Music app</a>, in the {@code Whats 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_MUSIC +
"(" +
DEVICE_MODEL_IOS +
"; U; CPU iOS " +
USER_AGENT_VERSION_IOS +
" like Mac OS X)";
private static final String CLIENT_VERSION_IOS_MUSIC = "7.04";
private static final String USER_AGENT_IOS_MUSIC =
iOSUserAgent(PACKAGE_NAME_IOS_MUSIC, CLIENT_VERSION_IOS_MUSIC);
// ANDROID VR
/**
* Video not playable: Kids
* Note: Audio track is not available
* <p>
* 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 (ByteDance Pico 4): com.google.android.apps.youtube.vr.pico
*/
private static final String PACKAGE_NAME_ANDROID_VR = "com.google.android.apps.youtube.vr.oculus";
/**
* The hardcoded client version of the Android VR app used for InnerTube requests with this client.
*
@ -66,7 +98,7 @@ public class AppClient {
* Store page of the YouTube app</a>, in the {@code Additional details} section.
* </p>
*/
private static final String CLIENT_VERSION_ANDROID_VR = "1.61.47";
private static final String CLIENT_VERSION_ANDROID_VR = "1.61.48";
/**
* The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client.
*
@ -82,19 +114,17 @@ public class AppClient {
* but for some reason the build.props for the {@code Quest 3} state that the SDK version is 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 (Meta Quests): com.google.android.apps.youtube.vr.oculus
* Package name for YouTube VR (ByteDance Pico 4): com.google.android.apps.youtube.vr.pico
*/
private static final String USER_AGENT_ANDROID_VR = "com.google.android.apps.youtube.vr.oculus/" +
CLIENT_VERSION_ANDROID_VR +
" (Linux; U; Android " +
OS_VERSION_ANDROID_VR +
"; GB) gzip";
private static final String USER_AGENT_ANDROID_VR =
androidUserAgent(PACKAGE_NAME_ANDROID_VR, CLIENT_VERSION_ANDROID_VR, OS_VERSION_ANDROID_VR);
// ANDROID UNPLUGGED
private static final String CLIENT_VERSION_ANDROID_UNPLUGGED = "8.49.0";
/**
* Video not playable: Playlists / Music
* Note: Audio track is not available
*/
private static final String PACKAGE_NAME_ANDROID_UNPLUGGED = "com.google.android.apps.youtube.unplugged";
private static final String CLIENT_VERSION_ANDROID_UNPLUGGED = "8.16.0";
/**
* The device machine id for the Chromecast with Google TV 4K.
*
@ -106,15 +136,47 @@ public class AppClient {
private static final String DEVICE_MODEL_ANDROID_UNPLUGGED = "Google TV Streamer";
private static final String OS_VERSION_ANDROID_UNPLUGGED = "14";
private static final String ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = "34";
private static final String USER_AGENT_ANDROID_UNPLUGGED = "com.google.android.apps.youtube.unplugged/" +
CLIENT_VERSION_ANDROID_UNPLUGGED +
" (Linux; U; Android " +
OS_VERSION_ANDROID_UNPLUGGED +
"; GB) gzip";
private static final String USER_AGENT_ANDROID_UNPLUGGED =
androidUserAgent(PACKAGE_NAME_ANDROID_UNPLUGGED, CLIENT_VERSION_ANDROID_UNPLUGGED, OS_VERSION_ANDROID_UNPLUGGED);
// ANDROID CREATOR
/**
* Video not playable: Livestream
* Note: Audio track is not available
*/
private static final String PACKAGE_NAME_ANDROID_CREATOR = "com.google.android.apps.youtube.creator";
private static final String CLIENT_VERSION_ANDROID_CREATOR = "24.14.101";
private static final String DEVICE_MODEL_ANDROID_CREATOR = Build.MODEL;
private static final String OS_VERSION_ANDROID_CREATOR = Build.VERSION.RELEASE;
private static final String ANDROID_SDK_VERSION_ANDROID_CREATOR = String.valueOf(Build.VERSION.SDK_INT);
private static final String USER_AGENT_ANDROID_CREATOR =
androidUserAgent(PACKAGE_NAME_ANDROID_CREATOR, CLIENT_VERSION_ANDROID_CREATOR, OS_VERSION_ANDROID_CREATOR);
private AppClient() {
}
private static String androidUserAgent(String packageName, String clientVersion, String osVersion) {
return packageName +
"/" +
clientVersion +
" (Linux; U; Android " +
osVersion +
"; GB) gzip";
}
private static String iOSUserAgent(String packageName, String clientVersion) {
return packageName +
"/" +
clientVersion +
"(" +
DEVICE_MODEL_IOS +
"; U; CPU iOS " +
USER_AGENT_VERSION_IOS +
" like Mac OS X)";
}
public enum ClientType {
IOS(5,
DEVICE_MODEL_IOS,
@ -140,6 +202,22 @@ public class AppClient {
CLIENT_VERSION_ANDROID_UNPLUGGED,
true
),
ANDROID_CREATOR(14,
DEVICE_MODEL_ANDROID_CREATOR,
OS_VERSION_ANDROID_CREATOR,
USER_AGENT_ANDROID_CREATOR,
ANDROID_SDK_VERSION_ANDROID_CREATOR,
CLIENT_VERSION_ANDROID_CREATOR,
true
),
IOS_UNPLUGGED(33,
DEVICE_MODEL_IOS,
OS_VERSION_IOS,
USER_AGENT_IOS_UNPLUGGED,
null,
CLIENT_VERSION_IOS_UNPLUGGED,
true
),
IOS_MUSIC(
26,
DEVICE_MODEL_IOS,
@ -208,8 +286,28 @@ public class AppClient {
this.canLogin = canLogin;
}
private static final ClientType[] CLIENT_ORDER_TO_USE_ANDROID = {
ANDROID_VR,
ANDROID_UNPLUGGED,
ANDROID_CREATOR,
};
private static final ClientType[] CLIENT_ORDER_TO_USE_DEFAULT = {
IOS,
ANDROID_VR,
ANDROID_UNPLUGGED,
IOS_UNPLUGGED,
IOS_MUSIC,
};
public final String getFriendlyName() {
return getString("revanced_spoof_streaming_data_type_entry_" + name().toLowerCase());
}
}
public static ClientType[] getAvailableClientTypes() {
return BaseSettings.SPOOF_STREAMING_DATA_ANDROID_ONLY.get()
? ClientType.CLIENT_ORDER_TO_USE_ANDROID
: ClientType.CLIENT_ORDER_TO_USE_DEFAULT;
}
}

View File

@ -1,5 +1,7 @@
package app.revanced.extension.shared.patches.spoof;
import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingData;
import android.net.Uri;
import android.text.TextUtils;
@ -10,14 +12,20 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import app.revanced.extension.shared.patches.BlockRequestPatch;
import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused")
public class SpoofStreamingDataPatch extends BlockRequestPatch {
public class SpoofStreamingDataPatch {
public static final boolean SPOOF_STREAMING_DATA = SpoofStreamingData() && BaseSettings.SPOOF_STREAMING_DATA.get();
/**
* Any unreachable ip address. Used to intentionally fail requests.
*/
private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0";
private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING);
/**
* Key: video id
@ -33,6 +41,55 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
}
});
/**
* Injection point.
* Blocks /get_watch requests by returning an unreachable URI.
*
* @param playerRequestUri The URI of the player request.
* @return An unreachable URI if the request is a /get_watch request, otherwise the original URI.
*/
public static Uri blockGetWatchRequest(Uri playerRequestUri) {
if (SPOOF_STREAMING_DATA) {
try {
String path = playerRequestUri.getPath();
if (path != null && path.contains("get_watch")) {
Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri");
return UNREACHABLE_HOST_URI;
}
} catch (Exception ex) {
Logger.printException(() -> "blockGetWatchRequest failure", ex);
}
}
return playerRequestUri;
}
/**
* Injection point.
* <p>
* Blocks /initplayback requests.
*/
public static String blockInitPlaybackRequest(String originalUrlString) {
if (SPOOF_STREAMING_DATA) {
try {
var originalUri = Uri.parse(originalUrlString);
String path = originalUri.getPath();
if (path != null && path.contains("initplayback")) {
Logger.printDebug(() -> "Blocking 'initplayback' by clearing query");
return originalUri.buildUpon().clearQuery().build().toString();
}
} catch (Exception ex) {
Logger.printException(() -> "blockInitPlaybackRequest failure", ex);
}
}
return originalUrlString;
}
/**
* Injection point.
*/
@ -146,7 +203,7 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
* Called after {@link #getStreamingData(String)}.
*/
public static long getApproxDurationMs(String videoId) {
if (videoId != null) {
if (SPOOF_STREAMING_DATA && videoId != null) {
final Long approxDurationMs = approxDurationMsMap.get(videoId);
if (approxDurationMs != null) {
Logger.printDebug(() -> "Replacing video length: " + approxDurationMs + " for videoId: " + videoId);

View File

@ -52,6 +52,10 @@ public final class PlayerRoutes {
client.put("osVersion", clientType.osVersion);
if (clientType.androidSdkVersion != null) {
client.put("androidSdkVersion", clientType.androidSdkVersion);
client.put("osName", "Android");
} else {
client.put("deviceMake", "Apple");
client.put("osName", "iOS");
}
client.put("hl", LOCALE_LANGUAGE);

View File

@ -1,5 +1,6 @@
package app.revanced.extension.shared.patches.spoof.requests;
import static app.revanced.extension.shared.patches.client.AppClient.getAvailableClientTypes;
import static app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA;
import androidx.annotation.NonNull;
@ -13,6 +14,7 @@ import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
@ -80,16 +82,20 @@ public class StreamingDataRequest {
}
static {
ClientType[] allClientTypes = ClientType.values();
ClientType[] allClientTypes = getAvailableClientTypes();
ClientType preferredClient = BaseSettings.SPOOF_STREAMING_DATA_TYPE.get();
CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length];
CLIENT_ORDER_TO_USE[0] = preferredClient;
if (Arrays.stream(allClientTypes).noneMatch(preferredClient::equals)) {
CLIENT_ORDER_TO_USE = allClientTypes;
} else {
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;
int i = 1;
for (ClientType c : allClientTypes) {
if (c != preferredClient) {
CLIENT_ORDER_TO_USE[i++] = c;
}
}
}
}

View File

@ -3,7 +3,7 @@ package app.revanced.extension.shared.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.patches.PatchStatus.HideFullscreenAdsDefaultBoolean;
import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingDataDefaultClient;
import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingDataAndroidOnlyDefaultBoolean;
import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat;
import app.revanced.extension.shared.patches.client.AppClient.ClientType;
@ -37,8 +37,8 @@ public class BaseSettings {
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_SYNC_VIDEO_LENGTH = new BooleanSetting("revanced_spoof_streaming_data_sync_video_length", TRUE, true);
public static final EnumSetting<ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.ANDROID_VR, true);
public static final BooleanSetting SPOOF_STREAMING_DATA_ANDROID_ONLY = new BooleanSetting("revanced_spoof_streaming_data_android_only", SpoofStreamingDataAndroidOnlyDefaultBoolean(), true, "revanced_spoof_streaming_data_android_only_user_dialog_message");
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
/**

View File

@ -0,0 +1,87 @@
package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.utils.ResourceUtils.getStringArray;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.ListPreference;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import app.revanced.extension.shared.patches.client.AppClient.ClientType;
import app.revanced.extension.shared.settings.EnumSetting;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"unused", "deprecation"})
public class SpoofStreamingDataDefaultClientListPreference extends ListPreference {
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
// Because this listener may run before the ReVanced settings fragment updates Settings,
// this could show the prior config and not the current.
//
// Push this call to the end of the main run queue,
// so all other listeners are done and Settings is up to date.
Utils.runOnMainThread(this::updateUI);
};
public SpoofStreamingDataDefaultClientListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public SpoofStreamingDataDefaultClientListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SpoofStreamingDataDefaultClientListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SpoofStreamingDataDefaultClientListPreference(Context context) {
super(context);
}
private void addChangeListener() {
Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener);
}
private void removeChangeListener() {
Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener);
}
@Override
protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
super.onAttachedToHierarchy(preferenceManager);
updateUI();
addChangeListener();
}
@Override
protected void onPrepareForRemoval() {
super.onPrepareForRemoval();
removeChangeListener();
}
private void updateUI() {
final boolean spoofStreamingDataAndroidOnly = Settings.SPOOF_STREAMING_DATA_ANDROID_ONLY.get();
final String entryKey = spoofStreamingDataAndroidOnly
? "revanced_spoof_streaming_data_type_android_entries"
: "revanced_spoof_streaming_data_type_android_ios_entries";
final String entryValueKey = spoofStreamingDataAndroidOnly
? "revanced_spoof_streaming_data_type_android_entry_values"
: "revanced_spoof_streaming_data_type_android_ios_entry_values";
final String[] mEntries = getStringArray(entryKey);
final String[] mEntryValues = getStringArray(entryValueKey);
setEntries(mEntries);
setEntryValues(mEntryValues);
final EnumSetting<ClientType> clientType = Settings.SPOOF_STREAMING_DATA_TYPE;
final boolean isAndroid = clientType.get().name().startsWith("ANDROID");
if (spoofStreamingDataAndroidOnly && !isAndroid) {
clientType.resetToDefault();
}
setEnabled(Settings.SPOOF_STREAMING_DATA.get());
}
}

View File

@ -8,7 +8,6 @@ import android.preference.Preference;
import android.preference.PreferenceManager;
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.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
@ -63,8 +62,14 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
}
private void updateUI() {
final ClientType clientType = Settings.SPOOF_STREAMING_DATA_TYPE.get();
final String summaryTextKey = "revanced_spoof_streaming_data_side_effects_" + clientType.name().toLowerCase();
final String clientName = Settings.SPOOF_STREAMING_DATA_TYPE.get().name().toLowerCase();
String summaryTextKey = "revanced_spoof_streaming_data_side_effects_";
if (Settings.SPOOF_STREAMING_DATA_ANDROID_ONLY.get()) {
summaryTextKey += "android";
} else {
summaryTextKey += clientName;
}
setSummary(str(summaryTextKey));
setEnabled(Settings.SPOOF_STREAMING_DATA.get());