mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-01-04 00:55:49 +01:00
fix(YouTube - ReturnYouTubeDislike): Show estimated like count for videos with hidden likes (#684)
This commit is contained in:
parent
55c278dc08
commit
27d2b60444
@ -363,6 +363,23 @@ public class Utils {
|
||||
return isRightToLeftTextLayout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the text contains at least 1 number character,
|
||||
* including any unicode numbers such as Arabic.
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public static boolean containsNumber(@NonNull CharSequence text) {
|
||||
for (int index = 0, length = text.length(); index < length;) {
|
||||
final int codePoint = Character.codePointAt(text, index);
|
||||
if (Character.isDigit(codePoint)) {
|
||||
return true;
|
||||
}
|
||||
index += Character.charCount(codePoint);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe to call from any thread
|
||||
*/
|
||||
|
@ -225,7 +225,6 @@ public class ReturnYouTubeDislikePatch {
|
||||
return original;
|
||||
}
|
||||
|
||||
final CharSequence replacement;
|
||||
if (conversionContextString.contains("segmented_like_dislike_button.eml")) {
|
||||
// Regular video.
|
||||
ReturnYouTubeDislike videoData = currentVideoData;
|
||||
@ -235,46 +234,62 @@ public class ReturnYouTubeDislikePatch {
|
||||
if (!(original instanceof Spanned)) {
|
||||
original = new SpannableString(original);
|
||||
}
|
||||
replacement = videoData.getDislikesSpanForRegularVideo((Spanned) original,
|
||||
return videoData.getDislikesSpanForRegularVideo((Spanned) original,
|
||||
true, isRollingNumber);
|
||||
} else if (!isRollingNumber && conversionContextString.contains("|shorts_dislike_button.eml|")) {
|
||||
// Litho Shorts player.
|
||||
if (!Settings.RYD_SHORTS.get() || Settings.HIDE_SHORTS_DISLIKE_BUTTON.get()) {
|
||||
// Must clear the current video here, otherwise if the user opens a regular video
|
||||
// then opens a litho short (while keeping the regular video on screen), then closes the short,
|
||||
// the original video may show the incorrect dislike value.
|
||||
clearData();
|
||||
return original;
|
||||
}
|
||||
ReturnYouTubeDislike videoData = lastLithoShortsVideoData;
|
||||
if (videoData == null) {
|
||||
// The Shorts litho video id filter did not detect the video id.
|
||||
// This is normal in incognito mode, but otherwise is abnormal.
|
||||
Logger.printDebug(() -> "Cannot modify Shorts litho span, data is null");
|
||||
return original;
|
||||
}
|
||||
// Use the correct dislikes data after voting.
|
||||
if (lithoShortsShouldUseCurrentData) {
|
||||
lithoShortsShouldUseCurrentData = false;
|
||||
videoData = currentVideoData;
|
||||
if (videoData == null) {
|
||||
Logger.printException(() -> "currentVideoData is null"); // Should never happen
|
||||
return original;
|
||||
}
|
||||
Logger.printDebug(() -> "Using current video data for litho span");
|
||||
}
|
||||
replacement = videoData.getDislikeSpanForShort((Spanned) original);
|
||||
} else {
|
||||
return original;
|
||||
}
|
||||
|
||||
return replacement;
|
||||
if (isRollingNumber) {
|
||||
return original; // No need to check for Shorts in the context.
|
||||
}
|
||||
|
||||
if (conversionContextString.contains("|shorts_dislike_button.eml")) {
|
||||
return getShortsSpan(original, true);
|
||||
}
|
||||
|
||||
if (conversionContextString.contains("|shorts_like_button.eml")
|
||||
&& !Utils.containsNumber(original)) {
|
||||
Logger.printDebug(() -> "Replacing hidden likes count");
|
||||
return getShortsSpan(original, false);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onLithoTextLoaded failure", ex);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
private static CharSequence getShortsSpan(@NonNull CharSequence original, boolean isDislikesSpan) {
|
||||
// Litho Shorts player.
|
||||
if (!Settings.RYD_SHORTS.get() || (isDislikesSpan && Settings.HIDE_SHORTS_DISLIKE_BUTTON.get())
|
||||
|| (!isDislikesSpan && Settings.HIDE_SHORTS_LIKE_BUTTON.get())) {
|
||||
return original;
|
||||
}
|
||||
|
||||
ReturnYouTubeDislike videoData = lastLithoShortsVideoData;
|
||||
if (videoData == null) {
|
||||
// The Shorts litho video id filter did not detect the video id.
|
||||
// This is normal in incognito mode, but otherwise is abnormal.
|
||||
Logger.printDebug(() -> "Cannot modify Shorts litho span, data is null");
|
||||
return original;
|
||||
}
|
||||
|
||||
// Use the correct dislikes data after voting.
|
||||
if (lithoShortsShouldUseCurrentData) {
|
||||
if (isDislikesSpan) {
|
||||
lithoShortsShouldUseCurrentData = false;
|
||||
}
|
||||
videoData = currentVideoData;
|
||||
if (videoData == null) {
|
||||
Logger.printException(() -> "currentVideoData is null"); // Should never happen
|
||||
return original;
|
||||
}
|
||||
Logger.printDebug(() -> "Using current video data for litho span");
|
||||
}
|
||||
|
||||
return isDislikesSpan
|
||||
? videoData.getDislikeSpanForShort((Spanned) original)
|
||||
: videoData.getLikeSpanForShort((Spanned) original);
|
||||
}
|
||||
|
||||
//
|
||||
// Rolling Number
|
||||
//
|
||||
@ -597,6 +612,7 @@ public class ReturnYouTubeDislikePatch {
|
||||
Logger.printDebug(() -> "Waiting for prefetch to complete: " + videoId);
|
||||
fetch.getFetchData(20000); // Any arbitrarily large max wait time.
|
||||
}
|
||||
|
||||
// Set the fields after the fetch completes, so any concurrent calls will also wait.
|
||||
lastPlayerResponseWasShort = videoIdIsShort;
|
||||
lastPrefetchedVideoId = videoId;
|
||||
@ -657,6 +673,7 @@ public class ReturnYouTubeDislikePatch {
|
||||
clearData();
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "New litho Shorts video id: " + videoId);
|
||||
ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
videoData.setVideoIdIsShort(true);
|
||||
|
@ -52,7 +52,7 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
|
||||
@SuppressWarnings("unused")
|
||||
public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) {
|
||||
try {
|
||||
if (!isShortAndOpeningOrPlaying || !Settings.RYD_SHORTS.get()) {
|
||||
if (!isShortAndOpeningOrPlaying || !Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
|
||||
return;
|
||||
}
|
||||
synchronized (lastVideoIds) {
|
||||
@ -68,21 +68,28 @@ public final class ReturnYouTubeDislikeFilterPatch extends Filter {
|
||||
private final ByteArrayFilterGroupList videoIdFilterGroup = new ByteArrayFilterGroupList();
|
||||
|
||||
public ReturnYouTubeDislikeFilterPatch() {
|
||||
// Likes always seems to load before the dislikes, but if this
|
||||
// ever changes then both likes and dislikes need callbacks.
|
||||
addPathCallbacks(
|
||||
new StringFilterGroup(Settings.RYD_SHORTS, "|shorts_dislike_button.eml|")
|
||||
new StringFilterGroup(null, "|shorts_like_button.eml")
|
||||
);
|
||||
// After the dislikes icon name is some binary data and then the video id for that specific short.
|
||||
|
||||
// After the likes icon name is some binary data and then the video id for that specific short.
|
||||
videoIdFilterGroup.addAll(
|
||||
// Video was previously disliked before video was opened.
|
||||
new ByteArrayFilterGroup(null, "ic_right_dislike_on_shadowed"),
|
||||
// Video was not already disliked.
|
||||
new ByteArrayFilterGroup(null, "ic_right_dislike_off_shadowed")
|
||||
// Video was previously liked before video was opened.
|
||||
new ByteArrayFilterGroup(null, "ic_right_like_on_shadowed"),
|
||||
// Video was not already liked.
|
||||
new ByteArrayFilterGroup(null, "ic_right_like_off_shadowed")
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray);
|
||||
if (result.isFiltered()) {
|
||||
String matchedVideoId = findVideoId(protobufBufferArray);
|
||||
|
@ -23,6 +23,9 @@ public class Requester {
|
||||
public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException {
|
||||
String url = apiUrl + route.getCompiledRoute();
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
// Request data is in the URL parameters and no body is sent.
|
||||
// The calling code must set a length if using a request body.
|
||||
connection.setFixedLengthStreamingMode(0);
|
||||
connection.setRequestMethod(route.getMethod().name());
|
||||
String agentString = System.getProperty("http.agent")
|
||||
+ "; ReVanced/" + Utils.getAppVersionName()
|
||||
|
@ -10,6 +10,9 @@ import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.OvalShape;
|
||||
import android.graphics.drawable.shapes.RectShape;
|
||||
import android.icu.text.CompactDecimalFormat;
|
||||
import android.icu.text.DecimalFormat;
|
||||
import android.icu.text.DecimalFormatSymbols;
|
||||
import android.icu.text.NumberFormat;
|
||||
import android.os.Build;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
@ -25,17 +28,11 @@ import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
import app.revanced.integrations.shared.Utils;
|
||||
@ -223,32 +220,29 @@ public class ReturnYouTubeDislike {
|
||||
|
||||
// Note: Some locales use right to left layout (Arabic, Hebrew, etc).
|
||||
// If making changes to this code, change device settings to a RTL language and verify layout is correct.
|
||||
String oldLikesString = oldSpannable.toString();
|
||||
CharSequence oldLikes = oldSpannable;
|
||||
|
||||
// YouTube creators can hide the like count on a video,
|
||||
// and the like count appears as a device language specific string that says 'Like'.
|
||||
// Check if the string contains any numbers.
|
||||
if (!stringContainsNumber(oldLikesString)) {
|
||||
// Likes are hidden.
|
||||
// RYD does not provide usable data for these types of videos,
|
||||
// and the API returns bogus data (zero likes and zero dislikes)
|
||||
// discussion about this: https://github.com/Anarios/return-youtube-dislike/discussions/530
|
||||
if (!Utils.containsNumber(oldLikes)) {
|
||||
// Likes are hidden by video creator
|
||||
//
|
||||
// RYD does not directly provide like data, but can use an estimated likes
|
||||
// using the same scale factor RYD applied to the raw dislikes.
|
||||
//
|
||||
// example video: https://www.youtube.com/watch?v=UnrU5vxCHxw
|
||||
// RYD data: https://returnyoutubedislikeapi.com/votes?videoId=UnrU5vxCHxw
|
||||
//
|
||||
// Change the "Likes" string to show that likes and dislikes are hidden.
|
||||
String hiddenMessageString = str("revanced_ryd_video_likes_hidden_by_video_owner");
|
||||
return newSpanUsingStylingOfAnotherSpan(oldSpannable, hiddenMessageString);
|
||||
Logger.printDebug(() -> "Using estimated likes");
|
||||
oldLikes = formatDislikeCount(voteData.getLikeCount());
|
||||
}
|
||||
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get();
|
||||
|
||||
if (!compactLayout) {
|
||||
String leftSeparatorString = Utils.isRightToLeftTextLayout()
|
||||
? "\u200F" // u200F = right to left character
|
||||
: "\u200E"; // u200E = left to right character
|
||||
String leftSeparatorString = getTextDirectionString();
|
||||
final Spannable leftSeparatorSpan;
|
||||
if (isRollingNumber) {
|
||||
leftSeparatorSpan = new SpannableString(leftSeparatorString);
|
||||
@ -267,7 +261,7 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
|
||||
// likes
|
||||
builder.append(newSpanUsingStylingOfAnotherSpan(oldSpannable, oldLikesString));
|
||||
builder.append(newSpanUsingStylingOfAnotherSpan(oldSpannable, oldLikes));
|
||||
|
||||
// middle separator
|
||||
String middleSeparatorString = compactLayout
|
||||
@ -292,6 +286,12 @@ public class ReturnYouTubeDislike {
|
||||
return new SpannableString(builder);
|
||||
}
|
||||
|
||||
private static @NonNull String getTextDirectionString() {
|
||||
return Utils.isRightToLeftTextLayout()
|
||||
? "\u200F" // u200F = right to left character
|
||||
: "\u200E"; // u200E = left to right character
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the text is likely for a previously created likes/dislikes segmented span.
|
||||
*/
|
||||
@ -299,20 +299,6 @@ public class ReturnYouTubeDislike {
|
||||
return text.indexOf(MIDDLE_SEPARATOR_CHARACTER) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correctly handles any unicode numbers (such as Arabic numbers).
|
||||
*
|
||||
* @return if the string contains at least 1 number.
|
||||
*/
|
||||
private static boolean stringContainsNumber(@NonNull String text) {
|
||||
for (int index = 0, length = text.length(); index < length; index++) {
|
||||
if (Character.isDigit(text.codePointAt(index))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean spansHaveEqualTextAndColor(@NonNull Spanned one, @NonNull Spanned two) {
|
||||
// Cannot use equals on the span, because many of the inner styling spans do not implement equals.
|
||||
// Instead, compare the underlying text and the text color to handle when dark mode is changed.
|
||||
@ -334,6 +320,10 @@ public class ReturnYouTubeDislike {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static SpannableString newSpannableWithLikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) {
|
||||
return newSpanUsingStylingOfAnotherSpan(sourceStyling, formatDislikeCount(voteData.getLikeCount()));
|
||||
}
|
||||
|
||||
private static SpannableString newSpannableWithDislikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) {
|
||||
return newSpanUsingStylingOfAnotherSpan(sourceStyling,
|
||||
Settings.RYD_DISLIKE_PERCENTAGE.get()
|
||||
@ -342,11 +332,16 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
|
||||
private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull CharSequence newSpanText) {
|
||||
if (sourceStyle == newSpanText && sourceStyle instanceof SpannableString) {
|
||||
return (SpannableString) sourceStyle; // Nothing to do.
|
||||
}
|
||||
|
||||
SpannableString destination = new SpannableString(newSpanText);
|
||||
Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class);
|
||||
for (Object span : spans) {
|
||||
destination.setSpan(span, 0, destination.length(), sourceStyle.getSpanFlags(span));
|
||||
}
|
||||
|
||||
return destination;
|
||||
}
|
||||
|
||||
@ -354,13 +349,18 @@ public class ReturnYouTubeDislike {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||
if (dislikeCountFormatter == null) {
|
||||
// Note: Java number formatters will use the locale specific number characters.
|
||||
// such as Arabic which formats "1.234" into "۱,۲۳٤"
|
||||
// But YouTube disregards locale specific number characters
|
||||
// and instead shows english number characters everywhere.
|
||||
Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().locale;
|
||||
Logger.printDebug(() -> "Locale: " + locale);
|
||||
dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
|
||||
|
||||
// YouTube disregards locale specific number characters
|
||||
// and instead shows english number characters everywhere.
|
||||
// To use the same behavior, override the digit characters to use English
|
||||
// so languages such as Arabic will show "1.234" instead of the native "۱,۲۳٤"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
|
||||
symbols.setDigitStrings(DecimalFormatSymbols.getInstance(Locale.ENGLISH).getDigitStrings());
|
||||
dislikeCountFormatter.setDecimalFormatSymbols(symbols);
|
||||
}
|
||||
}
|
||||
return dislikeCountFormatter.format(dislikeCount);
|
||||
}
|
||||
@ -371,19 +371,31 @@ public class ReturnYouTubeDislike {
|
||||
}
|
||||
|
||||
private static String formatDislikePercentage(float dislikePercentage) {
|
||||
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||
if (dislikePercentageFormatter == null) {
|
||||
Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().locale;
|
||||
Logger.printDebug(() -> "Locale: " + locale);
|
||||
dislikePercentageFormatter = NumberFormat.getPercentInstance(locale);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||
if (dislikePercentageFormatter == null) {
|
||||
Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().locale;
|
||||
dislikePercentageFormatter = NumberFormat.getPercentInstance(locale);
|
||||
|
||||
// Want to set the digit strings, and the simplest way is to cast to the implementation NumberFormat returns.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
&& dislikePercentageFormatter instanceof DecimalFormat) {
|
||||
DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale);
|
||||
symbols.setDigitStrings(DecimalFormatSymbols.getInstance(Locale.ENGLISH).getDigitStrings());
|
||||
((DecimalFormat) dislikePercentageFormatter).setDecimalFormatSymbols(symbols);
|
||||
}
|
||||
}
|
||||
if (dislikePercentage >= 0.01) { // at least 1%
|
||||
dislikePercentageFormatter.setMaximumFractionDigits(0); // show only whole percentage points
|
||||
} else {
|
||||
dislikePercentageFormatter.setMaximumFractionDigits(1); // show up to 1 digit precision
|
||||
}
|
||||
return dislikePercentageFormatter.format(dislikePercentage);
|
||||
}
|
||||
if (dislikePercentage >= 0.01) { // at least 1%
|
||||
dislikePercentageFormatter.setMaximumFractionDigits(0); // show only whole percentage points
|
||||
} else {
|
||||
dislikePercentageFormatter.setMaximumFractionDigits(1); // show up to 1 digit precision
|
||||
}
|
||||
return dislikePercentageFormatter.format(dislikePercentage);
|
||||
}
|
||||
|
||||
// Will never be reached, as the oldest supported YouTube app requires Android N or greater.
|
||||
return String.valueOf((int) (dislikePercentage * 100));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -484,7 +496,17 @@ public class ReturnYouTubeDislike {
|
||||
public synchronized Spanned getDislikesSpanForRegularVideo(@NonNull Spanned original,
|
||||
boolean isSegmentedButton,
|
||||
boolean isRollingNumber) {
|
||||
return waitForFetchAndUpdateReplacementSpan(original, isSegmentedButton, isRollingNumber,false);
|
||||
return waitForFetchAndUpdateReplacementSpan(original, isSegmentedButton,
|
||||
isRollingNumber, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a Shorts like Spannable is created.
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized Spanned getLikeSpanForShort(@NonNull Spanned original) {
|
||||
return waitForFetchAndUpdateReplacementSpan(original, false,
|
||||
false, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -492,14 +514,16 @@ public class ReturnYouTubeDislike {
|
||||
*/
|
||||
@NonNull
|
||||
public synchronized Spanned getDislikeSpanForShort(@NonNull Spanned original) {
|
||||
return waitForFetchAndUpdateReplacementSpan(original, false, false, true);
|
||||
return waitForFetchAndUpdateReplacementSpan(original, false,
|
||||
false, true, false);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned original,
|
||||
boolean isSegmentedButton,
|
||||
boolean isRollingNumber,
|
||||
boolean spanIsForShort) {
|
||||
boolean spanIsForShort,
|
||||
boolean spanIsForLikes) {
|
||||
try {
|
||||
RYDVoteData votingData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH);
|
||||
if (votingData == null) {
|
||||
@ -526,24 +550,17 @@ public class ReturnYouTubeDislike {
|
||||
return original;
|
||||
}
|
||||
|
||||
if (originalDislikeSpan != null && replacementLikeDislikeSpan != null) {
|
||||
if (spansHaveEqualTextAndColor(original, replacementLikeDislikeSpan)) {
|
||||
Logger.printDebug(() -> "Ignoring previously created dislikes span of data: " + videoId);
|
||||
return original;
|
||||
}
|
||||
if (spansHaveEqualTextAndColor(original, originalDislikeSpan)) {
|
||||
Logger.printDebug(() -> "Replacing span with previously created dislike span of data: " + videoId);
|
||||
return replacementLikeDislikeSpan;
|
||||
}
|
||||
if (spanIsForLikes) {
|
||||
// Scrolling Shorts does not cause the Spans to be reloaded,
|
||||
// so there is no need to cache the likes for this situations.
|
||||
Logger.printDebug(() -> "Creating likes span for: " + votingData.videoId);
|
||||
return newSpannableWithLikes(original, votingData);
|
||||
}
|
||||
if (isSegmentedButton && isPreviouslyCreatedSegmentedSpan(original.toString())) {
|
||||
// need to recreate using original, as original has prior outdated dislike values
|
||||
if (originalDislikeSpan == null) {
|
||||
// Should never happen.
|
||||
Logger.printDebug(() -> "Cannot add dislikes - original span is null. videoId: " + videoId);
|
||||
return original;
|
||||
}
|
||||
original = originalDislikeSpan;
|
||||
|
||||
if (originalDislikeSpan != null && replacementLikeDislikeSpan != null
|
||||
&& spansHaveEqualTextAndColor(original, originalDislikeSpan)) {
|
||||
Logger.printDebug(() -> "Replacing span with previously created dislike span of data: " + videoId);
|
||||
return replacementLikeDislikeSpan;
|
||||
}
|
||||
|
||||
// No replacement span exist, create it now.
|
||||
@ -558,9 +575,10 @@ public class ReturnYouTubeDislike {
|
||||
|
||||
return replacementLikeDislikeSpan;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.printException(() -> "waitForFetchAndUpdateReplacementSpan failure", e); // should never happen
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "waitForFetchAndUpdateReplacementSpan failure", ex);
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
|
@ -3,10 +3,13 @@ package app.revanced.integrations.youtube.returnyoutubedislike.requests;
|
||||
import static app.revanced.integrations.youtube.returnyoutubedislike.ReturnYouTubeDislike.Vote;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import app.revanced.integrations.shared.Logger;
|
||||
|
||||
/**
|
||||
* ReturnYouTubeDislike API estimated like/dislike/view counts.
|
||||
*
|
||||
@ -23,38 +26,65 @@ public final class RYDVoteData {
|
||||
public final long viewCount;
|
||||
|
||||
private final long fetchedLikeCount;
|
||||
private volatile long likeCount; // read/write from different threads
|
||||
private volatile long likeCount; // Read/write from different threads.
|
||||
/**
|
||||
* Like count can be hidden by video creator, but RYD still tracks the number
|
||||
* of like/dislikes it received thru it's browser extension and and API.
|
||||
* The raw like/dislikes can be used to calculate a percentage.
|
||||
*
|
||||
* Raw values can be null, especially for older videos with little to no views.
|
||||
*/
|
||||
@Nullable
|
||||
private final Long fetchedRawLikeCount;
|
||||
private volatile float likePercentage;
|
||||
|
||||
private final long fetchedDislikeCount;
|
||||
private volatile long dislikeCount; // read/write from different threads
|
||||
private volatile long dislikeCount; // Read/write from different threads.
|
||||
@Nullable
|
||||
private final Long fetchedRawDislikeCount;
|
||||
private volatile float dislikePercentage;
|
||||
|
||||
@Nullable
|
||||
private static Long getLongIfExist(JSONObject json, String key) throws JSONException {
|
||||
return json.isNull(key)
|
||||
? null
|
||||
: json.getLong(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws JSONException if JSON parse error occurs, or if the values make no sense (ie: negative values)
|
||||
*/
|
||||
public RYDVoteData(@NonNull JSONObject json) throws JSONException {
|
||||
videoId = json.getString("id");
|
||||
viewCount = json.getLong("viewCount");
|
||||
|
||||
fetchedLikeCount = json.getLong("likes");
|
||||
fetchedRawLikeCount = getLongIfExist(json, "rawLikes");
|
||||
|
||||
fetchedDislikeCount = json.getLong("dislikes");
|
||||
fetchedRawDislikeCount = getLongIfExist(json, "rawDislikes");
|
||||
|
||||
if (viewCount < 0 || fetchedLikeCount < 0 || fetchedDislikeCount < 0) {
|
||||
throw new JSONException("Unexpected JSON values: " + json);
|
||||
}
|
||||
likeCount = fetchedLikeCount;
|
||||
dislikeCount = fetchedDislikeCount;
|
||||
updatePercentages();
|
||||
|
||||
updateUsingVote(Vote.LIKE_REMOVE); // Calculate percentages.
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimated like count
|
||||
* Public like count of the video, as reported by YT when RYD last updated it's data.
|
||||
*
|
||||
* If the likes were hidden by the video creator, then this returns an
|
||||
* estimated likes using the same extrapolation as the dislikes.
|
||||
*/
|
||||
public long getLikeCount() {
|
||||
return likeCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimated dislike count
|
||||
* Estimated total dislike count, extrapolated from the public like count using RYD data.
|
||||
*/
|
||||
public long getDislikeCount() {
|
||||
return dislikeCount;
|
||||
@ -79,28 +109,56 @@ public final class RYDVoteData {
|
||||
}
|
||||
|
||||
public void updateUsingVote(Vote vote) {
|
||||
final int likesToAdd, dislikesToAdd;
|
||||
|
||||
switch (vote) {
|
||||
case LIKE:
|
||||
likeCount = fetchedLikeCount + 1;
|
||||
dislikeCount = fetchedDislikeCount;
|
||||
likesToAdd = 1;
|
||||
dislikesToAdd = 0;
|
||||
break;
|
||||
case DISLIKE:
|
||||
likeCount = fetchedLikeCount;
|
||||
dislikeCount = fetchedDislikeCount + 1;
|
||||
likesToAdd = 0;
|
||||
dislikesToAdd = 1;
|
||||
break;
|
||||
case LIKE_REMOVE:
|
||||
likeCount = fetchedLikeCount;
|
||||
dislikeCount = fetchedDislikeCount;
|
||||
likesToAdd = 0;
|
||||
dislikesToAdd = 0;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
updatePercentages();
|
||||
}
|
||||
|
||||
private void updatePercentages() {
|
||||
likePercentage = (likeCount == 0 ? 0 : (float) likeCount / (likeCount + dislikeCount));
|
||||
dislikePercentage = (dislikeCount == 0 ? 0 : (float) dislikeCount / (likeCount + dislikeCount));
|
||||
// If a video has no public likes but RYD has raw like data,
|
||||
// then use the raw data instead.
|
||||
final boolean videoHasNoPublicLikes = fetchedLikeCount == 0;
|
||||
final boolean hasRawData = fetchedRawLikeCount != null && fetchedRawDislikeCount != null;
|
||||
|
||||
if (videoHasNoPublicLikes && hasRawData && fetchedRawDislikeCount > 0) {
|
||||
// YT creator has hidden the likes count, and this is an older video that
|
||||
// RYD does not provide estimated like counts.
|
||||
//
|
||||
// But we can calculate the public likes the same way RYD does for newer videos with hidden likes,
|
||||
// by using the same raw to estimated scale factor applied to dislikes.
|
||||
// This calculation exactly matches the public likes RYD provides for newer hidden videos.
|
||||
final float estimatedRawScaleFactor = (float) fetchedDislikeCount / fetchedRawDislikeCount;
|
||||
likeCount = (long) (estimatedRawScaleFactor * fetchedRawLikeCount) + likesToAdd;
|
||||
Logger.printDebug(() -> "Using locally calculated estimated likes since RYD did not return an estimate");
|
||||
} else {
|
||||
likeCount = fetchedLikeCount + likesToAdd;
|
||||
}
|
||||
// RYD now always returns an estimated dislike count, even if the likes are hidden.
|
||||
dislikeCount = fetchedDislikeCount + dislikesToAdd;
|
||||
|
||||
// Update percentages.
|
||||
|
||||
final float totalCount = likeCount + dislikeCount;
|
||||
if (totalCount == 0) {
|
||||
likePercentage = 0;
|
||||
dislikePercentage = 0;
|
||||
} else {
|
||||
likePercentage = likeCount / totalCount;
|
||||
dislikePercentage = dislikeCount / totalCount;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
@ -197,7 +197,7 @@ public class ReturnYouTubeDislikeApi {
|
||||
return httpResponseCode == HTTP_STATUS_CODE_RATE_LIMIT;
|
||||
}
|
||||
|
||||
@SuppressWarnings("NonAtomicOperationOnVolatileField") // Don't care, fields are estimates.
|
||||
@SuppressWarnings("NonAtomicOperationOnVolatileField") // Don't care, fields are only estimates.
|
||||
private static void updateRateLimitAndStats(long timeNetworkCallStarted, boolean connectionError, boolean rateLimitHit) {
|
||||
if (connectionError && rateLimitHit) {
|
||||
throw new IllegalArgumentException();
|
||||
@ -368,10 +368,12 @@ public class ReturnYouTubeDislikeApi {
|
||||
applyCommonPostRequestSettings(connection);
|
||||
|
||||
String jsonInputString = "{\"solution\": \"" + solution + "\"}";
|
||||
byte[] body = jsonInputString.getBytes(StandardCharsets.UTF_8);
|
||||
connection.setFixedLengthStreamingMode(body.length);
|
||||
try (OutputStream os = connection.getOutputStream()) {
|
||||
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
|
||||
os.write(input, 0, input.length);
|
||||
os.write(body);
|
||||
}
|
||||
|
||||
final int responseCode = connection.getResponseCode();
|
||||
if (checkIfRateLimitWasHit(responseCode)) {
|
||||
connection.disconnect(); // disconnect, as no more connections will be made for a little while
|
||||
@ -440,9 +442,10 @@ public class ReturnYouTubeDislikeApi {
|
||||
applyCommonPostRequestSettings(connection);
|
||||
|
||||
String voteJsonString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"value\": \"" + vote.value + "\"}";
|
||||
byte[] body = voteJsonString.getBytes(StandardCharsets.UTF_8);
|
||||
connection.setFixedLengthStreamingMode(body.length);
|
||||
try (OutputStream os = connection.getOutputStream()) {
|
||||
byte[] input = voteJsonString.getBytes(StandardCharsets.UTF_8);
|
||||
os.write(input, 0, input.length);
|
||||
os.write(body);
|
||||
}
|
||||
|
||||
final int responseCode = connection.getResponseCode();
|
||||
@ -490,10 +493,12 @@ public class ReturnYouTubeDislikeApi {
|
||||
applyCommonPostRequestSettings(connection);
|
||||
|
||||
String jsonInputString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"solution\": \"" + solution + "\"}";
|
||||
byte[] body = jsonInputString.getBytes(StandardCharsets.UTF_8);
|
||||
connection.setFixedLengthStreamingMode(body.length);
|
||||
try (OutputStream os = connection.getOutputStream()) {
|
||||
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
|
||||
os.write(input, 0, input.length);
|
||||
os.write(body);
|
||||
}
|
||||
|
||||
final int responseCode = connection.getResponseCode();
|
||||
if (checkIfRateLimitWasHit(responseCode)) {
|
||||
connection.disconnect(); // disconnect, as no more connections will be made for a little while
|
||||
|
Loading…
Reference in New Issue
Block a user