mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-01-20 08:47:33 +01:00
fix(youtube/return-youtube-dislike): feedback when dislikes hidden (#224)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
parent
9dc9ce364c
commit
37869dc5b8
@ -1,6 +1,7 @@
|
|||||||
package app.revanced.integrations.requests;
|
package app.revanced.integrations.requests;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
@ -24,32 +25,58 @@ public class Requester {
|
|||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse, and then disconnect the {@link HttpURLConnection}
|
||||||
|
*
|
||||||
|
* TODO: rename this to #parseJsonAndDisconnect
|
||||||
|
*/
|
||||||
public static String parseJson(HttpURLConnection connection) throws IOException {
|
public static String parseJson(HttpURLConnection connection) throws IOException {
|
||||||
return parseJson(connection.getInputStream(), false);
|
String result = parseJson(connection.getInputStream(), false);
|
||||||
|
connection.disconnect();
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse, and then close the {@link InputStream}
|
||||||
|
*
|
||||||
|
* TODO: rename this to #parseJsonAndCloseStream
|
||||||
|
*/
|
||||||
public static String parseJson(InputStream inputStream, boolean isError) throws IOException {
|
public static String parseJson(InputStream inputStream, boolean isError) throws IOException {
|
||||||
StringBuilder jsonBuilder = new StringBuilder();
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
|
StringBuilder jsonBuilder = new StringBuilder();
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
jsonBuilder.append(line);
|
jsonBuilder.append(line);
|
||||||
if (isError)
|
if (isError)
|
||||||
jsonBuilder.append("\n");
|
jsonBuilder.append("\n");
|
||||||
|
}
|
||||||
|
return jsonBuilder.toString();
|
||||||
}
|
}
|
||||||
inputStream.close();
|
|
||||||
return jsonBuilder.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse, and then do NOT disconnect the {@link HttpURLConnection}
|
||||||
|
*/
|
||||||
public static String parseErrorJson(HttpURLConnection connection) throws IOException {
|
public static String parseErrorJson(HttpURLConnection connection) throws IOException {
|
||||||
|
// TODO: make this also disconnect, and rename method to #parseErrorJsonAndDisconnect
|
||||||
return parseJson(connection.getErrorStream(), true);
|
return parseJson(connection.getErrorStream(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static JSONObject getJSONObject(HttpURLConnection connection) throws Exception {
|
/**
|
||||||
|
* Parse, and then disconnect the {@link HttpURLConnection}
|
||||||
|
*
|
||||||
|
* TODO: rename this to #getJSONObjectAndDisconnect
|
||||||
|
*/
|
||||||
|
public static JSONObject getJSONObject(HttpURLConnection connection) throws JSONException, IOException {
|
||||||
return new JSONObject(parseJsonAndDisconnect(connection));
|
return new JSONObject(parseJsonAndDisconnect(connection));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static JSONArray getJSONArray(HttpURLConnection connection) throws Exception {
|
/**
|
||||||
|
* Parse, and then disconnect the {@link HttpURLConnection}
|
||||||
|
*
|
||||||
|
* TODO: rename this to #getJSONArrayAndDisconnect
|
||||||
|
*/
|
||||||
|
public static JSONArray getJSONArray(HttpURLConnection connection) throws JSONException, IOException {
|
||||||
return new JSONArray(parseJsonAndDisconnect(connection));
|
return new JSONArray(parseJsonAndDisconnect(connection));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,11 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
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.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
import app.revanced.integrations.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||||
@ -20,18 +25,35 @@ import app.revanced.integrations.utils.SharedPrefHelper;
|
|||||||
|
|
||||||
public class ReturnYouTubeDislike {
|
public class ReturnYouTubeDislike {
|
||||||
/**
|
/**
|
||||||
* maximum amount of time to block the UI from updates, while waiting for dislike network call to complete
|
* Maximum amount of time to block the UI from updates while waiting for dislike network call to complete.
|
||||||
|
*
|
||||||
|
* Must be less than 5 seconds, as per:
|
||||||
|
* https://developer.android.com/topic/performance/vitals/anr
|
||||||
*/
|
*/
|
||||||
private static final long MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE = 5000;
|
private static final long MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE = 4000;
|
||||||
|
|
||||||
// Different threads read and write these fields. access to them must be synchronized
|
/**
|
||||||
@GuardedBy("this")
|
* Used to send votes, one by one, in the same order the user created them
|
||||||
|
*/
|
||||||
|
private static final ExecutorService voteSerialExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
|
// Must be volatile, since non-main threads read this field.
|
||||||
|
private static volatile boolean isEnabled = SettingsEnum.RYD_ENABLED.getBoolean();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to guard {@link #currentVideoId} and {@link #dislikeFetchFuture},
|
||||||
|
* as multiple threads access this class.
|
||||||
|
*/
|
||||||
|
private static final Object videoIdLockObject = new Object();
|
||||||
|
|
||||||
|
@GuardedBy("videoIdLockObject")
|
||||||
private static String currentVideoId;
|
private static String currentVideoId;
|
||||||
@GuardedBy("this")
|
|
||||||
private static Integer dislikeCount;
|
|
||||||
|
|
||||||
private static boolean isEnabled;
|
/**
|
||||||
private static boolean segmentedButton;
|
* Stores the results of the dislike fetch, and used as a barrier to wait until fetch completes
|
||||||
|
*/
|
||||||
|
@GuardedBy("videoIdLockObject")
|
||||||
|
private static Future<Integer> dislikeFetchFuture;
|
||||||
|
|
||||||
public enum Vote {
|
public enum Vote {
|
||||||
LIKE(1),
|
LIKE(1),
|
||||||
@ -45,172 +67,113 @@ public class ReturnYouTubeDislike {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Thread _dislikeFetchThread = null;
|
|
||||||
private static Thread _votingThread = null;
|
|
||||||
private static CompactDecimalFormat compactNumberFormatter;
|
|
||||||
|
|
||||||
static {
|
|
||||||
isEnabled = SettingsEnum.RYD_ENABLED.getBoolean();
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
Context context = ReVancedUtils.getContext();
|
|
||||||
Locale locale = context.getResources().getConfiguration().locale;
|
|
||||||
LogHelper.printDebug(() -> "Locale: " + locale);
|
|
||||||
compactNumberFormatter = CompactDecimalFormat.getInstance(
|
|
||||||
locale,
|
|
||||||
CompactDecimalFormat.CompactStyle.SHORT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ReturnYouTubeDislike() {
|
private ReturnYouTubeDislike() {
|
||||||
} // only static methods
|
} // only static methods
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to format like/dislike count.
|
||||||
|
*/
|
||||||
|
@GuardedBy("ReturnYouTubeDislike.class") // number formatter is not thread safe
|
||||||
|
private static CompactDecimalFormat compactNumberFormatter;
|
||||||
|
|
||||||
public static void onEnabledChange(boolean enabled) {
|
public static void onEnabledChange(boolean enabled) {
|
||||||
isEnabled = enabled;
|
isEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized String getCurrentVideoId() {
|
private static String getCurrentVideoId() {
|
||||||
return currentVideoId;
|
synchronized (videoIdLockObject) {
|
||||||
}
|
return currentVideoId;
|
||||||
|
|
||||||
public static synchronized void setCurrentVideoId(String videoId) {
|
|
||||||
Objects.requireNonNull(videoId);
|
|
||||||
currentVideoId = videoId;
|
|
||||||
dislikeCount = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the dislikeCount for {@link #getCurrentVideoId()}.
|
|
||||||
* Returns NULL if the dislike is not yet loaded, or if the dislike network fetch failed.
|
|
||||||
*/
|
|
||||||
public static synchronized Integer getDislikeCount() {
|
|
||||||
return dislikeCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return true if the videoId parameter matches the current dislike request, and the set value was successful.
|
|
||||||
* If videoID parameter does not match currentVideoId, then this call does nothing
|
|
||||||
*/
|
|
||||||
private static synchronized boolean setCurrentDislikeCount(String videoId, Integer videoIdDislikeCount) {
|
|
||||||
if (!videoId.equals(currentVideoId)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
dislikeCount = videoIdDislikeCount;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void interruptDislikeFetchThreadIfRunning() {
|
|
||||||
if (_dislikeFetchThread == null) return;
|
|
||||||
try {
|
|
||||||
Thread.State dislikeFetchThreadState = _dislikeFetchThread.getState();
|
|
||||||
if (dislikeFetchThreadState != Thread.State.TERMINATED) {
|
|
||||||
LogHelper.printDebug(() -> "Interrupting the fetch dislike thread of state: " + dislikeFetchThreadState);
|
|
||||||
_dislikeFetchThread.interrupt();
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LogHelper.printException(() -> "Error in the fetch dislike thread", ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void interruptVoteThreadIfRunning() {
|
private static Future<Integer> getDislikeFetchFuture() {
|
||||||
if (_votingThread == null) return;
|
synchronized (videoIdLockObject) {
|
||||||
try {
|
return dislikeFetchFuture;
|
||||||
Thread.State voteThreadState = _votingThread.getState();
|
|
||||||
if (voteThreadState != Thread.State.TERMINATED) {
|
|
||||||
LogHelper.printDebug(() -> "Interrupting the voting thread of state: " + voteThreadState);
|
|
||||||
_votingThread.interrupt();
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LogHelper.printException(() -> "Error in the voting thread", ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// It is unclear if this method is always called on the main thread (since the YouTube app is the one making the call)
|
||||||
|
// treat this as if any thread could call this method
|
||||||
public static void newVideoLoaded(String videoId) {
|
public static void newVideoLoaded(String videoId) {
|
||||||
if (!isEnabled) return;
|
if (!isEnabled) return;
|
||||||
LogHelper.printDebug(() -> "New video loaded: " + videoId);
|
try {
|
||||||
|
Objects.requireNonNull(videoId);
|
||||||
|
LogHelper.printDebug(() -> "New video loaded: " + videoId);
|
||||||
|
|
||||||
setCurrentVideoId(videoId);
|
synchronized (videoIdLockObject) {
|
||||||
interruptDislikeFetchThreadIfRunning();
|
currentVideoId = videoId;
|
||||||
|
// no need to wrap the fetchDislike call in a try/catch,
|
||||||
// TODO use a private fixed size thread pool
|
// as any exceptions are propagated out in the later Future#Get call
|
||||||
_dislikeFetchThread = new Thread(() -> {
|
dislikeFetchFuture = ReVancedUtils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchDislikes(videoId));
|
||||||
try {
|
|
||||||
Integer fetchedDislikeCount = ReturnYouTubeDislikeApi.fetchDislikes(videoId);
|
|
||||||
if (fetchedDislikeCount == null) {
|
|
||||||
return; // fetch failed or thread was interrupted
|
|
||||||
}
|
|
||||||
if (!ReturnYouTubeDislike.setCurrentDislikeCount(videoId, fetchedDislikeCount)) {
|
|
||||||
LogHelper.printDebug(() -> "Ignoring stale dislike fetched call for video " + videoId);
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LogHelper.printException(() -> "Failed to fetch dislikes for videoId: " + videoId, ex);
|
|
||||||
}
|
}
|
||||||
});
|
} catch (Exception ex) {
|
||||||
_dislikeFetchThread.start();
|
LogHelper.printException(() -> "Failed to load new video: " + videoId, ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BEWARE! This method is sometimes called on the main thread, but it usually is called _off_ the main thread!
|
||||||
public static void onComponentCreated(Object conversionContext, AtomicReference<Object> textRef) {
|
public static void onComponentCreated(Object conversionContext, AtomicReference<Object> textRef) {
|
||||||
if (!isEnabled) return;
|
if (!isEnabled) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var conversionContextString = conversionContext.toString();
|
var conversionContextString = conversionContext.toString();
|
||||||
|
|
||||||
|
boolean isSegmentedButton = false;
|
||||||
// Check for new component
|
// Check for new component
|
||||||
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
|
if (conversionContextString.contains("|segmented_like_dislike_button.eml|")) {
|
||||||
segmentedButton = true;
|
isSegmentedButton = true;
|
||||||
} else if (!conversionContextString.contains("|dislike_button.eml|")) {
|
} else if (!conversionContextString.contains("|dislike_button.eml|")) {
|
||||||
LogHelper.printDebug(() -> "could not find a dislike button in " + conversionContextString);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Have to block the current thread until fetching is done
|
// Have to block the current thread until fetching is done
|
||||||
// There's no known way to edit the text after creation yet
|
// There's no known way to edit the text after creation yet
|
||||||
if (_dislikeFetchThread != null) {
|
Integer dislikeCount;
|
||||||
_dislikeFetchThread.join(MILLISECONDS_TO_BLOCK_UI_WHILE_WAITING_FOR_DISLIKE_FETCH_TO_COMPLETE);
|
try {
|
||||||
|
dislikeCount = getDislikeFetchFuture().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) {
|
||||||
Integer fetchedDislikeCount = getDislikeCount();
|
|
||||||
if (fetchedDislikeCount == null) {
|
|
||||||
LogHelper.printDebug(() -> "Cannot add dislike count to UI (dislike count not available)");
|
LogHelper.printDebug(() -> "Cannot add dislike count to UI (dislike count not available)");
|
||||||
|
|
||||||
// There's no point letting the request continue, as there is not another chance to use the result
|
|
||||||
interruptDislikeFetchThreadIfRunning();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDislike(textRef, dislikeCount);
|
updateDislike(textRef, isSegmentedButton, dislikeCount);
|
||||||
LogHelper.printDebug(() -> "Updated text on component" + conversionContextString);
|
LogHelper.printDebug(() -> "Updated text on component: " + conversionContextString);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(() -> "Error while trying to set dislikes text", ex);
|
LogHelper.printException(() -> "Error while trying to update dislikes text", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void sendVote(Vote vote) {
|
public static void sendVote(Vote vote) {
|
||||||
if (!isEnabled) return;
|
if (!isEnabled) return;
|
||||||
|
try {
|
||||||
Context context = Objects.requireNonNull(ReVancedUtils.getContext());
|
Objects.requireNonNull(vote);
|
||||||
if (SharedPrefHelper.getBoolean(context, SharedPrefHelper.SharedPrefNames.YOUTUBE, "user_signed_out", true))
|
Context context = Objects.requireNonNull(ReVancedUtils.getContext());
|
||||||
return;
|
if (SharedPrefHelper.getBoolean(context, SharedPrefHelper.SharedPrefNames.YOUTUBE, "user_signed_out", true)) {
|
||||||
|
return;
|
||||||
// Must make a local copy of videoId, since it may change between now and when the vote thread runs
|
|
||||||
String videoIdToVoteFor = getCurrentVideoId();
|
|
||||||
interruptVoteThreadIfRunning();
|
|
||||||
|
|
||||||
// TODO: use a private fixed sized thread pool
|
|
||||||
_votingThread = new Thread(() -> {
|
|
||||||
try {
|
|
||||||
ReturnYouTubeDislikeApi.sendVote(videoIdToVoteFor, getUserId(), vote);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LogHelper.printException(() -> "Failed to send vote", ex);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
_votingThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Must make a local copy of videoId, since it may change between now and when the vote thread runs
|
||||||
* Lock used exclusively by {@link #getUserId()}
|
String videoIdToVoteFor = getCurrentVideoId();
|
||||||
*/
|
|
||||||
private static final Object rydUserIdLock = new Object();
|
voteSerialExecutor.execute(() -> {
|
||||||
|
// must wrap in try/catch to properly log exceptions
|
||||||
|
try {
|
||||||
|
String userId = getUserId();
|
||||||
|
if (userId != null) {
|
||||||
|
ReturnYouTubeDislikeApi.sendVote(videoIdToVoteFor, userId, vote);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LogHelper.printException(() -> "Failed to send vote", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LogHelper.printException(() -> "Error while trying to send vote", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must call off main thread, as this will make a network call if user has not yet been registered
|
* Must call off main thread, as this will make a network call if user has not yet been registered
|
||||||
@ -222,48 +185,76 @@ public class ReturnYouTubeDislike {
|
|||||||
private static String getUserId() {
|
private static String getUserId() {
|
||||||
ReVancedUtils.verifyOffMainThread();
|
ReVancedUtils.verifyOffMainThread();
|
||||||
|
|
||||||
synchronized (rydUserIdLock) {
|
String userId = SettingsEnum.RYD_USER_ID.getString();
|
||||||
String userId = SettingsEnum.RYD_USER_ID.getString();
|
if (userId != null) {
|
||||||
if (userId != null) {
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
userId = ReturnYouTubeDislikeApi.registerAsNewUser(); // blocks until network call is completed
|
|
||||||
if (userId != null) {
|
|
||||||
SettingsEnum.RYD_USER_ID.saveValue(userId);
|
|
||||||
}
|
|
||||||
return userId;
|
return userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userId = ReturnYouTubeDislikeApi.registerAsNewUser(); // blocks until network call is completed
|
||||||
|
if (userId != null) {
|
||||||
|
SettingsEnum.RYD_USER_ID.saveValue(userId);
|
||||||
|
}
|
||||||
|
return userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void updateDislike(AtomicReference<Object> textRef, Integer dislikeCount) {
|
private static void updateDislike(AtomicReference<Object> textRef, boolean isSegmentedButton, int dislikeCount) {
|
||||||
SpannableString oldSpannableString = (SpannableString) textRef.get();
|
SpannableString oldSpannableString = (SpannableString) textRef.get();
|
||||||
|
String newDislikeString = formatDislikeCount(dislikeCount);
|
||||||
|
|
||||||
// Parse the buttons string.
|
if (isSegmentedButton) { // both likes and dislikes are on a custom segmented button
|
||||||
// If the button is segmented, only get the like count as a string
|
// parse out the like count as a string
|
||||||
var oldButtonString = oldSpannableString.toString();
|
String oldLikesString = oldSpannableString.toString().split(" \\| ")[0];
|
||||||
if (segmentedButton) oldButtonString = oldButtonString.split(" \\| ")[0];
|
|
||||||
|
|
||||||
var dislikeString = formatDislikes(dislikeCount);
|
// YouTube creators can hide the like count on a video,
|
||||||
SpannableString newString = new SpannableString(
|
// and the like count appears as a device language specific string that says 'Like'
|
||||||
segmentedButton ? (oldButtonString + " | " + dislikeString) : dislikeString
|
// check if the first character is not a number
|
||||||
);
|
if (!Character.isDigit(oldLikesString.charAt(0))) {
|
||||||
|
// 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)
|
||||||
|
//
|
||||||
|
// example video: https://www.youtube.com/watch?v=UnrU5vxCHxw
|
||||||
|
// RYD data: https://returnyoutubedislikeapi.com/votes?videoId=UnrU5vxCHxw
|
||||||
|
//
|
||||||
|
// discussion about this: https://github.com/Anarios/return-youtube-dislike/discussions/530
|
||||||
|
|
||||||
|
//
|
||||||
|
// Change the "Likes" string to show that likes and dislikes are hidden
|
||||||
|
//
|
||||||
|
newDislikeString = "Hidden"; // for now, this is not localized
|
||||||
|
LogHelper.printDebug(() -> "Like count is hidden by video creator. "
|
||||||
|
+ "RYD does not provide data for videos with hidden likes.");
|
||||||
|
} else {
|
||||||
|
// temporary fix for https://github.com/revanced/revanced-integrations/issues/118
|
||||||
|
newDislikeString = oldLikesString + " | " + newDislikeString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SpannableString newSpannableString = new SpannableString(newDislikeString);
|
||||||
// Copy style (foreground color, etc) to new string
|
// Copy style (foreground color, etc) to new string
|
||||||
Object[] spans = oldSpannableString.getSpans(0, oldSpannableString.length(), Object.class);
|
Object[] spans = oldSpannableString.getSpans(0, oldSpannableString.length(), Object.class);
|
||||||
for (Object span : spans)
|
for (Object span : spans) {
|
||||||
newString.setSpan(span, 0, newString.length(), oldSpannableString.getSpanFlags(span));
|
newSpannableString.setSpan(span, 0, newDislikeString.length(), oldSpannableString.getSpanFlags(span));
|
||||||
|
}
|
||||||
textRef.set(newString);
|
textRef.set(newSpannableString);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String formatDislikes(int dislikes) {
|
private static String formatDislikeCount(int dislikeCount) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && compactNumberFormatter != null) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
final String formatted = compactNumberFormatter.format(dislikes);
|
String formatted;
|
||||||
LogHelper.printDebug(() -> "Formatting dislikes - " + dislikes + " - " + formatted);
|
synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize
|
||||||
|
if (compactNumberFormatter == null) {
|
||||||
|
Locale locale = ReVancedUtils.getContext().getResources().getConfiguration().locale;
|
||||||
|
LogHelper.printDebug(() -> "Locale: " + locale);
|
||||||
|
compactNumberFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT);
|
||||||
|
}
|
||||||
|
formatted = compactNumberFormatter.format(dislikeCount);
|
||||||
|
}
|
||||||
|
LogHelper.printDebug(() -> "Dislike count: " + dislikeCount + " formatted as: " + formatted);
|
||||||
return formatted;
|
return formatted;
|
||||||
}
|
}
|
||||||
LogHelper.printDebug(() -> "Could not format dislikes, using the unformatted count - " + dislikes);
|
|
||||||
return String.valueOf(dislikes);
|
// never will be reached, as the oldest supported YouTube app requires Android N or greater
|
||||||
|
return String.valueOf(dislikeCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package app.revanced.integrations.returnyoutubedislike.requests;
|
package app.revanced.integrations.returnyoutubedislike.requests;
|
||||||
|
|
||||||
import static app.revanced.integrations.requests.Requester.parseJson;
|
|
||||||
|
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@ -14,6 +12,7 @@ import java.net.HttpURLConnection;
|
|||||||
import java.net.ProtocolException;
|
import java.net.ProtocolException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@ -26,7 +25,23 @@ import app.revanced.integrations.utils.ReVancedUtils;
|
|||||||
public class ReturnYouTubeDislikeApi {
|
public class ReturnYouTubeDislikeApi {
|
||||||
private static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/";
|
private static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/";
|
||||||
|
|
||||||
private static final int HTTP_CONNECTION_DEFAULT_TIMEOUT = 5000;
|
/**
|
||||||
|
* Default connection and response timeout for {@link #fetchDislikes(String)}
|
||||||
|
*/
|
||||||
|
private static final int API_GET_DISLIKE_DEFAULT_TIMEOUT_MILLISECONDS = 5000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default connection and response timeout for voting and registration.
|
||||||
|
*
|
||||||
|
* Voting and user registration runs in the background and has has no urgency
|
||||||
|
* so this can be a larger value.
|
||||||
|
*/
|
||||||
|
private static final int API_REGISTER_VOTE_DEFAULT_TIMEOUT_MILLISECONDS = 90000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response code of a successful API call
|
||||||
|
*/
|
||||||
|
private static final int SUCCESS_HTTP_STATUS_CODE = 200;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates a client rate limit has been reached
|
* Indicates a client rate limit has been reached
|
||||||
@ -34,7 +49,7 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
private static final int RATE_LIMIT_HTTP_STATUS_CODE = 429;
|
private static final int RATE_LIMIT_HTTP_STATUS_CODE = 429;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How long wait until API calls are resumed, if a rate limit is hit
|
* How long to wait until API calls are resumed, if a rate limit is hit.
|
||||||
* No clear guideline of how long to backoff. Using 60 seconds for now.
|
* No clear guideline of how long to backoff. Using 60 seconds for now.
|
||||||
*/
|
*/
|
||||||
private static final int RATE_LIMIT_BACKOFF_SECONDS = 60;
|
private static final int RATE_LIMIT_BACKOFF_SECONDS = 60;
|
||||||
@ -43,11 +58,36 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
* Last time a {@link #RATE_LIMIT_HTTP_STATUS_CODE} was reached.
|
* Last time a {@link #RATE_LIMIT_HTTP_STATUS_CODE} was reached.
|
||||||
* zero if has not been reached.
|
* zero if has not been reached.
|
||||||
*/
|
*/
|
||||||
private static volatile long lastTimeLimitWasHit; // must be volatile, since different threads access this
|
private static volatile long lastTimeLimitWasHit; // must be volatile, since different threads read/write to this
|
||||||
|
|
||||||
private ReturnYouTubeDislikeApi() {
|
private ReturnYouTubeDislikeApi() {
|
||||||
} // utility class
|
} // utility class
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only for local debugging to simulate a slow api call.
|
||||||
|
* Does this by doing meaningless calculations.
|
||||||
|
*
|
||||||
|
* @param maximumTimeToWait maximum time to wait
|
||||||
|
*/
|
||||||
|
private static long randomlyWaitIfLocallyDebugging(long maximumTimeToWait) {
|
||||||
|
final boolean DEBUG_RANDOMLY_DELAY_NETWORK_CALLS = false;
|
||||||
|
if (DEBUG_RANDOMLY_DELAY_NETWORK_CALLS) {
|
||||||
|
final long amountOfTimeToWaste = (long) (Math.random() * maximumTimeToWait);
|
||||||
|
final long timeCalculationStarted = System.currentTimeMillis();
|
||||||
|
LogHelper.printDebug(() -> "Artificially creating network delay of: " + amountOfTimeToWaste + " ms");
|
||||||
|
|
||||||
|
long meaninglessValue = 0;
|
||||||
|
while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) {
|
||||||
|
// could do a thread sleep, but that will trigger an exception if the thread is interrupted
|
||||||
|
meaninglessValue += Long.numberOfLeadingZeros((long) (Math.random() * Long.MAX_VALUE));
|
||||||
|
}
|
||||||
|
// return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work,
|
||||||
|
// leaving an empty loop that hammers on the System.currentTimeMillis native call
|
||||||
|
return meaninglessValue;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return True, if api rate limit is in effect.
|
* @return True, if api rate limit is in effect.
|
||||||
*/
|
*/
|
||||||
@ -68,7 +108,7 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
* @return True, if the rate limit was reached.
|
* @return True, if the rate limit was reached.
|
||||||
*/
|
*/
|
||||||
private static boolean checkIfRateLimitWasHit(int httpResponseCode) {
|
private static boolean checkIfRateLimitWasHit(int httpResponseCode) {
|
||||||
// set to true, to verify rate limit logic is working.
|
// set to true, to verify rate limit works
|
||||||
final boolean DEBUG_RATE_LIMIT = false;
|
final boolean DEBUG_RATE_LIMIT = false;
|
||||||
if (DEBUG_RATE_LIMIT) {
|
if (DEBUG_RATE_LIMIT) {
|
||||||
final double RANDOM_RATE_LIMIT_PERCENTAGE = 0.1; // 10% chance of a triggering a rate limit
|
final double RANDOM_RATE_LIMIT_PERCENTAGE = 0.1; // 10% chance of a triggering a rate limit
|
||||||
@ -89,7 +129,7 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The number of dislikes.
|
* @return The number of dislikes.
|
||||||
* Returns NULL if fetch failed, calling thread is interrupted, or rate limit is in effect.
|
* Returns NULL if fetch failed, or a rate limit is in effect.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public static Integer fetchDislikes(String videoId) {
|
public static Integer fetchDislikes(String videoId) {
|
||||||
@ -99,23 +139,36 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
if (checkIfRateLimitInEffect("fetchDislikes")) {
|
if (checkIfRateLimitInEffect("fetchDislikes")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
LogHelper.printDebug(() -> "Fetching dislikes for " + videoId);
|
LogHelper.printDebug(() -> "Fetching dislikes for: " + videoId);
|
||||||
|
|
||||||
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_DISLIKES, videoId);
|
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_DISLIKES, videoId);
|
||||||
connection.setConnectTimeout(HTTP_CONNECTION_DEFAULT_TIMEOUT);
|
// request headers, as per https://returnyoutubedislike.com/docs/fetching
|
||||||
|
// the documentation says to use 'Accept:text/html', but the RYD browser plugin uses 'Accept:application/json'
|
||||||
|
connection.setRequestProperty("Accept", "application/json");
|
||||||
|
connection.setRequestProperty("Connection", "keep-alive"); // keep-alive is on by default with http 1.1, but specify anyways
|
||||||
|
connection.setRequestProperty("Pragma", "no-cache");
|
||||||
|
connection.setRequestProperty("Cache-Control", "no-cache");
|
||||||
|
connection.setUseCaches(false);
|
||||||
|
connection.setConnectTimeout(API_GET_DISLIKE_DEFAULT_TIMEOUT_MILLISECONDS); // timeout for TCP connection to server
|
||||||
|
connection.setReadTimeout(API_GET_DISLIKE_DEFAULT_TIMEOUT_MILLISECONDS); // timeout for server response
|
||||||
|
|
||||||
|
randomlyWaitIfLocallyDebugging(2 * API_GET_DISLIKE_DEFAULT_TIMEOUT_MILLISECONDS);
|
||||||
|
|
||||||
final int responseCode = connection.getResponseCode();
|
final int responseCode = connection.getResponseCode();
|
||||||
if (checkIfRateLimitWasHit(responseCode)) {
|
if (checkIfRateLimitWasHit(responseCode)) {
|
||||||
connection.disconnect();
|
connection.disconnect();
|
||||||
return null;
|
return null;
|
||||||
} else if (responseCode == 200) {
|
|
||||||
JSONObject json = getJSONObject(connection);
|
|
||||||
Integer fetchedDislikeCount = json.getInt("dislikes");
|
|
||||||
LogHelper.printDebug(() -> "Dislikes fetched: " + fetchedDislikeCount);
|
|
||||||
connection.disconnect();
|
|
||||||
return fetchedDislikeCount;
|
|
||||||
} else {
|
|
||||||
LogHelper.printDebug(() -> "Dislikes fetch response was " + responseCode);
|
|
||||||
connection.disconnect();
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
LogHelper.printDebug(() -> "Failed to fetch dislikes for video: " + videoId
|
||||||
|
+ " response code was: " + responseCode);
|
||||||
|
connection.disconnect();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(() -> "Failed to fetch dislikes", ex);
|
LogHelper.printException(() -> "Failed to fetch dislikes", ex);
|
||||||
}
|
}
|
||||||
@ -133,35 +186,31 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String userId = randomString(36);
|
String userId = randomString(36);
|
||||||
LogHelper.printDebug(() -> "Trying to register the following userId: " + userId);
|
LogHelper.printDebug(() -> "Trying to register new user: " + userId);
|
||||||
|
|
||||||
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId);
|
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId);
|
||||||
connection.setConnectTimeout(HTTP_CONNECTION_DEFAULT_TIMEOUT);
|
connection.setRequestProperty("Accept", "application/json");
|
||||||
|
connection.setConnectTimeout(API_REGISTER_VOTE_DEFAULT_TIMEOUT_MILLISECONDS);
|
||||||
|
connection.setReadTimeout(API_REGISTER_VOTE_DEFAULT_TIMEOUT_MILLISECONDS);
|
||||||
|
|
||||||
final int responseCode = connection.getResponseCode();
|
final int responseCode = connection.getResponseCode();
|
||||||
if (checkIfRateLimitWasHit(responseCode)) {
|
if (checkIfRateLimitWasHit(responseCode)) {
|
||||||
connection.disconnect();
|
connection.disconnect();
|
||||||
return null;
|
return null;
|
||||||
} else if (responseCode == 200) {
|
}
|
||||||
JSONObject json = getJSONObject(connection);
|
if (responseCode == SUCCESS_HTTP_STATUS_CODE) {
|
||||||
|
JSONObject json = Requester.getJSONObject(connection); // also disconnects
|
||||||
String challenge = json.getString("challenge");
|
String challenge = json.getString("challenge");
|
||||||
int difficulty = json.getInt("difficulty");
|
int difficulty = json.getInt("difficulty");
|
||||||
|
|
||||||
LogHelper.printDebug(() -> "Registration challenge - " + challenge + " with difficulty of " + difficulty);
|
|
||||||
connection.disconnect();
|
|
||||||
|
|
||||||
// Solve the puzzle
|
|
||||||
String solution = solvePuzzle(challenge, difficulty);
|
String solution = solvePuzzle(challenge, difficulty);
|
||||||
LogHelper.printDebug(() -> "Registration confirmation solution is " + solution);
|
|
||||||
if (solution == null) {
|
|
||||||
return null; // failed to solve puzzle
|
|
||||||
}
|
|
||||||
return confirmRegistration(userId, solution);
|
return confirmRegistration(userId, solution);
|
||||||
} else {
|
|
||||||
LogHelper.printDebug(() -> "Registration response was " + responseCode);
|
|
||||||
connection.disconnect();
|
|
||||||
}
|
}
|
||||||
|
LogHelper.printDebug(() -> "Failed to register new user: " + userId
|
||||||
|
+ " response code was: " + responseCode);
|
||||||
|
connection.disconnect();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(() -> "Failed to register userId", ex);
|
LogHelper.printException(() -> "Failed to register user", ex);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -175,10 +224,10 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
if (checkIfRateLimitInEffect("confirmRegistration")) {
|
if (checkIfRateLimitInEffect("confirmRegistration")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
LogHelper.printDebug(() -> "Trying to confirm registration for the following userId: " + userId + " with solution: " + solution);
|
LogHelper.printDebug(() -> "Trying to confirm registration for user: " + userId + " with solution: " + solution);
|
||||||
|
|
||||||
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_REGISTRATION, userId);
|
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_REGISTRATION, userId);
|
||||||
applyCommonRequestSettings(connection);
|
applyCommonPostRequestSettings(connection);
|
||||||
|
|
||||||
String jsonInputString = "{\"solution\": \"" + solution + "\"}";
|
String jsonInputString = "{\"solution\": \"" + solution + "\"}";
|
||||||
try (OutputStream os = connection.getOutputStream()) {
|
try (OutputStream os = connection.getOutputStream()) {
|
||||||
@ -190,22 +239,22 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
connection.disconnect();
|
connection.disconnect();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (responseCode == SUCCESS_HTTP_STATUS_CODE) {
|
||||||
if (responseCode == 200) {
|
String result = Requester.parseJson(connection); // also disconnects
|
||||||
String result = parseJson(connection);
|
|
||||||
LogHelper.printDebug(() -> "Registration confirmation result was " + result);
|
|
||||||
connection.disconnect();
|
|
||||||
|
|
||||||
if (result.equalsIgnoreCase("true")) {
|
if (result.equalsIgnoreCase("true")) {
|
||||||
LogHelper.printDebug(() -> "Registration was successful for user " + userId);
|
LogHelper.printDebug(() -> "Registration confirmation successful for user: " + userId);
|
||||||
return userId;
|
return userId;
|
||||||
}
|
}
|
||||||
} else {
|
LogHelper.printDebug(() -> "Failed to confirm registration for user: " + userId
|
||||||
LogHelper.printDebug(() -> "Registration confirmation response was " + responseCode);
|
+ " solution: " + solution + " response string was: " + result);
|
||||||
connection.disconnect();
|
return null;
|
||||||
}
|
}
|
||||||
|
LogHelper.printDebug(() -> "Failed to confirm registration for user: " + userId
|
||||||
|
+ " solution: " + solution + " response code was: " + responseCode);
|
||||||
|
connection.disconnect();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(() -> "Failed to confirm registration", ex);
|
LogHelper.printException(() -> "Failed to confirm registration for user: " + userId
|
||||||
|
+ "solution: " + solution, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -217,14 +266,15 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
Objects.requireNonNull(userId);
|
Objects.requireNonNull(userId);
|
||||||
Objects.requireNonNull(vote);
|
Objects.requireNonNull(vote);
|
||||||
|
|
||||||
if (checkIfRateLimitInEffect("sendVote")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
LogHelper.printDebug(() -> "Trying to vote the following video: "
|
|
||||||
+ videoId + " with vote " + vote + " and userId: " + userId);
|
|
||||||
try {
|
try {
|
||||||
|
if (checkIfRateLimitInEffect("sendVote")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LogHelper.printDebug(() -> "Trying to vote for video: "
|
||||||
|
+ videoId + " with vote: " + vote + " user: " + userId);
|
||||||
|
|
||||||
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.SEND_VOTE);
|
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.SEND_VOTE);
|
||||||
applyCommonRequestSettings(connection);
|
applyCommonPostRequestSettings(connection);
|
||||||
|
|
||||||
String voteJsonString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"value\": \"" + vote.value + "\"}";
|
String voteJsonString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"value\": \"" + vote.value + "\"}";
|
||||||
try (OutputStream os = connection.getOutputStream()) {
|
try (OutputStream os = connection.getOutputStream()) {
|
||||||
@ -237,26 +287,20 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
connection.disconnect();
|
connection.disconnect();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (responseCode == SUCCESS_HTTP_STATUS_CODE) {
|
||||||
if (responseCode == 200) {
|
JSONObject json = Requester.getJSONObject(connection); // also disconnects
|
||||||
JSONObject json = getJSONObject(connection);
|
|
||||||
String challenge = json.getString("challenge");
|
String challenge = json.getString("challenge");
|
||||||
int difficulty = json.getInt("difficulty");
|
int difficulty = json.getInt("difficulty");
|
||||||
LogHelper.printDebug(() -> "Vote challenge - " + challenge + " with difficulty of " + difficulty);
|
|
||||||
connection.disconnect();
|
|
||||||
|
|
||||||
// Solve the puzzle
|
|
||||||
String solution = solvePuzzle(challenge, difficulty);
|
String solution = solvePuzzle(challenge, difficulty);
|
||||||
LogHelper.printDebug(() -> "Vote confirmation solution is " + solution);
|
|
||||||
|
|
||||||
// Confirm vote
|
|
||||||
return confirmVote(videoId, userId, solution);
|
return confirmVote(videoId, userId, solution);
|
||||||
} else {
|
|
||||||
LogHelper.printDebug(() -> "Vote response was " + responseCode);
|
|
||||||
connection.disconnect();
|
|
||||||
}
|
}
|
||||||
|
LogHelper.printDebug(() -> "Failed to send vote for video: " + videoId
|
||||||
|
+ " userId: " + userId + " vote: " + vote + " response code was: " + responseCode);
|
||||||
|
connection.disconnect();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(() -> "Failed to send vote", ex);
|
LogHelper.printException(() -> "Failed to send vote for video: " + videoId
|
||||||
|
+ " user: " + userId + " vote: " + vote, ex);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -267,12 +311,14 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
Objects.requireNonNull(userId);
|
Objects.requireNonNull(userId);
|
||||||
Objects.requireNonNull(solution);
|
Objects.requireNonNull(solution);
|
||||||
|
|
||||||
if (checkIfRateLimitInEffect("confirmVote")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
|
if (checkIfRateLimitInEffect("confirmVote")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LogHelper.printDebug(() -> "Trying to confirm vote for video: "
|
||||||
|
+ videoId + " user: " + userId + " solution: " + solution);
|
||||||
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE);
|
HttpURLConnection connection = getConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE);
|
||||||
applyCommonRequestSettings(connection);
|
applyCommonPostRequestSettings(connection);
|
||||||
|
|
||||||
String jsonInputString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"solution\": \"" + solution + "\"}";
|
String jsonInputString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"solution\": \"" + solution + "\"}";
|
||||||
try (OutputStream os = connection.getOutputStream()) {
|
try (OutputStream os = connection.getOutputStream()) {
|
||||||
@ -285,36 +331,38 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseCode == 200) {
|
if (responseCode == SUCCESS_HTTP_STATUS_CODE) {
|
||||||
String result = parseJson(connection);
|
String result = Requester.parseJson(connection); // also disconnects
|
||||||
LogHelper.printDebug(() -> "Vote confirmation result was " + result);
|
|
||||||
connection.disconnect();
|
|
||||||
|
|
||||||
if (result.equalsIgnoreCase("true")) {
|
if (result.equalsIgnoreCase("true")) {
|
||||||
LogHelper.printDebug(() -> "Vote was successful for user " + userId);
|
LogHelper.printDebug(() -> "Vote confirm successful for video: " + videoId);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
|
||||||
LogHelper.printDebug(() -> "Vote was unsuccessful for user " + userId);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
} else {
|
LogHelper.printDebug(() -> "Failed to confirm vote for video: " + videoId
|
||||||
LogHelper.printDebug(() -> "Vote confirmation response was " + responseCode);
|
+ " user: " + userId + " solution: " + solution + " response string was: " + result);
|
||||||
connection.disconnect();
|
return false;
|
||||||
}
|
}
|
||||||
|
LogHelper.printDebug(() -> "Failed to confirm vote for video: " + videoId
|
||||||
|
+ " user: " + userId + " solution: " + solution + " response code was: " + responseCode);
|
||||||
|
connection.disconnect();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
LogHelper.printException(() -> "Failed to confirm vote", ex);
|
LogHelper.printException(() -> "Failed to confirm vote for video: " + videoId
|
||||||
|
+ " user: " + userId + " solution: " + solution, ex);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// utils
|
// utils
|
||||||
|
|
||||||
private static void applyCommonRequestSettings(HttpURLConnection connection) throws ProtocolException {
|
private static void applyCommonPostRequestSettings(HttpURLConnection connection) throws ProtocolException {
|
||||||
connection.setRequestMethod("POST");
|
connection.setRequestMethod("POST");
|
||||||
connection.setRequestProperty("Content-Type", "application/json");
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
connection.setRequestProperty("Accept", "application/json");
|
connection.setRequestProperty("Accept", "application/json");
|
||||||
|
connection.setRequestProperty("Pragma", "no-cache");
|
||||||
|
connection.setRequestProperty("Cache-Control", "no-cache");
|
||||||
|
connection.setUseCaches(false);
|
||||||
connection.setDoOutput(true);
|
connection.setDoOutput(true);
|
||||||
connection.setConnectTimeout(HTTP_CONNECTION_DEFAULT_TIMEOUT);
|
connection.setConnectTimeout(API_REGISTER_VOTE_DEFAULT_TIMEOUT_MILLISECONDS); // timeout for TCP connection to server
|
||||||
|
connection.setReadTimeout(API_REGISTER_VOTE_DEFAULT_TIMEOUT_MILLISECONDS); // timeout for server response
|
||||||
}
|
}
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
@ -323,11 +371,8 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
return Requester.getConnectionFromRoute(RYD_API_URL, route, params);
|
return Requester.getConnectionFromRoute(RYD_API_URL, route, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JSONObject getJSONObject(HttpURLConnection connection) throws Exception {
|
|
||||||
return Requester.getJSONObject(connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String solvePuzzle(String challenge, int difficulty) {
|
private static String solvePuzzle(String challenge, int difficulty) {
|
||||||
|
final long timeSolveStarted = System.currentTimeMillis();
|
||||||
byte[] decodedChallenge = Base64.decode(challenge, Base64.NO_WRAP);
|
byte[] decodedChallenge = Base64.decode(challenge, Base64.NO_WRAP);
|
||||||
|
|
||||||
byte[] buffer = new byte[20];
|
byte[] buffer = new byte[20];
|
||||||
@ -335,25 +380,31 @@ public class ReturnYouTubeDislikeApi {
|
|||||||
buffer[i] = decodedChallenge[i - 4];
|
buffer[i] = decodedChallenge[i - 4];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessageDigest md;
|
||||||
try {
|
try {
|
||||||
int maxCount = (int) (Math.pow(2, difficulty + 1) * 5);
|
md = MessageDigest.getInstance("SHA-512");
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-512");
|
} catch (NoSuchAlgorithmException ex) {
|
||||||
for (int i = 0; i < maxCount; i++) {
|
throw new IllegalStateException(ex); // should never happen
|
||||||
buffer[0] = (byte) i;
|
|
||||||
buffer[1] = (byte) (i >> 8);
|
|
||||||
buffer[2] = (byte) (i >> 16);
|
|
||||||
buffer[3] = (byte) (i >> 24);
|
|
||||||
byte[] messageDigest = md.digest(buffer);
|
|
||||||
|
|
||||||
if (countLeadingZeroes(messageDigest) >= difficulty) {
|
|
||||||
return Base64.encodeToString(new byte[]{buffer[0], buffer[1], buffer[2], buffer[3]}, Base64.NO_WRAP);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
LogHelper.printException(() -> "Failed to solve puzzle", ex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
final int maxCount = (int) (Math.pow(2, difficulty + 1) * 5);
|
||||||
|
for (int i = 0; i < maxCount; i++) {
|
||||||
|
buffer[0] = (byte) i;
|
||||||
|
buffer[1] = (byte) (i >> 8);
|
||||||
|
buffer[2] = (byte) (i >> 16);
|
||||||
|
buffer[3] = (byte) (i >> 24);
|
||||||
|
byte[] messageDigest = md.digest(buffer);
|
||||||
|
|
||||||
|
if (countLeadingZeroes(messageDigest) >= difficulty) {
|
||||||
|
String solution = Base64.encodeToString(new byte[]{buffer[0], buffer[1], buffer[2], buffer[3]}, Base64.NO_WRAP);
|
||||||
|
LogHelper.printDebug(() -> "Found puzzle solution: " + solution + " of difficulty: " + difficulty
|
||||||
|
+ " in: " + (System.currentTimeMillis() - timeSolveStarted) + " ms");
|
||||||
|
return solution;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// should never be reached
|
||||||
|
throw new IllegalStateException("Failed to solve puzzle challenge: " + challenge + " of difficulty: " + difficulty);
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://stackoverflow.com/a/157202
|
// https://stackoverflow.com/a/157202
|
||||||
|
@ -6,6 +6,12 @@ import android.content.res.Resources;
|
|||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import app.revanced.integrations.sponsorblock.player.PlayerType;
|
import app.revanced.integrations.sponsorblock.player.PlayerType;
|
||||||
|
|
||||||
public class ReVancedUtils {
|
public class ReVancedUtils {
|
||||||
@ -19,6 +25,44 @@ public class ReVancedUtils {
|
|||||||
private ReVancedUtils() {
|
private ReVancedUtils() {
|
||||||
} // utility class
|
} // utility class
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of background threads run concurrently
|
||||||
|
*/
|
||||||
|
private static final int SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS = 20;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General purpose pool for network calls and other background tasks.
|
||||||
|
*/
|
||||||
|
private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor(
|
||||||
|
2, // minimum 2 threads always ready to be used
|
||||||
|
10, // For any threads over the minimum, keep them alive 10 seconds after they go idle
|
||||||
|
SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS,
|
||||||
|
TimeUnit.SECONDS,
|
||||||
|
new LinkedBlockingQueue<Runnable>());
|
||||||
|
|
||||||
|
private static void checkIfPoolHasReachedLimit() {
|
||||||
|
if (backgroundThreadPool.getActiveCount() >= SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS) {
|
||||||
|
// Something is wrong. Background threads are piling up and not completing as expected,
|
||||||
|
// or some ReVanced code is submitting an unexpected number of background tasks.
|
||||||
|
LogHelper.printException(() -> "Reached maximum background thread count of "
|
||||||
|
+ SHARED_THREAD_POOL_MAXIMUM_BACKGROUND_THREADS + " threads");
|
||||||
|
|
||||||
|
// Because this condition will manifest as a slow running app or a memory leak,
|
||||||
|
// it might be best to show the user a toast or some other suggestion to restart the app.
|
||||||
|
// TODO? if debug is enabled, show a toast?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void runOnBackgroundThread(Runnable task) {
|
||||||
|
checkIfPoolHasReachedLimit();
|
||||||
|
backgroundThreadPool.execute(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> Future<T> submitOnBackgroundThread(Callable<T> call) {
|
||||||
|
checkIfPoolHasReachedLimit();
|
||||||
|
return backgroundThreadPool.submit(call);
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean containsAny(final String value, final String... targets) {
|
public static boolean containsAny(final String value, final String... targets) {
|
||||||
for (String string : targets)
|
for (String string : targets)
|
||||||
if (!string.isEmpty() && value.contains(string)) return true;
|
if (!string.isEmpty() && value.contains(string)) return true;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user