fix(YouTube - ReturnYouTubeDislike): Show estimated like count for videos with hidden likes (#684)

This commit is contained in:
LisoUseInAIKyrios 2024-09-01 17:49:15 -04:00 committed by GitHub
parent 55c278dc08
commit 27d2b60444
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 259 additions and 134 deletions

View File

@ -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
*/

View File

@ -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);

View File

@ -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);

View File

@ -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()

View File

@ -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;
}

View File

@ -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

View File

@ -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