Skip to content

Commit

Permalink
feat(youtube/return-youtube-dislike): show dislike as a percentage (#234
Browse files Browse the repository at this point in the history
)

Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
  • Loading branch information
LisoUseInAIKyrios and oSumAtrIX committed Dec 3, 2022
1 parent 718c5a7 commit 7840bc4
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 77 deletions.
Expand Up @@ -2,6 +2,8 @@

import android.content.Context;
import android.icu.text.CompactDecimalFormat;
import android.icu.text.DecimalFormat;
import android.icu.text.DecimalFormatSymbols;
import android.os.Build;
import android.text.SpannableString;

Expand All @@ -17,6 +19,7 @@
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;

import app.revanced.integrations.returnyoutubedislike.requests.RYDVoteData;
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.integrations.settings.SettingsEnum;
import app.revanced.integrations.utils.LogHelper;
Expand All @@ -41,7 +44,7 @@ public class ReturnYouTubeDislike {
private static volatile boolean isEnabled = SettingsEnum.RYD_ENABLED.getBoolean();

/**
* Used to guard {@link #currentVideoId} and {@link #dislikeFetchFuture},
* Used to guard {@link #currentVideoId} and {@link #voteFetchFuture},
* as multiple threads access this class.
*/
private static final Object videoIdLockObject = new Object();
Expand All @@ -50,10 +53,10 @@ public class ReturnYouTubeDislike {
private static String currentVideoId;

/**
* Stores the results of the dislike fetch, and used as a barrier to wait until fetch completes
* Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes
*/
@GuardedBy("videoIdLockObject")
private static Future<Integer> dislikeFetchFuture;
private static Future<RYDVoteData> voteFetchFuture;

public enum Vote {
LIKE(1),
Expand All @@ -73,8 +76,14 @@ private ReturnYouTubeDislike() {
/**
* Used to format like/dislike count.
*/
@GuardedBy("ReturnYouTubeDislike.class") // number formatter is not thread safe
private static CompactDecimalFormat compactNumberFormatter;
@GuardedBy("ReturnYouTubeDislike.class") // not thread safe
private static CompactDecimalFormat dislikeCountFormatter;

/**
* Used to format like/dislike count.
*/
@GuardedBy("ReturnYouTubeDislike.class") // not thread safe
private static DecimalFormat dislikePercentageFormatter;

public static void onEnabledChange(boolean enabled) {
isEnabled = enabled;
Expand All @@ -86,9 +95,9 @@ private static String getCurrentVideoId() {
}
}

private static Future<Integer> getDislikeFetchFuture() {
private static Future<RYDVoteData> getVoteFetchFuture() {
synchronized (videoIdLockObject) {
return dislikeFetchFuture;
return voteFetchFuture;
}
}

Expand All @@ -104,7 +113,7 @@ public static void newVideoLoaded(String videoId) {
currentVideoId = videoId;
// no need to wrap the fetchDislike call in a try/catch,
// as any exceptions are propagated out in the later Future#Get call
dislikeFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchDislikes(videoId));
voteFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId));
}
} catch (Exception ex) {
LogHelper.printException(() -> "Failed to load new video: " + videoId, ex);
Expand All @@ -128,19 +137,19 @@ public static void onComponentCreated(Object conversionContext, AtomicReference<

// Have to block the current thread until fetching is done
// There's no known way to edit the text after creation yet
Integer dislikeCount;
RYDVoteData votingData;
try {
dislikeCount = getDislikeFetchFuture().get(MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE, TimeUnit.MILLISECONDS);
votingData = getVoteFetchFuture().get(MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
LogHelper.printDebug(() -> "UI timed out waiting for dislike fetch to complete");
return;
}
if (dislikeCount == null) {
LogHelper.printDebug(() -> "Cannot add dislike count to UI (dislike count not available)");
if (votingData == null) {
LogHelper.printDebug(() -> "Cannot add dislike count to UI (RYD data not available)");
return;
}

updateDislike(textRef, isSegmentedButton, dislikeCount);
updateDislike(textRef, isSegmentedButton, votingData);
LogHelper.printDebug(() -> "Updated text on component: " + conversionContextString);
} catch (Exception ex) {
LogHelper.printException(() -> "Error while trying to update dislikes text", ex);
Expand Down Expand Up @@ -197,9 +206,11 @@ private static String getUserId() {
return userId;
}

private static void updateDislike(AtomicReference<Object> textRef, boolean isSegmentedButton, int dislikeCount) {
private static void updateDislike(AtomicReference<Object> textRef, boolean isSegmentedButton, RYDVoteData voteData) {
SpannableString oldSpannableString = (SpannableString) textRef.get();
String newDislikeString = formatDislikeCount(dislikeCount);
String newDislikeString = SettingsEnum.RYD_SHOW_DISLIKE_PERCENTAGE.getBoolean()
? formatDislikePercentage(voteData.dislikePercentage)
: formatDislikeCount(voteData.dislikeCount);

if (isSegmentedButton) { // both likes and dislikes are on a custom segmented button
// parse out the like count as a string
Expand Down Expand Up @@ -239,16 +250,16 @@ private static void updateDislike(AtomicReference<Object> textRef, boolean isSeg
textRef.set(newSpannableString);
}

private static String formatDislikeCount(int dislikeCount) {
private static String formatDislikeCount(long dislikeCount) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String formatted;
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
if (compactNumberFormatter == null) {
if (dislikeCountFormatter == null) {
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
LogHelper.printDebug(() -> "Locale: " + locale);
compactNumberFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
}
formatted = compactNumberFormatter.format(dislikeCount);
formatted = dislikeCountFormatter.format(dislikeCount);
}
LogHelper.printDebug(() -> "Dislike count: " + dislikeCount + " formatted as: " + formatted);
return formatted;
Expand All @@ -257,4 +268,29 @@ private static String formatDislikeCount(int dislikeCount) {
// never will be reached, as the oldest supported YouTube app requires Android N or greater
return String.valueOf(dislikeCount);
}

private static String formatDislikePercentage(float dislikePercentage) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
String formatted;
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
if (dislikePercentageFormatter == null) {
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
LogHelper.printDebug(() -> "Locale: " + locale);
dislikePercentageFormatter = new DecimalFormat("", new DecimalFormatSymbols(locale));
}
if (dislikePercentage == 0 || dislikePercentage >= 0.01) { // zero, or at least 1%
dislikePercentageFormatter.applyLocalizedPattern("0"); // show only whole percentage points
} else { // between (0, 1)%
dislikePercentageFormatter.applyLocalizedPattern("0.#"); // show 1 digit precision
}
final char percentChar = dislikePercentageFormatter.getDecimalFormatSymbols().getPercent();
formatted = dislikePercentageFormatter.format(100 * dislikePercentage) + percentChar;
}
LogHelper.printDebug(() -> "Dislike percentage: " + dislikePercentage + " formatted as: " + formatted);
return formatted;
}

// never will be reached, as the oldest supported YouTube app requires Android N or greater
return (int) (100 * dislikePercentage) + "%";
}
}
@@ -0,0 +1,76 @@
package app.revanced.integrations.returnyoutubedislike.requests;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.Objects;

/**
* ReturnYouTubeDislike API estimated like/dislike/view counts.
*
* ReturnYouTubeDislike does not guarantee when the counts are updated.
* So these values may lag behind what YouTube shows.
*/
public final class RYDVoteData {

public final String videoId;

/**
* Estimated number of views
*/
public final long viewCount;

/**
* Estimated like count
*/
public final long likeCount;

/**
* Estimated dislike count
*/
public final long dislikeCount;

/**
* Estimated percentage of likes for all votes. Value has range of [0, 1]
*
* A video with 400 positive votes, and 100 negative votes, has a likePercentage of 0.8
*/
public final float likePercentage;

/**
* Estimated percentage of dislikes for all votes. Value has range of [0, 1]
*
* A video with 400 positive votes, and 100 negative votes, has a dislikePercentage of 0.2
*/
public final float dislikePercentage;

/**
* @throws JSONException if JSON parse error occurs, or if the values make no sense (ie: negative values)
*/
public RYDVoteData(JSONObject json) throws JSONException {
Objects.requireNonNull(json);
videoId = json.getString("id");
viewCount = json.getLong("viewCount");
likeCount = json.getLong("likes");
dislikeCount = json.getLong("dislikes");
if (likeCount < 0 || dislikeCount < 0 || viewCount < 0) {
throw new JSONException("Unexpected JSON values: " + json);
}
likePercentage = (likeCount == 0 ? 0 : (float)likeCount / (likeCount + dislikeCount));
dislikePercentage = (dislikeCount == 0 ? 0 : (float)dislikeCount / (likeCount + dislikeCount));
}

@Override
public String toString() {
return "RYDVoteData{"
+ "videoId=" + videoId
+ ", viewCount=" + viewCount
+ ", likeCount=" + likeCount
+ ", dislikeCount=" + dislikeCount
+ ", likePercentage=" + likePercentage
+ ", dislikePercentage=" + dislikePercentage
+ '}';
}

// equals and hashcode is not implemented (currently not needed)
}
Expand Up @@ -4,6 +4,7 @@

import androidx.annotation.Nullable;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
Expand All @@ -26,7 +27,7 @@ public class ReturnYouTubeDislikeApi {
private static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/";

/**
* Default connection and response timeout for {@link #fetchDislikes(String)}
* Default connection and response timeout for {@link #fetchVotes(String)}
*/
private static final int API_GET_DISLIKE_DEFAULT_TIMEOUT_MILLISECONDS = 5000;

Expand Down Expand Up @@ -128,11 +129,10 @@ private static boolean checkIfRateLimitWasHit(int httpResponseCode) {
}

/**
* @return The number of dislikes.
* Returns NULL if fetch failed, or a rate limit is in effect.
* @return NULL if fetch failed, or if a rate limit is in effect.
*/
@Nullable
public static Integer fetchDislikes(String videoId) {
public static RYDVoteData fetchVotes(String videoId) {
ReVancedUtils.verifyOffMainThread();
Objects.requireNonNull(videoId);
try {
Expand Down Expand Up @@ -161,10 +161,14 @@ public static Integer fetchDislikes(String videoId) {
}
if (responseCode == SUCCESS_HTTP_STATUS_CODE) {
JSONObject json = Requester.getJSONObject(connection); // also disconnects
Integer fetchedDislikeCount = json.getInt("dislikes");
LogHelper.printDebug(() -> "Fetched video: " + videoId
+ " dislikes: " + fetchedDislikeCount);
return fetchedDislikeCount;
try {
RYDVoteData votingData = new RYDVoteData(json);
LogHelper.printDebug(() -> "Voting data fetched: " + votingData);
return votingData;
} catch (JSONException ex) {
LogHelper.printException(() -> "Failed to parse video: " + videoId + " json: " + json, ex);
return null;
}
}
LogHelper.printDebug(() -> "Failed to fetch dislikes for video: " + videoId
+ " response code was: " + responseCode);
Expand Down
Expand Up @@ -115,6 +115,7 @@ public enum SettingsEnum {
// RYD settings
RYD_USER_ID("ryd_userId", null, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.STRING),
RYD_ENABLED("ryd_enabled", true, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.BOOLEAN),
RYD_SHOW_DISLIKE_PERCENTAGE("ryd_show_dislike_percentage", false, SharedPrefHelper.SharedPrefNames.RYD, ReturnType.BOOLEAN),

// SponsorBlock settings
SB_ENABLED("sb-enabled", true, SharedPrefHelper.SharedPrefNames.SPONSOR_BLOCK, ReturnType.BOOLEAN),
Expand Down

0 comments on commit 7840bc4

Please sign in to comment.