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