chore: Merge branch dev to main (#665)

This commit is contained in:
oSumAtrIX 2024-08-06 02:05:46 +02:00 committed by GitHub
commit e5736fc27f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 449 additions and 141 deletions

View File

@ -45,7 +45,7 @@ jobs:
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
fingerprint: ${{ env.GPG_FINGERPRINT }}
fingerprint: ${{ vars.GPG_FINGERPRINT }}
- name: Release
env:

View File

@ -7,7 +7,13 @@
}
],
"plugins": [
"@semantic-release/commit-analyzer",
[
"@semantic-release/commit-analyzer", {
"releaseRules": [
{ "type": "build", "scope": "Needs bump", "release": "patch" }
]
}
],
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"gradle-semantic-release-plugin",

View File

@ -1,3 +1,99 @@
# [1.12.0-dev.10](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.9...v1.12.0-dev.10) (2024-08-05)
### Bug Fixes
* **YouTube - Return YouTube Dislike:** Fix dislikes not appearing due to new component name ([#674](https://github.com/ReVanced/revanced-integrations/issues/674)) ([509e151](https://github.com/ReVanced/revanced-integrations/commit/509e1516f817bd736c3b2cc75bb5b48ab7de404a))
# [1.12.0-dev.9](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.8...v1.12.0-dev.9) (2024-08-04)
### Bug Fixes
* **YouTube - Spoof client:** Restore livestream audio only playback with iOS spoofing ([#673](https://github.com/ReVanced/revanced-integrations/issues/673)) ([5bf5fbd](https://github.com/ReVanced/revanced-integrations/commit/5bf5fbd1a79389895991f6b672d87373e96b698c))
# [1.12.0-dev.8](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.7...v1.12.0-dev.8) (2024-08-02)
### Bug Fixes
* **YouTube - SponsorBlock:** Improve create segment manual seek accuracy ([#671](https://github.com/ReVanced/revanced-integrations/issues/671)) ([34c02ae](https://github.com/ReVanced/revanced-integrations/commit/34c02aeb2a75bd95492e55958a446c9f99efdbb3))
# [1.12.0-dev.7](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.6...v1.12.0-dev.7) (2024-08-01)
### Features
* **YouTube - Description components:** Add `Hide 'Key concepts' section` option ([#670](https://github.com/ReVanced/revanced-integrations/issues/670)) ([86b25ea](https://github.com/ReVanced/revanced-integrations/commit/86b25ea468a132bd01e3fb1e2972cc903dd46d0c))
# [1.12.0-dev.6](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.5...v1.12.0-dev.6) (2024-07-28)
### Bug Fixes
* **YouTube - Keyword filter:** Filter videos from new subscription layout ([2f2eeea](https://github.com/ReVanced/revanced-integrations/commit/2f2eeea5a722b6b7053eb2825d16fa37938b4e9e))
# [1.12.0-dev.5](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.4...v1.12.0-dev.5) (2024-07-28)
### Bug Fixes
* **YouTube - Client Spoof:** Restore missing high qualities by spoofing the iOS client user agent ([#668](https://github.com/ReVanced/revanced-integrations/issues/668)) ([fbf629f](https://github.com/ReVanced/revanced-integrations/commit/fbf629fd6278440e70b0f1fb07e4cb7c412f0949))
# [1.12.0-dev.4](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.3...v1.12.0-dev.4) (2024-07-28)
### Bug Fixes
* **YouTube - Spoof client:** Fix tracking history on brand accounts ([#669](https://github.com/ReVanced/revanced-integrations/issues/669)) ([4ac698f](https://github.com/ReVanced/revanced-integrations/commit/4ac698fd4bd493d3830009853454a8f6566362b5))
# [1.12.0-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.2...v1.12.0-dev.3) (2024-07-26)
### Bug Fixes
* **YouTube - SponsorBlock:** Correctly show minute timestamp when creating a new segment ([e71955d](https://github.com/ReVanced/revanced-integrations/commit/e71955d5bbe58c1c634e82262d0e67dc65eca078))
# [1.12.0-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.12.0-dev.1...v1.12.0-dev.2) (2024-07-15)
### Bug Fixes
* **YouTube - Disable auto captions:** Do not break Shorts captions menu ([0345a00](https://github.com/ReVanced/revanced-integrations/commit/0345a00d6095797e275bb31f92ccda2e861f44c4))
# [1.12.0-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.11.2-dev.3...v1.12.0-dev.1) (2024-07-14)
### Features
* **YouTube:** Add `Bypass image region restrictions` patch ([#667](https://github.com/ReVanced/revanced-integrations/issues/667)) ([396ba77](https://github.com/ReVanced/revanced-integrations/commit/396ba77c207b438651ba6b83fb4b31e623544c00))
## [1.11.2-dev.3](https://github.com/ReVanced/revanced-integrations/compare/v1.11.2-dev.2...v1.11.2-dev.3) (2024-07-14)
## [1.11.2-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.11.2-dev.1...v1.11.2-dev.2) (2024-07-14)
### Bug Fixes
* **YouTube - Alternative thumbnails:** Always use primary thumbnail domain for still captures ([#666](https://github.com/ReVanced/revanced-integrations/issues/666)) ([7cdaf8d](https://github.com/ReVanced/revanced-integrations/commit/7cdaf8df146fdc0da8254a27d9c125f1e3d34765))
* **YouTube - Hide layout components:** Hide new type of horizontal shelf ([1fa59a6](https://github.com/ReVanced/revanced-integrations/commit/1fa59a62a17c63916808647331fa682d3de6aafb))
## [1.11.2-dev.2](https://github.com/ReVanced/revanced-integrations/compare/v1.11.2-dev.1...v1.11.2-dev.2) (2024-07-13)
### Bug Fixes
* **YouTube - Alternative thumbnails:** Always use primary thumbnail domain for still captures ([#666](https://github.com/ReVanced/revanced-integrations/issues/666)) ([7cdaf8d](https://github.com/ReVanced/revanced-integrations/commit/7cdaf8df146fdc0da8254a27d9c125f1e3d34765))
## [1.11.2-dev.1](https://github.com/ReVanced/revanced-integrations/compare/v1.11.1...v1.11.2-dev.1) (2024-07-12)
### Bug Fixes
* adjust blacklist ([d8d2a85](https://github.com/ReVanced/revanced-integrations/commit/d8d2a852d3879060bd95cc43d66c7cf195e82b43))
* **YouTube - Hide keyword content:** Do not hide flyout menu ([cda1f31](https://github.com/ReVanced/revanced-integrations/commit/cda1f3160c12d239df1183799ead39526cbac20f))
* **YouTube - Hide keyword content:** Do not hide flyout menu ([#664](https://github.com/ReVanced/revanced-integrations/issues/664)) ([120188d](https://github.com/ReVanced/revanced-integrations/commit/120188d6431b5500d6fde9cec136c752f8ee0ea4))
## [1.11.1](https://github.com/ReVanced/revanced-integrations/compare/v1.11.0...v1.11.1) (2024-07-11)

View File

@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin)
publishing
signing
}
android {
@ -53,28 +54,27 @@ dependencies {
compileOnly(project(":stub"))
}
tasks {
// Because the signing plugin doesn't support signing APKs, do it manually.
register("sign") {
group = "signing"
dependsOn(build)
tasks {
val assembleReleaseSignApk by registering {
dependsOn("assembleRelease")
val apk = layout.buildDirectory.file("outputs/apk/release/${rootProject.name}-$version.apk")
inputs.file(apk).withPropertyName("input")
outputs.file(apk.map { it.asFile.resolveSibling("${it.asFile.name}.asc") })
doLast {
val outputDirectory = layout.buildDirectory.dir("outputs/apk/release").get().asFile
val integrationsApk = outputDirectory.resolve("${rootProject.name}-$version.apk")
org.gradle.security.internal.gnupg.GnupgSignatoryFactory().createSignatory(project).sign(
integrationsApk.inputStream(),
outputDirectory.resolve("${integrationsApk.name}.asc").outputStream(),
)
signing {
useGpgCmd()
sign(*inputs.files.files.toTypedArray())
}
}
}
// Needed by gradle-semantic-release-plugin.
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435.
publish {
dependsOn(build)
dependsOn("sign")
dependsOn(assembleReleaseSignApk)
}
}

View File

@ -273,7 +273,6 @@ public class Utils {
@NonNull MatchFilter<View> filter) {
for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) {
View childAt = viewGroup.getChildAt(i);
Logger.printDebug(() -> "View id: " + childAt.getId() + " tag: " + childAt.getTag());
if (filter.matches(childAt)) {
//noinspection unchecked
@ -285,6 +284,7 @@ public class Utils {
if (match != null) return match;
}
}
return null;
}

View File

@ -190,16 +190,17 @@ public final class AlternativeThumbnailsPatch {
* Build the alternative thumbnail url using YouTube provided still video captures.
*
* @param decodedUrl Decoded original thumbnail request url.
* @return The alternative thumbnail url, or the original url. Both without tracking parameters.
* @return The alternative thumbnail url, or if not available NULL.
*/
@NonNull
private static String buildYoutubeVideoStillURL(@NonNull DecodedThumbnailUrl decodedUrl,
@Nullable
private static String buildYouTubeVideoStillURL(@NonNull DecodedThumbnailUrl decodedUrl,
@NonNull ThumbnailQuality qualityToUse) {
String sanitizedReplacement = decodedUrl.createStillsUrl(qualityToUse, false);
if (VerifiedQualities.verifyAltThumbnailExist(decodedUrl.videoId, qualityToUse, sanitizedReplacement)) {
return sanitizedReplacement;
}
return decodedUrl.sanitizedUrl;
return null;
}
/**
@ -284,14 +285,21 @@ public final class AlternativeThumbnailsPatch {
final boolean includeTracking;
if (option.useDeArrow && canUseDeArrowAPI()) {
includeTracking = false; // Do not include view tracking parameters with API call.
final String fallbackUrl = option.useStillImages
? buildYoutubeVideoStillURL(decodedUrl, qualityToUse)
: decodedUrl.sanitizedUrl;
String fallbackUrl = null;
if (option.useStillImages) {
fallbackUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse);
}
if (fallbackUrl == null) {
fallbackUrl = decodedUrl.sanitizedUrl;
}
sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl);
} else if (option.useStillImages) {
includeTracking = true; // Include view tracking parameters if present.
sanitizedReplacementUrl = buildYoutubeVideoStillURL(decodedUrl, qualityToUse);
sanitizedReplacementUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse);
if (sanitizedReplacementUrl == null) {
return originalUrl; // Still capture is not available. Return the untouched original url.
}
} else {
return originalUrl; // Recently experienced DeArrow failure and video stills are not enabled.
}
@ -345,7 +353,7 @@ public final class AlternativeThumbnailsPatch {
return; // Not a thumbnail.
}
Logger.printDebug(() -> "handleCronetSuccess, image not available: " + url);
Logger.printDebug(() -> "handleCronetSuccess, image not available: " + decodedUrl.sanitizedUrl);
ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedUrl.imageQuality);
if (quality == null) {
@ -627,14 +635,17 @@ public final class AlternativeThumbnailsPatch {
* YouTube video thumbnail url, decoded into it's relevant parts.
*/
private static class DecodedThumbnailUrl {
/**
* YouTube thumbnail URL prefix. Can be '/vi/' or '/vi_webp/'
*/
private static final String YOUTUBE_THUMBNAIL_PREFIX = "https://i.ytimg.com/vi";
private static final String YOUTUBE_THUMBNAIL_DOMAIN = "https://i.ytimg.com/";
@Nullable
static DecodedThumbnailUrl decodeImageUrl(String url) {
final int videoIdStartIndex = url.indexOf('/', YOUTUBE_THUMBNAIL_PREFIX.length()) + 1;
final int urlPathStartIndex = url.indexOf('/', "https://".length()) + 1;
if (urlPathStartIndex <= 0) return null;
final int urlPathEndIndex = url.indexOf('/', urlPathStartIndex);
if (urlPathEndIndex < 0) return null;
final int videoIdStartIndex = url.indexOf('/', urlPathEndIndex) + 1;
if (videoIdStartIndex <= 0) return null;
final int videoIdEndIndex = url.indexOf('/', videoIdStartIndex);
@ -647,15 +658,15 @@ public final class AlternativeThumbnailsPatch {
int imageExtensionEndIndex = url.indexOf('?', imageSizeEndIndex);
if (imageExtensionEndIndex < 0) imageExtensionEndIndex = url.length();
return new DecodedThumbnailUrl(url, videoIdStartIndex, videoIdEndIndex,
return new DecodedThumbnailUrl(url, urlPathStartIndex, urlPathEndIndex, videoIdStartIndex, videoIdEndIndex,
imageSizeStartIndex, imageSizeEndIndex, imageExtensionEndIndex);
}
final String originalFullUrl;
/** Full usable url, but stripped of any tracking information. */
final String sanitizedUrl;
/** Url up to the video ID. */
final String urlPrefix;
/** Url path, such as 'vi' or 'vi_webp' */
final String urlPath;
final String videoId;
/** Quality, such as hq720 or sddefault. */
final String imageQuality;
@ -664,11 +675,11 @@ public final class AlternativeThumbnailsPatch {
/** User view tracking parameters, only present on some images. */
final String viewTrackingParameters;
DecodedThumbnailUrl(String fullUrl, int videoIdStartIndex, int videoIdEndIndex,
DecodedThumbnailUrl(String fullUrl, int urlPathStartIndex, int urlPathEndIndex, int videoIdStartIndex, int videoIdEndIndex,
int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) {
originalFullUrl = fullUrl;
sanitizedUrl = fullUrl.substring(0, imageExtensionEndIndex);
urlPrefix = fullUrl.substring(0, videoIdStartIndex);
urlPath = fullUrl.substring(urlPathStartIndex, urlPathEndIndex);
videoId = fullUrl.substring(videoIdStartIndex, videoIdEndIndex);
imageQuality = fullUrl.substring(imageSizeStartIndex, imageSizeEndIndex);
imageExtension = fullUrl.substring(imageSizeEndIndex + 1, imageExtensionEndIndex);
@ -681,9 +692,12 @@ public final class AlternativeThumbnailsPatch {
// Images could be upgraded to webp if they are not already, but this fails quite often,
// especially for new videos uploaded in the last hour.
// And even if alt webp images do exist, sometimes they can load much slower than the original jpg alt images.
// (as much as 4x slower has been observed, despite the alt webp image being a smaller file).
// (as much as 4x slower network response has been observed, despite the alt webp image being a smaller file).
StringBuilder builder = new StringBuilder(originalFullUrl.length() + 2);
builder.append(urlPrefix);
// Many different "i.ytimage.com" domains exist such as "i9.ytimg.com",
// but still captures are frequently not available on the other domains (especially newly uploaded videos).
// So always use the primary domain for a higher success rate.
builder.append(YOUTUBE_THUMBNAIL_DOMAIN).append(urlPath).append('/');
builder.append(videoId).append('/');
builder.append(qualityToUse.getAltImageNameToUse());
builder.append('.').append(imageExtension);

View File

@ -0,0 +1,46 @@
package app.revanced.integrations.youtube.patches;
import static app.revanced.integrations.youtube.settings.Settings.BYPASS_IMAGE_REGION_RESTRICTIONS;
import java.util.regex.Pattern;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class BypassImageRegionRestrictionsPatch {
private static final boolean BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED = BYPASS_IMAGE_REGION_RESTRICTIONS.get();
private static final String REPLACEMENT_IMAGE_DOMAIN = "https://yt4.ggpht.com";
/**
* YouTube static images domain. Includes user and channel avatar images and community post images.
*/
private static final Pattern YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN
= Pattern.compile("^https://(yt3|lh[3-6]|play-lh)\\.(ggpht|googleusercontent)\\.com");
/**
* Injection point. Called off the main thread and by multiple threads at the same time.
*
* @param originalUrl Image url for all image urls loaded.
*/
public static String overrideImageURL(String originalUrl) {
try {
if (BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED) {
String replacement = YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN
.matcher(originalUrl).replaceFirst(REPLACEMENT_IMAGE_DOMAIN);
if (Settings.DEBUG.get() && !replacement.equals(originalUrl)) {
Logger.printDebug(() -> "Replaced: '" + originalUrl + "' with: '" + replacement + "'");
}
return replacement;
}
} catch (Exception ex) {
Logger.printException(() -> "overrideImageURL failure", ex);
}
return originalUrl;
}
}

View File

@ -1,6 +1,7 @@
package app.revanced.integrations.youtube.patches;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.shared.PlayerType;
@SuppressWarnings("unused")
public class DisableAutoCaptionsPatch {
@ -11,7 +12,9 @@ public class DisableAutoCaptionsPatch {
public static boolean captionsButtonDisabled;
public static boolean autoCaptionsEnabled() {
return Settings.AUTO_CAPTIONS.get();
return Settings.AUTO_CAPTIONS.get()
// Do not use auto captions for Shorts.
&& !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized();
}
}

View File

@ -221,12 +221,12 @@ public class ReturnYouTubeDislikePatch {
String conversionContextString = conversionContext.toString();
if (isRollingNumber && !conversionContextString.contains("video_action_bar.eml|")) {
if (isRollingNumber && !conversionContextString.contains("video_action_bar.eml")) {
return original;
}
final CharSequence replacement;
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
if (conversionContextString.contains("segmented_like_dislike_button.eml")) {
// Regular video.
ReturnYouTubeDislike videoData = currentVideoData;
if (videoData == null) {

View File

@ -7,7 +7,6 @@ import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.Objects;
/**
@ -15,17 +14,21 @@ import java.util.Objects;
* @noinspection unused
*/
public final class VideoInformation {
public interface PlaybackController {
// Methods are added to YT classes during patching.
boolean seekTo(long videoTime);
boolean seekToRelative(long videoTimeOffset);
}
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
private static final String SEEK_METHOD_NAME = "seekTo";
/**
* Prefix present in all Short player parameters signature.
*/
private static final String SHORTS_PLAYER_PARAMETERS = "8AEB";
private static WeakReference<Object> playerControllerRef;
private static WeakReference<Object> mdxPlayerDirectorRef;
private static Method seekMethod;
private static Method mdxSeekMethod;
private static WeakReference<PlaybackController> playerControllerRef = new WeakReference<>(null);
private static WeakReference<PlaybackController> mdxPlayerDirectorRef = new WeakReference<>(null);
@NonNull
private static String videoId = "";
@ -47,15 +50,12 @@ public final class VideoInformation {
*
* @param playerController player controller object.
*/
public static void initialize(@NonNull Object playerController) {
public static void initialize(@NonNull PlaybackController playerController) {
try {
playerControllerRef = new WeakReference<>(Objects.requireNonNull(playerController));
videoTime = -1;
videoLength = 0;
playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
seekMethod = playerController.getClass().getMethod(SEEK_METHOD_NAME, Long.TYPE);
seekMethod.setAccessible(true);
} catch (Exception ex) {
Logger.printException(() -> "Failed to initialize", ex);
}
@ -66,12 +66,9 @@ public final class VideoInformation {
*
* @param mdxPlayerDirector MDX player director object (casting mode).
*/
public static void initializeMdx(@NonNull Object mdxPlayerDirector) {
public static void initializeMdx(@NonNull PlaybackController mdxPlayerDirector) {
try {
mdxPlayerDirectorRef = new WeakReference<>(Objects.requireNonNull(mdxPlayerDirector));
mdxSeekMethod = mdxPlayerDirector.getClass().getMethod(SEEK_METHOD_NAME, Long.TYPE);
mdxSeekMethod.setAccessible(true);
} catch (Exception ex) {
Logger.printException(() -> "Failed to initialize MDX", ex);
}
@ -195,42 +192,80 @@ public final class VideoInformation {
return false;
}
Logger.printDebug(() -> "Seeking to " + adjustedSeekTime);
Logger.printDebug(() -> "Seeking to: " + adjustedSeekTime);
try {
//noinspection DataFlowIssue
if ((Boolean) seekMethod.invoke(playerControllerRef.get(), adjustedSeekTime)) {
return true;
} // Else the video is loading or changing videos, or video is casting to a different device.
} catch (Exception ex) {
Logger.printInfo(() -> "seekTo method call failed", ex);
// Try regular playback controller first, and it will not succeed if casting.
PlaybackController controller = playerControllerRef.get();
if (controller == null) {
Logger.printDebug(() -> "Cannot seekTo because player controller is null");
} else {
if (controller.seekTo(adjustedSeekTime)) return true;
Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD.");
// Else the video is loading or changing videos, or video is casting to a different device.
}
// Try calling the seekTo method of the MDX player director (called when casting).
// The difference has to be a different second mark in order to avoid infinite skip loops
// as the Lounge API only supports seconds.
if ((adjustedSeekTime / 1000) == (videoTime / 1000)) {
Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small ("
+ (adjustedSeekTime - videoTime) + "ms)");
return false;
}
try {
//noinspection DataFlowIssue
return (Boolean) mdxSeekMethod.invoke(mdxPlayerDirectorRef.get(), adjustedSeekTime);
} catch (Exception ex) {
Logger.printInfo(() -> "seekTo (MDX) method call failed", ex);
if (adjustedSeekTime / 1000 == videoTime / 1000) {
Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small "
+ "(" + (adjustedSeekTime - videoTime) + "ms)");
return false;
}
controller = mdxPlayerDirectorRef.get();
if (controller == null) {
Logger.printDebug(() -> "Cannot seekTo MXD because player controller is null");
return false;
}
return controller.seekTo(adjustedSeekTime);
} catch (Exception ex) {
Logger.printException(() -> "Failed to seek", ex);
return false;
}
}
/** @noinspection UnusedReturnValue*/
public static boolean seekToRelative(long millisecondsRelative) {
return seekTo(videoTime + millisecondsRelative);
/**
* Seeks a relative amount. Should always be used over {@link #seekTo(long)}
* when the desired seek time is an offset of the current time.
*
* @noinspection UnusedReturnValue
*/
public static boolean seekToRelative(long seekTime) {
Utils.verifyOnMainThread();
try {
Logger.printDebug(() -> "Seeking relative to: " + seekTime);
// Try regular playback controller first, and it will not succeed if casting.
PlaybackController controller = playerControllerRef.get();
if (controller == null) {
Logger.printDebug(() -> "Cannot seek relative as player controller is null");
} else {
if (controller.seekToRelative(seekTime)) return true;
Logger.printDebug(() -> "seekToRelative did not succeeded. Trying MXD.");
}
// Adjust the fine adjustment function so it's at least 1 second before/after.
// Otherwise the fine adjustment will do nothing when casting.
final long adjustedSeekTime;
if (seekTime < 0) {
adjustedSeekTime = Math.min(seekTime, -1000);
} else {
adjustedSeekTime = Math.max(seekTime, 1000);
}
controller = mdxPlayerDirectorRef.get();
if (controller == null) {
Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null");
return false;
}
return controller.seekToRelative(adjustedSeekTime);
} catch (Exception ex) {
Logger.printException(() -> "Failed to seek relative", ex);
return false;
}
}
/**

View File

@ -1,14 +1,19 @@
package app.revanced.integrations.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.StringTrieSearch;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
final class DescriptionComponentsFilter extends Filter {
private final StringTrieSearch exceptions = new StringTrieSearch();
private final ByteArrayFilterGroupList macroMarkersCarouselGroupList = new ByteArrayFilterGroupList();
private final StringFilterGroup macroMarkersCarousel;
public DescriptionComponentsFilter() {
exceptions.addPatterns(
"compact_channel",
@ -25,11 +30,6 @@ final class DescriptionComponentsFilter extends Filter {
"video_attributes_section"
);
final StringFilterGroup chaptersSection = new StringFilterGroup(
Settings.HIDE_CHAPTERS_SECTION,
"macro_markers_carousel"
);
final StringFilterGroup infoCardsSection = new StringFilterGroup(
Settings.HIDE_INFO_CARDS_SECTION,
"infocards_section"
@ -45,21 +45,44 @@ final class DescriptionComponentsFilter extends Filter {
"transcript_section"
);
macroMarkersCarousel = new StringFilterGroup(
null,
"macro_markers_carousel.eml"
);
macroMarkersCarouselGroupList.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_CHAPTERS_SECTION,
"chapters_horizontal_shelf"
),
new ByteArrayFilterGroup(
Settings.HIDE_KEY_CONCEPTS_SECTION,
"learning_concept_macro_markers_carousel_shelf"
)
);
addPathCallbacks(
attributesSection,
chaptersSection,
infoCardsSection,
podcastSection,
transcriptSection
transcriptSection,
macroMarkersCarousel
);
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (exceptions.matches(path)) return false;
if (matchedGroup == macroMarkersCarousel) {
if (contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}
}

View File

@ -18,6 +18,7 @@ import java.util.concurrent.atomic.AtomicReference;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.youtube.ByteTrieSearch;
import app.revanced.integrations.youtube.StringTrieSearch;
import app.revanced.integrations.youtube.TrieSearch;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.shared.NavigationBar;
@ -59,11 +60,15 @@ final class KeywordContentFilter extends Filter {
*/
private static final String[] STRINGS_IN_EVERY_BUFFER = {
// Video playback data.
"https://i.ytimg.com/vi/", // Thumbnail url.
"sddefault.jpg", // More video sizes exist, but for most devices only these 2 are used.
"hqdefault.webp",
"googlevideo.com/initplayback?source=youtube", // Video url.
"ANDROID", // Video url parameter.
"https://i.ytimg.com/vi/", // Thumbnail url.
"mqdefault.jpg",
"hqdefault.jpg",
"sddefault.jpg",
"hq720.jpg",
"webp",
"_custom_", // Custom thumbnail set by video creator.
// Video decoders.
"OMX.ffmpeg.vp9.decoder",
"OMX.Intel.sw_vd.vp9",
@ -75,15 +80,22 @@ final class KeywordContentFilter extends Filter {
"c2.android.av1-dav1d.decoder",
"c2.android.vp9.decoder",
"c2.mtk.sw.vp9.decoder",
// User analytics.
"https://ad.doubleclick.net/ddm/activity/",
"DEVICE_ADVERTISER_ID_FOR_CONVERSION_TRACKING",
"tag_for_child_directed_treatment", // Found in overflow menu such as 'Watch later'.
// Litho components frequently found in the buffer that belong to the path filter items.
// Analytics.
"searchR",
"browse-feed",
"FEwhat_to_watch",
"FEsubscriptions",
"search_vwc_description_transition_key",
"g-high-recZ",
// Text and litho components found in the buffer that belong to path filters.
"metadata.eml",
"thumbnail.eml",
"avatar.eml",
"overflow_button.eml",
"shorts-lockup-image",
"shorts-lockup.overlay-metadata.secondary-text",
"YouTubeSans-SemiBold",
"sans-serif"
};
/**
@ -95,6 +107,7 @@ final class KeywordContentFilter extends Filter {
"search_video_with_context.eml",
"video_with_context.eml", // Subscription tab videos.
"related_video_with_context.eml",
"video_lockup_with_attachment.eml", // A/B test for subscribed video.
"compact_video.eml",
"inline_shorts",
"shorts_video_cell",
@ -108,10 +121,24 @@ final class KeywordContentFilter extends Filter {
private final StringFilterGroup containsFilter = new StringFilterGroup(
null,
"modern_type_shelf_header_content.eml",
"shorts_lockup_cell.eml", // Part of 'shorts_shelf_carousel.eml'
"shorts_lockup_cell.eml", // Part of 'shorts_shelf_carousel.eml'
"video_card.eml" // Shorts that appear in a horizontal shelf.
);
/**
* Path components to not filter. Cannot filter the buffer when these are present,
* otherwise text in UI controls can be filtered as a keyword (such as using "Playlist" as a keyword).
*
* This is also a small performance improvement since
* the buffer of the parent component was already searched and passed.
*/
private final StringTrieSearch exceptions = new StringTrieSearch(
"metadata.eml",
"thumbnail.eml",
"avatar.eml",
"overflow_button.eml"
);
/**
* Threshold for {@link #filteredVideosPercentage}
* that indicates all or nearly all videos have been filtered.
@ -121,7 +148,7 @@ final class KeywordContentFilter extends Filter {
private static final float ALL_VIDEOS_FILTERED_SAMPLE_SIZE = 50;
private static final long ALL_VIDEOS_FILTERED_TIMEOUT_MILLISECONDS = 60 * 1000; // 60 seconds
private static final long ALL_VIDEOS_FILTERED_BACKOFF_MILLISECONDS = 60 * 1000; // 60 seconds
/**
* Rolling average of how many videos were filtered by a keyword.
@ -140,7 +167,7 @@ final class KeywordContentFilter extends Filter {
/**
* If filtering is temporarily turned off, the time to resume filtering.
* Field is zero if no timeout is in effect.
* Field is zero if no backoff is in effect.
*/
private volatile long timeToResumeFiltering;
@ -339,7 +366,7 @@ final class KeywordContentFilter extends Filter {
// A keyword is hiding everything.
// Inform the user, and temporarily turn off filtering.
timeToResumeFiltering = System.currentTimeMillis() + ALL_VIDEOS_FILTERED_TIMEOUT_MILLISECONDS;
timeToResumeFiltering = System.currentTimeMillis() + ALL_VIDEOS_FILTERED_BACKOFF_MILLISECONDS;
Logger.printDebug(() -> "Temporarily turning off filtering due to excessively broad filter: " + keyword);
Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_broad", keyword));
@ -361,6 +388,10 @@ final class KeywordContentFilter extends Filter {
if (!hideKeywordSettingIsActive()) return false;
if (exceptions.matches(path)) {
return false; // Do not update statistics.
}
MutableReference<String> matchRef = new MutableReference<>();
if (bufferSearch.matches(protobufBufferArray, matchRef)) {
updateStats(true, matchRef.value);

View File

@ -250,6 +250,7 @@ public final class LayoutComponentsFilter extends Filter {
Settings.HIDE_HORIZONTAL_SHELVES,
"horizontal_video_shelf.eml",
"horizontal_shelf.eml",
"horizontal_shelf_inline.eml",
"horizontal_tile_shelf.eml"
);

View File

@ -1,19 +1,20 @@
package app.revanced.integrations.youtube.patches.playback.quality;
import androidx.annotation.Nullable;
import static app.revanced.integrations.shared.StringRef.str;
import static app.revanced.integrations.shared.Utils.NetworkType;
import app.revanced.integrations.shared.settings.IntegerSetting;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import androidx.annotation.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import static app.revanced.integrations.shared.StringRef.str;
import static app.revanced.integrations.shared.Utils.NetworkType;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.IntegerSetting;
import app.revanced.integrations.youtube.patches.VideoInformation;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public class RememberVideoQualityPatch {
@ -158,7 +159,7 @@ public class RememberVideoQualityPatch {
/**
* Injection point.
*/
public static void newVideoStarted(Object ignoredPlayerController) {
public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) {
Logger.printDebug(() -> "newVideoStarted");
qualityNeedsUpdating = true;
videoQualities = null;

View File

@ -13,7 +13,7 @@ public final class RememberPlaybackSpeedPatch {
/**
* Injection point.
*/
public static void newVideoStarted(Object ignoredPlayerController) {
public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) {
Logger.printDebug(() -> "newVideoStarted");
VideoInformation.overridePlaybackSpeed(Settings.PLAYBACK_SPEED_DEFAULT.get());
}

View File

@ -5,12 +5,15 @@ import android.media.MediaCodecList;
import android.net.Uri;
import android.os.Build;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.youtube.patches.BackgroundPlaybackPatch;
import app.revanced.integrations.youtube.settings.Settings;
import org.chromium.net.ExperimentalUrlRequest;
@SuppressWarnings("unused")
public class SpoofClientPatch {
private static final boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get();
private static final ClientType SPOOF_CLIENT_TYPE = Settings.SPOOF_CLIENT_USE_IOS.get() ? ClientType.IOS : ClientType.ANDROID_VR;
private static final boolean SPOOFING_TO_IOS = SPOOF_CLIENT_ENABLED && SPOOF_CLIENT_TYPE == ClientType.IOS;
/**
* Any unreachable ip address. Used to intentionally fail requests.
@ -45,7 +48,7 @@ public class SpoofClientPatch {
/**
* Injection point.
*
* <p>
* Blocks /initplayback requests.
*/
public static String blockInitPlaybackRequest(String originalUrlString) {
@ -71,33 +74,29 @@ public class SpoofClientPatch {
* Injection point.
*/
public static int getClientTypeId(int originalClientTypeId) {
if (SPOOF_CLIENT_ENABLED) {
return SPOOF_CLIENT_TYPE.id;
}
return originalClientTypeId;
return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.id : originalClientTypeId;
}
/**
* Injection point.
*/
public static String getClientVersion(String originalClientVersion) {
if (SPOOF_CLIENT_ENABLED) {
return SPOOF_CLIENT_TYPE.version;
}
return originalClientVersion;
return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.version : originalClientVersion;
}
/**
* Injection point.
*/
public static String getClientModel(String originalClientModel) {
if (SPOOF_CLIENT_ENABLED) {
return SPOOF_CLIENT_TYPE.model;
}
return SPOOF_CLIENT_ENABLED ? SPOOF_CLIENT_TYPE.model : originalClientModel;
}
return originalClientModel;
/**
* Injection point.
* Fix video qualities missing, if spoofing to iOS by using the correct client OS version.
*/
public static String getOsVersion(String originalOsVersion) {
return SPOOFING_TO_IOS ? ClientType.IOS.osVersion : originalOsVersion;
}
/**
@ -120,16 +119,41 @@ public class SpoofClientPatch {
* Return true to force create the playback speed menu.
*/
public static boolean forceCreatePlaybackSpeedMenu(boolean original) {
if (SPOOF_CLIENT_ENABLED && SPOOF_CLIENT_TYPE == ClientType.IOS) {
return true;
return SPOOFING_TO_IOS || original;
}
/**
* Injection point.
* When spoofing the client to iOS, background audio only playback of livestreams fails.
* Return true to force enable audio background play.
*/
public static boolean overrideBackgroundAudioPlayback() {
return SPOOFING_TO_IOS && BackgroundPlaybackPatch.playbackIsNotShort();
}
/**
* Injection point.
* Fix video qualities missing, if spoofing to iOS by using the correct iOS user-agent.
*/
public static ExperimentalUrlRequest overrideUserAgent(ExperimentalUrlRequest.Builder builder, String url) {
if (SPOOFING_TO_IOS) {
String path = Uri.parse(url).getPath();
if (path != null && path.contains("player")) {
return builder.addHeader("User-Agent", ClientType.IOS.userAgent).build();
}
}
return original;
return builder.build();
}
private enum ClientType {
// https://dumps.tadiphone.dev/dumps/oculus/eureka
ANDROID_VR(28, "Quest 3", "1.56.21"),
ANDROID_VR(28,
"Quest 3",
"1.56.21",
"12",
"com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip"
),
// 11,4 = iPhone XS Max.
// 16,2 = iPhone 15 Pro Max.
// Since the 15 supports AV1 hardware decoding, only spoof that device if this
@ -137,7 +161,12 @@ public class SpoofClientPatch {
//
// Version number should be a valid iOS release.
// https://www.ipa4fun.com/history/185230
IOS(5, deviceHasAV1HardwareDecoding() ? "iPhone16,2" : "iPhone11,4", "19.10.7");
IOS(5,
deviceHasAV1HardwareDecoding() ? "iPhone16,2" : "iPhone11,4",
"19.10.7",
"17.5.1.21F90",
"com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)"
);
/**
* YouTube
@ -155,10 +184,22 @@ public class SpoofClientPatch {
*/
final String version;
ClientType(int id, String model, String version) {
/**
* Device OS version.
*/
final String osVersion;
/**
* Player user-agent.
*/
final String userAgent;
ClientType(int id, String model, String version, String osVersion, String userAgent) {
this.id = id;
this.model = model;
this.version = version;
this.osVersion = osVersion;
this.userAgent = userAgent;
}
}

View File

@ -157,6 +157,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_ATTRIBUTES_SECTION = new BooleanSetting("revanced_hide_attributes_section", FALSE);
public static final BooleanSetting HIDE_CHAPTERS_SECTION = new BooleanSetting("revanced_hide_chapters_section", TRUE);
public static final BooleanSetting HIDE_INFO_CARDS_SECTION = new BooleanSetting("revanced_hide_info_cards_section", TRUE);
public static final BooleanSetting HIDE_KEY_CONCEPTS_SECTION = new BooleanSetting("revanced_hide_key_concepts_section", FALSE);
public static final BooleanSetting HIDE_PODCAST_SECTION = new BooleanSetting("revanced_hide_podcast_section", TRUE);
public static final BooleanSetting HIDE_TRANSCRIPT_SECTION = new BooleanSetting("revanced_hide_transcript_section", TRUE);
@ -189,6 +190,7 @@ public class Settings extends BaseSettings {
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "17.33.42", true, parent(SPOOF_APP_VERSION));
public static final BooleanSetting TABLET_LAYOUT = new BooleanSetting("revanced_tablet_layout", FALSE, true, "revanced_tablet_layout_user_dialog_message");
public static final BooleanSetting WIDE_SEARCHBAR = new BooleanSetting("revanced_wide_searchbar", FALSE, true);
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE,
"revanced_remove_viewer_discretion_dialog_user_dialog_message");

View File

@ -11,11 +11,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.*;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
@ -182,7 +178,7 @@ public class SegmentPlaybackController {
* Injection point.
* Initializes SponsorBlock when the video player starts playing a new video.
*/
public static void initialize(Object ignoredPlayerController) {
public static void initialize(VideoInformation.PlaybackController ignoredPlayerController) {
try {
Utils.verifyOnMainThread();
SponsorBlockSettings.initialize();
@ -632,6 +628,7 @@ public class SegmentPlaybackController {
/**
* Injection point
*/
@SuppressWarnings("unused")
public static void setSponsorBarRect(final Object self) {
try {
Field field = self.getClass().getDeclaredField("replaceMeWithsetSponsorBarRect");
@ -663,6 +660,7 @@ public class SegmentPlaybackController {
/**
* Injection point
*/
@SuppressWarnings("unused")
public static void setSponsorBarThickness(int thickness) {
if (sponsorBarThickness != thickness) {
Logger.printDebug(() -> "setSponsorBarThickness: " + thickness);
@ -673,6 +671,7 @@ public class SegmentPlaybackController {
/**
* Injection point.
*/
@SuppressWarnings("unused")
public static String appendTimeWithoutSegments(String totalTime) {
try {
if (Settings.SB_ENABLED.get() && Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get()
@ -725,9 +724,9 @@ public class SegmentPlaybackController {
final long minutes = (timeWithoutSegmentsValue / 60000) % 60;
final long seconds = (timeWithoutSegmentsValue / 1000) % 60;
if (hours > 0) {
timeWithoutSegments = String.format("\u2009(%d:%02d:%02d)", hours, minutes, seconds);
timeWithoutSegments = String.format(Locale.ENGLISH, "\u2009(%d:%02d:%02d)", hours, minutes, seconds);
} else {
timeWithoutSegments = String.format("\u2009(%d:%02d)", minutes, seconds);
timeWithoutSegments = String.format(Locale.ENGLISH, "\u2009(%d:%02d)", minutes, seconds);
}
}
@ -744,6 +743,7 @@ public class SegmentPlaybackController {
/**
* Injection point.
*/
@SuppressWarnings("unused")
public static void drawSponsorTimeBars(final Canvas canvas, final float posY) {
try {
if (segments == null) return;

View File

@ -234,9 +234,7 @@ public class SponsorBlockUtils {
new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext())
.setTitle(str("revanced_sb_new_segment_title"))
.setMessage(str("revanced_sb_new_segment_mark_time_as_question",
newSponsorSegmentDialogShownMillis / 3600000,
newSponsorSegmentDialogShownMillis / 1000 % 60,
newSponsorSegmentDialogShownMillis % 1000))
formatSegmentTime(newSponsorSegmentDialogShownMillis)))
.setNeutralButton(android.R.string.cancel, null)
.setNegativeButton(str("revanced_sb_new_segment_mark_start"), newSponsorSegmentDialogListener)
.setPositiveButton(str("revanced_sb_new_segment_mark_end"), newSponsorSegmentDialogListener)
@ -448,17 +446,20 @@ public class SponsorBlockUtils {
Duration duration = Duration.ofSeconds(totalSecondsSaved);
final long hours = duration.toHours();
final long minutes = duration.toMinutes() % 60;
// Format all numbers so non-western numbers use a consistent appearance.
String minutesFormatted = statsNumberFormatter.format(minutes);
if (hours > 0) {
String hoursFormatted = statsNumberFormatter.format(hours);
return str("revanced_sb_stats_saved_hour_format", hoursFormatted, minutesFormatted);
}
final long seconds = duration.getSeconds() % 60;
String secondsFormatted = statsNumberFormatter.format(seconds);
if (minutes > 0) {
return str("revanced_sb_stats_saved_minute_format", minutesFormatted, secondsFormatted);
}
return str("revanced_sb_stats_saved_second_format", secondsFormatted);
}
return "error"; // will never be reached. YouTube requires Android O or greater

View File

@ -1,4 +1,4 @@
org.gradle.parallel = true
org.gradle.caching = true
android.useAndroidX = true
version = 1.11.1
version = 1.12.0-dev.10

View File

@ -0,0 +1,8 @@
package org.chromium.net;
public abstract class ExperimentalUrlRequest {
public abstract class Builder {
public abstract ExperimentalUrlRequest.Builder addHeader(String name, String value);
public abstract ExperimentalUrlRequest build();
}
}