diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index edb9d1b8af..7922c2cf30 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,6 +10,10 @@ on: jobs: release: name: Release + permissions: + contents: write + issues: write + pull-requests: write runs-on: ubuntu-latest steps: - name: Checkout @@ -49,5 +53,5 @@ jobs: - name: Release env: - GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm exec semantic-release diff --git a/CHANGELOG.md b/CHANGELOG.md index 20f49125cd..13d6c11bd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# [0.159.0-dev.1](https://github.com/anddea/revanced-integrations/compare/v0.158.0...v0.159.0-dev.1) (2024-11-10) + + +### Bug Fixes + +* **YouTube - Hide feed components:** Rollback `CarouselShelfFilter` ([15dff30](https://github.com/anddea/revanced-integrations/commit/15dff30e761ab606bd2a77c82950e2f400b734a4)) +* **YouTube - Toolbar components:** Premium header not applied when `Hide YouTube Doodles` is turned on ([e8e9923](https://github.com/anddea/revanced-integrations/commit/e8e9923458e67f9fa67e8ae099677ed292ece5ec)) + + +### Features + +* **YouTube - Spoof app version:** Remove obsolete `19.13.37` spoof target ([78ffd65](https://github.com/anddea/revanced-integrations/commit/78ffd6571e682fd05f7c81fb83725a3e942f910a)) +* **YouTube - Spoof streaming data:** Add `iOS Compatibility mode` setting ([07b3d8a](https://github.com/anddea/revanced-integrations/commit/07b3d8ac3c0f3b774e4ad2126c6d820faf40939c)) +* **YouTube - Spoof streaming data:** Change default client to iOS ([79d34e4](https://github.com/anddea/revanced-integrations/commit/79d34e44023817d087e79a32d63a3a104d455a13)) +* **YouTube - Spoof streaming data:** Update the hardcoded iOS client version ([74d26e5](https://github.com/anddea/revanced-integrations/commit/74d26e5923648c93eaa8031d4fc64b491b9af1c0)) + # [0.158.0](https://github.com/anddea/revanced-integrations/compare/v0.157.0...v0.158.0) (2024-11-07) diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/CarouselShelfFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/CarouselShelfFilter.java index 72799ea724..5a35cbce5a 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/CarouselShelfFilter.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/CarouselShelfFilter.java @@ -8,6 +8,7 @@ import app.revanced.integrations.shared.patches.components.Filter; import app.revanced.integrations.shared.patches.components.StringFilterGroup; import app.revanced.integrations.shared.utils.Logger; +import app.revanced.integrations.shared.utils.StringTrieSearch; import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; import app.revanced.integrations.youtube.shared.RootView; @@ -33,16 +34,25 @@ public final class CarouselShelfFilter extends Filter { BROWSE_ID_NOTIFICATION_INBOX ); + private final StringTrieSearch exceptions = new StringTrieSearch(); + public final StringFilterGroup horizontalShelf; + public CarouselShelfFilter() { - addPathCallbacks( - new StringFilterGroup( - Settings.HIDE_CAROUSEL_SHELF, - "horizontal_video_shelf.eml", - "horizontal_shelf.eml", - "horizontal_shelf_inline.eml", - "horizontal_tile_shelf.eml" - ) + exceptions.addPattern("library_recent_shelf.eml"); + + final StringFilterGroup carouselShelf = new StringFilterGroup( + Settings.HIDE_CAROUSEL_SHELF, + "horizontal_shelf_inline.eml", + "horizontal_tile_shelf.eml", + "horizontal_video_shelf.eml" + ); + + horizontalShelf = new StringFilterGroup( + Settings.HIDE_CAROUSEL_SHELF, + "horizontal_shelf.eml" ); + + addPathCallbacks(carouselShelf, horizontalShelf); } private static boolean hideShelves(boolean playerActive, boolean searchBarActive, NavigationButton selectedNavButton, String browseId) { @@ -64,12 +74,15 @@ private static boolean hideShelves(boolean playerActive, boolean searchBarActive @Override public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (exceptions.matches(path)) { + return false; + } final boolean playerActive = RootView.isPlayerActive(); final boolean searchBarActive = RootView.isSearchBarActive(); final NavigationButton navigationButton = NavigationButton.getSelectedNavigationButton(); final String navigation = navigationButton == null ? "null" : navigationButton.name(); final String browseId = RootView.getBrowseId(); - final boolean hideShelves = hideShelves(playerActive, searchBarActive, navigationButton, browseId); + final boolean hideShelves = matchedGroup != horizontalShelf || hideShelves(playerActive, searchBarActive, navigationButton, browseId); if (contentIndex != 0) { return false; } diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/general/GeneralPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/general/GeneralPatch.java index 2772ebff5c..0db0933aaa 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/general/GeneralPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/general/GeneralPatch.java @@ -310,10 +310,10 @@ public static void setDrawerNavigationHeader(View lithoView) { return; if (!(viewGroup.getChildAt(0) instanceof ImageView imageView)) return; - final Activity mAcrivity = Utils.getActivity(); - if (mAcrivity == null) + final Activity mActivity = Utils.getActivity(); + if (mActivity == null) return; - imageView.setImageDrawable(getHeaderDrawable(mAcrivity, headerAttributeId)); + imageView.setImageDrawable(getHeaderDrawable(mActivity, headerAttributeId)); }); } @@ -481,10 +481,18 @@ public static void hideVoiceSearchButton(View view, int visibility) { } } + /** + * In ReVanced, image files are replaced to change the header, + * Whereas in RVX, the header is changed programmatically. + * There is an issue where the header is not changed in RVX when YouTube Doodles are hidden. + * As a workaround, manually set the header when YouTube Doodles are hidden. + */ public static void hideYouTubeDoodles(ImageView imageView, Drawable drawable) { - if (!Settings.HIDE_YOUTUBE_DOODLES.get()) { - imageView.setImageDrawable(drawable); + final Activity mActivity = Utils.getActivity(); + if (Settings.HIDE_YOUTUBE_DOODLES.get() && mActivity != null) { + drawable = getHeaderDrawable(mActivity, getHeaderAttributeId()); } + imageView.setImageDrawable(drawable); } private static final int settingsDrawableId = diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/AppClient.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/AppClient.java index 21fc215be9..d80c5c19ad 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/AppClient.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/client/AppClient.java @@ -40,7 +40,7 @@ public class AppClient { * Store page of the YouTube app, in the {@code What’s New} section. *

*/ - private static final String CLIENT_VERSION_IOS = "19.16.3"; + private static final String CLIENT_VERSION_IOS = "19.30.2"; private static final String DEVICE_MAKE_IOS = "Apple"; /** * The device machine id for the iPhone XS Max (iPhone11,4), used to get 60fps. diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java index 6d71c49274..289f478542 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlayerRoutes.java @@ -30,6 +30,13 @@ public final class PlayerRoutes { "?fields=contents.singleColumnWatchNextResults.playlist.playlist" ).compile(); + static final Route.CompiledRoute GET_LIVE_STREAM_RENDERER = new Route( + Route.Method.POST, + "player" + + "?fields=playabilityStatus.status," + + "videoDetails.isLiveContent" + ).compile(); + /** * TCP connection and HTTP read timeout */ diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlaylistRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlaylistRequest.java index be781f9501..42b3f0905b 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlaylistRequest.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/PlaylistRequest.java @@ -77,7 +77,7 @@ private static JSONObject send(ClientType clientType, String videoId) { final long startTime = System.currentTimeMillis(); String clientTypeName = clientType.name(); - Logger.printDebug(() -> "Fetching playlist request for: " + videoId + " using client: " + clientType.name()); + Logger.printDebug(() -> "Fetching playlist request for: " + videoId + " using client: " + clientTypeName); try { HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_PLAYLIST_PAGE, clientType); diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java index df9839e4c8..fb848fac8b 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/misc/requests/StreamingDataRequest.java @@ -1,11 +1,15 @@ package app.revanced.integrations.youtube.patches.misc.requests; +import static app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.GET_LIVE_STREAM_RENDERER; import static app.revanced.integrations.youtube.patches.misc.requests.PlayerRoutes.GET_STREAMING_DATA; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.json.JSONException; +import org.json.JSONObject; + import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -23,16 +27,19 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import app.revanced.integrations.shared.requests.Requester; import app.revanced.integrations.shared.utils.Logger; import app.revanced.integrations.shared.utils.Utils; import app.revanced.integrations.youtube.patches.misc.client.AppClient.ClientType; import app.revanced.integrations.youtube.settings.Settings; public class StreamingDataRequest { + private static final boolean SPOOF_STREAMING_DATA_IOS_COMPATIBILITY = Settings.SPOOF_STREAMING_DATA_IOS_COMPATIBILITY.get(); + private static final ClientType[] allClientTypes = { + ClientType.IOS, ClientType.ANDROID_VR, ClientType.ANDROID_UNPLUGGED, - ClientType.IOS, }; private static final ClientType[] clientTypesToUse; @@ -100,6 +107,59 @@ private static void handleConnectionError(String toastMessage, @Nullable Excepti Logger.printInfo(() -> toastMessage, ex); } + private static boolean isUnplayableOrLiveStream(ClientType clientType, String videoId) { + if (!SPOOF_STREAMING_DATA_IOS_COMPATIBILITY || clientType != ClientType.IOS) { + return false; + } + Objects.requireNonNull(videoId); + try { + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_LIVE_STREAM_RENDERER, clientType); + String innerTubeBody = PlayerRoutes.createInnertubeBody(clientType, videoId); + byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(requestBody.length); + connection.getOutputStream().write(requestBody); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) { + JSONObject playerResponse = Requester.parseJSONObject(connection); + final boolean isPlayabilityOk = isPlayabilityStatusOk(playerResponse); + final boolean isLiveStream = isLiveStream(playerResponse); + return !isPlayabilityOk || isLiveStream; + } + + // Always show a toast for this, as a non 200 response means something is broken. + handleConnectionError("Fetch livestreams not available: " + responseCode, null); + } catch (SocketTimeoutException ex) { + handleConnectionError("Fetch livestreams temporarily not available (API timed out)", ex); + } catch (IOException ex) { + handleConnectionError("Fetch livestreams temporarily not available: " + ex.getMessage(), ex); + } catch (Exception ex) { + Logger.printException(() -> "Fetch livestreams failed", ex); // Should never happen. + } + + return true; + } + + private static boolean isPlayabilityStatusOk(@NonNull JSONObject playerResponse) { + try { + return playerResponse.getJSONObject("playabilityStatus").getString("status").equals("OK"); + } catch (JSONException e) { + Logger.printDebug(() -> "Failed to get playabilityStatus for response: " + playerResponse); + } + + return false; + } + + private static boolean isLiveStream(@NonNull JSONObject playerResponse) { + try { + return playerResponse.getJSONObject("videoDetails").getBoolean("isLiveContent"); + } catch (JSONException e) { + Logger.printDebug(() -> "Failed to get videoDetails for response: " + playerResponse); + } + + return false; + } + private static final String[] REQUEST_HEADER_KEYS = { "Authorization", // Available only to logged in users. "X-GOOG-API-FORMAT-VERSION", @@ -158,6 +218,11 @@ private static ByteBuffer fetch(@NonNull String videoId, Map pla // Retry with different client if empty response body is received. for (ClientType clientType : clientTypesToUse) { + if (isUnplayableOrLiveStream(clientType, videoId)) { + Logger.printDebug(() -> "Ignore IOS spoofing as it is unplayable or a live stream (video: " + videoId + ")"); + continue; + } + HttpURLConnection connection = send(clientType, videoId, playerHeaders); if (connection != null) { try { diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/utils/InitializationPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/utils/InitializationPatch.java index 39d36a7a7a..c1f4f465d6 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/utils/InitializationPatch.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/utils/InitializationPatch.java @@ -5,21 +5,16 @@ import static app.revanced.integrations.shared.utils.Utils.runOnMainThreadDelayed; import android.app.Activity; -import android.app.AlertDialog; import androidx.annotation.NonNull; import app.revanced.integrations.shared.settings.BaseSettings; import app.revanced.integrations.shared.settings.BooleanSetting; -import app.revanced.integrations.shared.utils.Utils; -import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.youtube.utils.ExtendedUtils; @SuppressWarnings("unused") public class InitializationPatch { private static final BooleanSetting SETTINGS_INITIALIZED = BaseSettings.SETTINGS_INITIALIZED; - private static final BooleanSetting SPOOF_APP_VERSION = Settings.SPOOF_APP_VERSION; - private static final boolean hasRollingNumberIssue = Settings.SPOOF_APP_VERSION_TARGET.defaultValue.startsWith("19"); /** * Some layouts that depend on litho do not load when the app is first installed. @@ -31,13 +26,7 @@ public static void onCreate(@NonNull Activity mActivity) { if (SETTINGS_INITIALIZED.get()) { return; } - runOnMainThreadDelayed(() -> { - if (hasRollingNumberIssue) { - showSpoofAppVersionDialog(mActivity); - } else { - showRestartDialog(mActivity, str("revanced_extended_restart_first_run"), 3500); - } - }, 500); + runOnMainThreadDelayed(() -> showRestartDialog(mActivity, str("revanced_extended_restart_first_run"), 3500), 500); runOnMainThreadDelayed(() -> SETTINGS_INITIALIZED.save(true), 1000); } @@ -47,18 +36,4 @@ public static void setExtendedUtils(@NonNull Activity mActivity) { ExtendedUtils.setVersionName(); ExtendedUtils.setPlayerFlyoutMenuAdditionalSettings(); } - - private static void showSpoofAppVersionDialog(@NonNull Activity mActivity) { - new AlertDialog.Builder(mActivity) - .setMessage(str("revanced_extended_restart_first_run_rolling_number")) - .setPositiveButton(android.R.string.ok, (dialog, id) - -> Utils.runOnMainThreadDelayed(() -> { - SPOOF_APP_VERSION.save(true); - Utils.restartApp(mActivity); - }, 3500)) - .setNegativeButton(android.R.string.cancel, (dialog, id) - -> Utils.runOnMainThreadDelayed(() -> Utils.restartApp(mActivity), 3500)) - .setCancelable(false) - .show(); - } } \ No newline at end of file diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java b/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java index 7383f0b46b..bc617610ad 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java +++ b/app/src/main/java/app/revanced/integrations/youtube/patches/utils/PatchStatus.java @@ -47,9 +47,4 @@ public static boolean OldSeekbarThumbnailsDefaultBoolean() { return false; } - // Modified by a patch. Do not touch. - public static String SpoofAppVersionDefaultString() { - return "18.17.43"; - } - } diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java index 29acd99b01..5f13957ead 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java @@ -153,8 +153,7 @@ public class Settings extends BaseSettings { public static final EnumSetting CHANGE_LAYOUT = new EnumSetting<>("revanced_change_layout", FormFactor.ORIGINAL, true); public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", false, true, "revanced_spoof_app_version_user_dialog_message"); - public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", - PatchStatus.SpoofAppVersionDefaultString(), true, parent(SPOOF_APP_VERSION)); + public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "18.17.43", true, parent(SPOOF_APP_VERSION)); // PreferenceScreen: General - Account menu public static final BooleanSetting HIDE_ACCOUNT_MENU = new BooleanSetting("revanced_hide_account_menu", FALSE); @@ -544,10 +543,12 @@ public class Settings extends BaseSettings { public static final EnumSetting WATCH_HISTORY_TYPE = new EnumSetting<>("revanced_watch_history_type", WatchHistoryType.REPLACE); // PreferenceScreen: Miscellaneous - Spoof streaming data - public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", FALSE, true, "revanced_spoof_streaming_data_user_dialog_message"); + // 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_FORCE_AVC = new BooleanSetting("revanced_spoof_streaming_data_ios_force_avc", FALSE, true, "revanced_spoof_streaming_data_ios_force_avc_user_dialog_message", new SpoofStreamingDataPatch.ForceiOSAVCAvailability()); - public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.ANDROID_VR, true, parent(SPOOF_STREAMING_DATA)); + public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_COMPATIBILITY = new BooleanSetting("revanced_spoof_streaming_data_ios_compatibility", TRUE, true, new SpoofStreamingDataPatch.ForceiOSAVCAvailability()); + public static final EnumSetting 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 diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java index 58567071e0..db5061b80a 100644 --- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java +++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java @@ -76,7 +76,11 @@ private void updateUI() { final String summaryTextKey; if (selectableClientTypes.contains(clientType)) { - summaryTextKey = "revanced_spoof_streaming_data_side_effects_" + clientType.name().toLowerCase(); + if (clientType == ClientType.IOS && Settings.SPOOF_STREAMING_DATA_IOS_COMPATIBILITY.get()) { + summaryTextKey = "revanced_spoof_streaming_data_side_effects_ios_compatibility"; + } else { + summaryTextKey = "revanced_spoof_streaming_data_side_effects_" + clientType.name().toLowerCase(); + } } else { summaryTextKey = "revanced_spoof_streaming_data_side_effects_unknown"; } diff --git a/gradle.properties b/gradle.properties index af9711236b..ed203441e2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true android.useAndroidX = true -version = 0.158.0 +version = 0.159.0-dev.1