RYD and Video ad whitelisting base

This commit is contained in:
VancedOfficial 2022-01-10 22:58:25 +02:00
parent 92d58b7963
commit 308c49797a
21 changed files with 1527 additions and 2 deletions

View File

@ -0,0 +1,25 @@
package com.google.android.apps.youtube.app.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
public class SlimMetadataScrollableButtonContainerLayout extends ViewGroup {
public SlimMetadataScrollableButtonContainerLayout(Context context) {
super(context);
}
public SlimMetadataScrollableButtonContainerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SlimMetadataScrollableButtonContainerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
}
}

View File

@ -0,0 +1,213 @@
package fi.vanced.libraries.youtube.ads;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.player.VideoInformation.channelName;
import static fi.vanced.libraries.youtube.ui.SlimButtonContainer.adBlockButton;
import static fi.vanced.utils.VancedUtils.getPreferences;
import static fi.vanced.utils.VancedUtils.parseJson;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import org.json.JSONObject;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import fi.razerman.youtube.XGlobals;
import fi.vanced.libraries.youtube.player.ChannelModel;
import fi.vanced.libraries.youtube.player.VideoInformation;
import fi.vanced.utils.ObjectSerializer;
public class VideoAds {
public static final String TAG = "VI - VideoAds";
public static final String PREFERENCES_NAME = "channel-whitelist";
private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1";
private static final String YT_API_KEY = "replaceMeWithTheYouTubeAPIKey";
private static ArrayList<ChannelModel> whiteList;
private static Thread fetchThread = null;
static {
whiteList = parseWhitelist(YouTubeTikTokRoot_Application.getAppContext());
}
// Call to this needs to be injected in YT code
public static void setChannelName(String channelName) {
if (debug) {
Log.d(TAG, "channel name set to " + channelName);
}
VideoInformation.channelName = channelName;
if (adBlockButton != null) {
adBlockButton.changeEnabled(getShouldShowAds());
}
}
// Call to this needs to be injected in YT code (CURRENTLY NOT USED)
public static void newVideoLoaded(String videoId) {
if (debug) {
Log.d(TAG, "newVideoLoaded - " + videoId);
}
try {
if (fetchThread != null && fetchThread.getState() != Thread.State.TERMINATED) {
if (debug) {
Log.d(TAG, "Interrupting the thread. Current state " + fetchThread.getState());
}
fetchThread.interrupt();
}
}
catch (Exception ex) {
Log.e(TAG, "Error in the fetch thread", ex);
}
fetchThread = new Thread(() -> {
try {
if (debug) {
Log.d(TAG, "Fetching channelId for " + videoId);
}
HttpURLConnection connection = (HttpURLConnection) new URL(YT_API_URL + "/player?key=" + YT_API_KEY).openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json; utf-8");
connection.setRequestProperty("Accept", "application/json");
connection.setDoOutput(true);
connection.setConnectTimeout(2 * 1000);
// TODO: Actually fetch the version
String jsonInputString = "{\"context\": {\"client\": { \"clientName\": \"Android\", \"clientVersion\": \"16.49.37\" } }, \"videoId\": \"" + videoId + "\"}";
try(OutputStream os = connection.getOutputStream()) {
byte[] input = jsonInputString.getBytes("utf-8");
os.write(input, 0, input.length);
}
if (connection.getResponseCode() == 200) {
JSONObject json = new JSONObject(parseJson(connection));
JSONObject videoInfo = json.getJSONObject("videoDetails");
ChannelModel channelModel = new ChannelModel(videoInfo.getString("author"), videoInfo.getString("channelId"));
if (debug) {
Log.d(TAG, "channelId " + channelModel.getChannelId() + " fetched for author " + channelModel.getAuthor());
}
}
else if (debug) {
Log.d(TAG, "player fetch response was " + connection.getResponseCode());
}
}
catch (Exception ex) {
Log.e(TAG, "Failed to fetch channelId", ex);
return;
}
});
fetchThread.start();
}
public static boolean getShouldShowAds() {
if (channelName == null || channelName.isEmpty() || channelName.trim().isEmpty()) {
if (XGlobals.debug) {
Log.d(TAG, "getShouldShowAds skipped because channelId was null");
}
return false;
}
for (ChannelModel channelModel: whiteList) {
if (channelModel.getAuthor().equals(channelName)) {
if (XGlobals.debug) {
Log.d(TAG, "Video ad whitelist for " + channelName);
}
return true;
}
}
return false;
}
public static boolean addToWhitelist(Context context, String channelName, String channelId) {
try {
whiteList.add(new ChannelModel(channelName, channelId));
updateWhitelist(context);
return true;
}
catch (Exception ex) {
Log.d(TAG, "Unable to add " + channelName + " with id " + channelId + " to whitelist");
}
return false;
}
public static boolean removeFromWhitelist(Context context, String channelName) {
try {
//whiteList.removeIf(x -> x.getAuthor().equals(channelName)); // Requires Android N
Iterator<ChannelModel> iterator = whiteList.iterator();
while(iterator.hasNext())
{
ChannelModel value = iterator.next();
if (value.getAuthor().equals(channelName))
{
iterator.remove();
break;
}
}
updateWhitelist(context);
return true;
}
catch (Exception ex) {
Log.d(TAG, "Unable to remove " + channelName + " from whitelist");
}
return false;
}
private static void updateWhitelist(Context context) {
if (context == null) return;
SharedPreferences preferences = getPreferences(context, PREFERENCES_NAME);
SharedPreferences.Editor editor = preferences.edit();
try {
editor.putString("channels", ObjectSerializer.serialize(whiteList));
} catch (IOException e) {
e.printStackTrace();
}
editor.apply();
}
private static ArrayList<ChannelModel> parseWhitelist(Context context) {
if (context == null) return new ArrayList<>();
SharedPreferences preferences = getPreferences(context, PREFERENCES_NAME);
try {
String channels = preferences.getString("channels", null);
if (channels == null) {
if (debug) {
Log.d(TAG, "channels string was null for ad whitelisting");
}
return new ArrayList<>();
}
ArrayList<ChannelModel> channelModels = (ArrayList<ChannelModel>) ObjectSerializer.deserialize(channels);
if (debug) {
Log.d(TAG, channels);
for (ChannelModel channelModel: channelModels) {
Log.d(TAG, "Ad whitelisted " + channelModel.getAuthor() + " with id of " + channelModel.getChannelId());
}
}
return channelModels;
} catch (IOException e) {
e.printStackTrace();
}
return new ArrayList<>();
}
}

View File

@ -0,0 +1,29 @@
package fi.vanced.libraries.youtube.player;
import java.io.Serializable;
public class ChannelModel implements Serializable {
private String author;
private String channelId;
public ChannelModel(String author, String channelId) {
this.author = author;
this.channelId = channelId;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getChannelId() {
return channelId;
}
public void setChannelId(String channelId) {
this.channelId = channelId;
}
}

View File

@ -0,0 +1,62 @@
package fi.vanced.libraries.youtube.player;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import static fi.razerman.youtube.XGlobals.debug;
import static pl.jakubweg.StringRef.str;
public class VideoHelpers {
public static final String TAG = "VideoHelpers";
public static void copyVideoUrlToClipboard() {
generateVideoUrl(false);
}
public static void copyVideoUrlWithTimeStampToClipboard() {
generateVideoUrl(true);
}
private static void generateVideoUrl(boolean appendTimeStamp) {
try {
String videoId = VideoInformation.currentVideoId;
if (videoId == null || videoId.isEmpty()) {
if (debug) {
Log.d(TAG, "VideoId was empty");
}
return;
}
String videoUrl = String.format("https://youtu.be/%s", videoId);
if (appendTimeStamp) {
long videoTime = VideoInformation.lastKnownVideoTime;
videoUrl += String.format("?t=%s", (videoTime / 1000));
}
if (debug) {
Log.d(TAG, "Video URL: " + videoUrl);
}
setClipboard(YouTubeTikTokRoot_Application.getAppContext(), videoUrl);
Toast.makeText(YouTubeTikTokRoot_Application.getAppContext(), str("share_copy_url_success"), Toast.LENGTH_SHORT).show();
}
catch (Exception ex) {
Log.e(TAG, "Couldn't generate video url", ex);
}
}
private static void setClipboard(Context context, String text) {
if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) {
android.text.ClipboardManager clipboard = (android.text.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setText(text);
} else {
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
android.content.ClipData clip = android.content.ClipData.newPlainText("link", text);
clipboard.setPrimaryClip(clip);
}
}
}

View File

@ -1,6 +1,45 @@
package fi.vanced.libraries.youtube.player;
import static fi.razerman.youtube.XGlobals.debug;
import android.util.Log;
import fi.vanced.libraries.youtube.ryd.ReturnYouTubeDislikes;
public class VideoInformation {
private static final String TAG = "VI - VideoInfo";
public static String currentVideoId;
public static Integer dislikeCount = null;
public static String channelName = null;
public static long lastKnownVideoTime = -1L;
// Call hook in the YT code when the video changes
public static void setCurrentVideoId(final String videoId) {
if (videoId == null) {
if (debug) {
Log.d(TAG, "setCurrentVideoId - new id was null - currentVideoId was" + currentVideoId);
}
currentVideoId = null;
dislikeCount = null;
channelName = null;
return;
}
if (videoId.equals(currentVideoId)) {
if (debug) {
Log.d(TAG, "setCurrentVideoId - new and current video were equal - " + videoId);
}
return;
}
if (debug) {
Log.d(TAG, "setCurrentVideoId - video id updated from " + currentVideoId + " to " + videoId);
}
currentVideoId = videoId;
// New video
ReturnYouTubeDislikes.newVideoLoaded(videoId);
}
}

View File

@ -0,0 +1,149 @@
package fi.vanced.libraries.youtube.ryd;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.utils.VancedUtils.getPreferences;
import static fi.vanced.utils.VancedUtils.parseJson;
import static fi.vanced.utils.VancedUtils.randomString;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import org.json.JSONObject;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
public class Registration {
private static final String TAG = "VI - RYD - Registration";
public static final String PREFERENCES_NAME = "ryd";
private String userId;
private Context context;
public Registration(Context context) {
this.context = context;
}
public String getUserId() {
return userId != null ? userId : fetchUserId();
}
private String fetchUserId() {
try {
if (this.context == null) throw new Exception("Unable to fetch userId because context was null");
SharedPreferences preferences = getPreferences(context, PREFERENCES_NAME);
this.userId = preferences.getString("userId", null);
if (this.userId == null) {
this.userId = register();
}
}
catch (Exception ex) {
Log.e(TAG, "Unable to fetch the userId from shared preferences", ex);
}
return this.userId;
}
private void saveUserId(String userId) {
try {
if (this.context == null) throw new Exception("Unable to save userId because context was null");
SharedPreferences preferences = getPreferences(context, PREFERENCES_NAME);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("userId", userId).apply();
}
catch (Exception ex) {
Log.e(TAG, "Unable to save the userId in shared preferences", ex);
}
}
private String register() {
try {
// Generate a new userId
String userId = randomString(36);
if (debug) {
Log.d(TAG, "Trying to register the following userId: " + userId);
}
// Get the registration challenge
HttpURLConnection connection = (HttpURLConnection) new URL(ReturnYouTubeDislikes.RYD_API_URL + "/puzzle/registration?userId=" + userId).openConnection();
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced");
connection.setConnectTimeout(5 * 1000);
if (connection.getResponseCode() == 200) {
JSONObject json = new JSONObject(parseJson(connection));
String challenge = json.getString("challenge");
int difficulty = json.getInt("difficulty");
if (debug) {
Log.d(TAG, "Registration challenge - " + challenge + " with difficulty of " + difficulty);
}
// Solve the puzzle
String solution = Utils.solvePuzzle(challenge, difficulty);
if (debug) {
Log.d(TAG, "Registration confirmation solution is " + solution);
}
return confirmRegistration(userId, solution);
}
else if (debug) {
Log.d(TAG, "Registration response was " + connection.getResponseCode());
}
}
catch (Exception ex) {
Log.e(TAG, "Failed to register userId", ex);
}
return null;
}
public String confirmRegistration(String userId, String solution) {
try {
if (debug) {
Log.d(TAG, "Trying to confirm registration for the following userId: " + userId + " with solution: " + solution);
}
// Confirm registration
HttpURLConnection confirmationCon = (HttpURLConnection) new URL(ReturnYouTubeDislikes.RYD_API_URL + "/puzzle/registration?userId=" + userId).openConnection();
confirmationCon.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced");
confirmationCon.setRequestMethod("POST");
confirmationCon.setRequestProperty("Content-Type", "application/json");
confirmationCon.setRequestProperty("Accept", "application/json");
confirmationCon.setDoOutput(true);
confirmationCon.setConnectTimeout(5 * 1000);
String jsonInputString = "{\"solution\": \"" + solution + "\"}";
try(OutputStream os = confirmationCon.getOutputStream()) {
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
if (confirmationCon.getResponseCode() == 200) {
String result = parseJson(confirmationCon);
if (debug) {
Log.d(TAG, "Registration confirmation result was " + result);
}
if (result.equalsIgnoreCase("true")) {
saveUserId(userId);
if (debug) {
Log.d(TAG, "Registration was successful for user " + userId);
}
return userId;
}
}
else if (debug) {
Log.d(TAG, "Registration confirmation response was " + confirmationCon.getResponseCode());
}
}
catch (Exception ex) {
Log.e(TAG, "Failed to confirm registration", ex);
}
return null;
}
}

View File

@ -0,0 +1,285 @@
package fi.vanced.libraries.youtube.ryd;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.player.VideoInformation.currentVideoId;
import static fi.vanced.utils.VancedUtils.getIdentifier;
import static fi.vanced.utils.VancedUtils.parseJson;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
import static fi.vanced.libraries.youtube.player.VideoInformation.dislikeCount;
public class ReturnYouTubeDislikes {
public static final String RYD_API_URL = "https://returnyoutubedislikeapi.com";
private static final String TAG = "VI - RYD";
private static View _dislikeView = null;
private static Thread _dislikeFetchThread = null;
private static Thread _votingThread = null;
private static Registration registration;
private static Voting voting;
private static boolean likeActive;
private static boolean dislikeActive;
private static int votingValue = 0; // 1 = like, -1 = dislike, 0 = no vote
static {
registration = new Registration(YouTubeTikTokRoot_Application.getAppContext());
voting = new Voting(YouTubeTikTokRoot_Application.getAppContext(), registration);
}
public static void newVideoLoaded(String videoId) {
if (debug) {
Log.d(TAG, "newVideoLoaded - " + videoId);
}
try {
if (_dislikeFetchThread != null && _dislikeFetchThread.getState() != Thread.State.TERMINATED) {
if (debug) {
Log.d(TAG, "Interrupting the thread. Current state " + _dislikeFetchThread.getState());
}
_dislikeFetchThread.interrupt();
}
}
catch (Exception ex) {
Log.e(TAG, "Error in the dislike fetch thread", ex);
}
_dislikeFetchThread = new Thread(() -> {
try {
if (debug) {
Log.d(TAG, "Fetching dislikes for " + videoId);
}
HttpURLConnection connection = (HttpURLConnection) new URL(RYD_API_URL + "/votes?videoId=" + videoId).openConnection();
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced");
connection.setConnectTimeout(5 * 1000);
if (connection.getResponseCode() == 200) {
JSONObject json = new JSONObject(parseJson(connection));
dislikeCount = json.getInt("dislikes");
if (debug) {
Log.d(TAG, "dislikes fetched - " + dislikeCount);
}
// Set the dislikes
new Handler(Looper.getMainLooper()).post(new Runnable () {
@Override
public void run () {
trySetDislikes(String.valueOf(dislikeCount));
}
});
}
else if (debug) {
Log.d(TAG, "dislikes fetch response was " + connection.getResponseCode());
}
}
catch (Exception ex) {
dislikeCount = null;
Log.e(TAG, "Failed to fetch dislikes", ex);
return;
}
});
_dislikeFetchThread.start();
}
// Call to this needs to be injected in YT code
public static void setLikeTag(View view) {
setTag(view, "like");
}
public static void setLikeTag(View view, boolean active) {
likeActive = active;
if (likeActive) {
votingValue = 1;
}
if (debug) {
Log.d(TAG, "Like tag active " + likeActive);
}
setTag(view, "like");
}
// Call to this needs to be injected in YT code
public static void setDislikeTag(View view) {
_dislikeView = view;
setTag(view, "dislike");
}
public static void setDislikeTag(View view, boolean active) {
dislikeActive = active;
if (dislikeActive) {
votingValue = -1;
}
_dislikeView = view;
if (debug) {
Log.d(TAG, "Dislike tag active " + dislikeActive);
}
setTag(view, "dislike");
}
// Call to this needs to be injected in YT code
public static CharSequence onSetText(View view, CharSequence originalText) {
return handleOnSetText(view, originalText);
}
// Call to this needs to be injected in YT code
public static void onClick(View view, boolean inactive) {
handleOnClick(view, inactive);
}
private static CharSequence handleOnSetText(View view, CharSequence originalText) {
try {
CharSequence tag = (CharSequence) view.getTag();
if (debug) {
Log.d(TAG, "handleOnSetText - " + tag + " - original text - " + originalText);
}
if (tag == null) return originalText;
if (tag == "like") {
return originalText;
}
else if (tag == "dislike") {
return dislikeCount != null ? String.valueOf(dislikeCount) : originalText;
}
}
catch (Exception ex) {
Log.e(TAG, "Error while handling the setText", ex);
}
return originalText;
}
private static void trySetDislikes(String dislikeCount) {
try {
// Try to set normal video dislike count
if (_dislikeView == null) {
if (debug) { Log.d(TAG, "_dislikeView was null"); }
return;
}
View buttonView = _dislikeView.findViewById(getIdentifier("button_text", "id"));
if (buttonView == null) {
if (debug) { Log.d(TAG, "buttonView was null"); }
return;
}
TextView button = (TextView) buttonView;
button.setText(dislikeCount);
if (debug) {
Log.d(TAG, "trySetDislikes - " + dislikeCount);
}
}
catch (Exception ex) {
if (debug) {
Log.e(TAG, "Error while trying to set dislikes text", ex);
}
}
}
private static void handleOnClick(View view, boolean previousState) {
try {
String tag = (String) view.getTag();
if (debug) {
Log.d(TAG, "handleOnClick - " + tag + " - previousState - " + previousState);
}
if (tag == null) return;
// If active status was removed, vote should be none
if (previousState) { votingValue = 0; }
if (tag == "like") {
dislikeActive = false;
// Like was activated
if (!previousState) { votingValue = 1; likeActive = true; }
else { likeActive = false; }
// Like was activated and dislike was previously activated
if (!previousState && dislikeActive) { dislikeCount--; trySetDislikes(String.valueOf(dislikeCount)); }
}
else if (tag == "dislike") {
likeActive = false;
// Dislike was activated
if (!previousState) { votingValue = -1; dislikeActive = true; dislikeCount++; }
// Dislike was removed
else { dislikeActive = false; dislikeCount--; }
trySetDislikes(String.valueOf(dislikeCount));
}
else {
// Unknown tag
return;
}
if (debug) {
Log.d(TAG, "New vote status - " + votingValue);
Log.d(TAG, "Like button " + likeActive + " | Dislike button " + dislikeActive);
}
Toast.makeText(YouTubeTikTokRoot_Application.getAppContext(), "Voting value: " + votingValue, Toast.LENGTH_SHORT).show();
sendVote(votingValue);
}
catch (Exception ex) {
Log.e(TAG, "Error while handling the onClick", ex);
}
}
private static void sendVote(int vote) {
if (debug) {
Log.d(TAG, "sending vote - " + vote + " for video " + currentVideoId);
}
try {
if (_votingThread != null && _votingThread.getState() != Thread.State.TERMINATED) {
if (debug) {
Log.d(TAG, "Interrupting the thread. Current state " + _votingThread.getState());
}
_votingThread.interrupt();
}
}
catch (Exception ex) {
Log.e(TAG, "Error in the voting thread", ex);
}
_votingThread = new Thread(() -> {
try {
boolean result = voting.sendVote(currentVideoId, vote);
if (debug) {
Log.d(TAG, "sendVote status " + result);
}
}
catch (Exception ex) {
Log.e(TAG, "Failed to send vote", ex);
return;
}
});
_votingThread.start();
}
private static void setTag(View view, String tag) {
try {
if (view == null) {
if (debug) {
Log.d(TAG, "View was empty");
}
return;
}
if (debug) {
Log.d(TAG, "setTag - " + tag);
}
view.setTag(tag);
}
catch (Exception ex) {
Log.e(TAG, "Error while trying to set tag to view", ex);
}
}
}

View File

@ -0,0 +1,64 @@
package fi.vanced.libraries.youtube.ryd;
import android.util.Log;
import android.util.Base64;
import java.security.MessageDigest;
public class Utils {
private static final String TAG = "VI - RYD - Utils";
static String solvePuzzle(String challenge, int difficulty) {
byte[] decodedChallenge = Base64.decode(challenge, Base64.NO_WRAP);
byte[] buffer = new byte[20];
for (int i = 4; i < 20; i++) {
buffer[i] = decodedChallenge[i - 4];
}
try {
int maxCount = (int) (Math.pow(2, difficulty + 1) * 5);
MessageDigest md = MessageDigest.getInstance("SHA-512");
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 encode = Base64.encodeToString(new byte[]{buffer[0], buffer[1], buffer[2], buffer[3]}, Base64.NO_WRAP);
return encode;
}
}
}
catch (Exception ex) {
Log.e(TAG, "Failed to solve puzzle", ex);
}
return null;
}
static int countLeadingZeroes(byte[] uInt8View) {
int zeroes = 0;
int value = 0;
for (int i = 0; i < uInt8View.length; i++) {
value = uInt8View[i] & 0xFF;
if (value == 0) {
zeroes += 8;
} else {
int count = 1;
if (value >>> 4 == 0) {
count += 4;
value <<= 4;
}
if (value >>> 6 == 0) {
count += 2;
value <<= 2;
}
zeroes += count - (value >>> 7);
break;
}
}
return zeroes;
}
}

View File

@ -0,0 +1,120 @@
package fi.vanced.libraries.youtube.ryd;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.utils.VancedUtils.parseJson;
import android.content.Context;
import android.util.Log;
import org.json.JSONObject;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
public class Voting {
private static final String TAG = "VI - RYD - Voting";
private Registration registration;
private Context context;
public Voting(Context context, Registration registration) {
this.context = context;
this.registration = registration;
}
public boolean sendVote(String videoId, int vote) {
try {
String userId = registration.getUserId();
if (debug) {
Log.d(TAG, "Trying to vote the following video: " + videoId + " with vote " + vote + " and userId: " + userId);
}
// Send the vote
HttpURLConnection connection = (HttpURLConnection) new URL(ReturnYouTubeDislikes.RYD_API_URL + "/interact/vote").openConnection();
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced");
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "application/json");
connection.setDoOutput(true);
connection.setConnectTimeout(5 * 1000);
String voteJsonString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"value\": \"" + vote + "\"}";
try(OutputStream os = connection.getOutputStream()) {
byte[] input = voteJsonString.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
if (connection.getResponseCode() == 200) {
JSONObject json = new JSONObject(parseJson(connection));
String challenge = json.getString("challenge");
int difficulty = json.getInt("difficulty");
if (debug) {
Log.d(TAG, "Vote challenge - " + challenge + " with difficulty of " + difficulty);
}
// Solve the puzzle
String solution = Utils.solvePuzzle(challenge, difficulty);
if (debug) {
Log.d(TAG, "Vote confirmation solution is " + solution);
}
// Confirm vote
return confirmVote(userId, videoId, solution);
}
else if (debug) {
Log.d(TAG, "Vote response was " + connection.getResponseCode());
}
}
catch (Exception ex) {
Log.e(TAG, "Failed to send vote", ex);
}
return false;
}
public boolean confirmVote(String userId, String videoId, String solution) {
try {
if (debug) {
Log.d(TAG, "Trying to confirm vote for video: " + videoId + " with solution " + solution + " and userId: " + userId);
}
// Confirm vote
HttpURLConnection confirmationCon = (HttpURLConnection) new URL(ReturnYouTubeDislikes.RYD_API_URL + "/interact/confirmVote").openConnection();
confirmationCon.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced");
confirmationCon.setRequestMethod("POST");
confirmationCon.setRequestProperty("Content-Type", "application/json");
confirmationCon.setRequestProperty("Accept", "application/json");
confirmationCon.setDoOutput(true);
confirmationCon.setConnectTimeout(5 * 1000);
String jsonInputString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"solution\": \"" + solution + "\"}";
try(OutputStream os = confirmationCon.getOutputStream()) {
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
if (confirmationCon.getResponseCode() == 200) {
String result = parseJson(confirmationCon);
if (debug) {
Log.d(TAG, "Vote confirmation result was " + result);
}
if (result.equalsIgnoreCase("true")) {
if (debug) {
Log.d(TAG, "Vote was successful for user " + userId);
}
return true;
}
}
else if (debug) {
Log.d(TAG, "Vote confirmation response was " + confirmationCon.getResponseCode());
}
}
catch (Exception ex) {
Log.e(TAG, "Failed to send vote", ex);
}
return false;
}
}

View File

@ -0,0 +1,135 @@
package fi.vanced.libraries.youtube.ui;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.Toast;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.ads.VideoAds.getShouldShowAds;
import static fi.vanced.libraries.youtube.player.VideoInformation.currentVideoId;
import static fi.vanced.utils.VancedUtils.parseJson;
import static pl.jakubweg.StringRef.str;
import org.json.JSONObject;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import fi.vanced.libraries.youtube.ads.VideoAds;
import fi.vanced.libraries.youtube.player.ChannelModel;
import fi.vanced.libraries.youtube.player.VideoInformation;
import fi.vanced.utils.VancedUtils;
public class AdBlock extends SlimButton {
private static final String TAG = "VI - AdBlock - Button";
private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1";
private static final String YT_API_KEY = "replaceMeWithTheYouTubeAPIKey";
public AdBlock(Context context, ViewGroup container) {
super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, true);
initialize();
}
private void initialize() {
this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_yt_ad_button", "drawable"));
this.button_text.setText(str("action_ads"));
changeEnabled(getShouldShowAds());
}
public void changeEnabled(boolean enabled) {
if (debug) {
Log.d(TAG, "changeEnabled " + enabled);
}
this.button_icon.setEnabled(enabled);
}
@Override
public void onClick(View view) {
this.view.setEnabled(false);
if (this.button_icon.isEnabled()) {
removeFromWhitelist();
return;
}
//this.button_icon.setEnabled(!this.button_icon.isEnabled());
addToWhiteList(this.view, this.button_icon);
}
private void removeFromWhitelist() {
try {
VideoAds.removeFromWhitelist(this.context, VideoInformation.channelName);
this.button_icon.setEnabled(false);
}
catch (Exception ex) {
Log.e(TAG, "Failed to remove from whitelist", ex);
return;
}
this.view.setEnabled(true);
}
private void addToWhiteList(View view, ImageView buttonIcon) {
new Thread(() -> {
try {
if (debug) {
Log.d(TAG, "Fetching channelId for " + currentVideoId);
}
HttpURLConnection connection = (HttpURLConnection) new URL(YT_API_URL + "/player?key=" + YT_API_KEY).openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json; utf-8");
connection.setRequestProperty("Accept", "application/json");
connection.setDoOutput(true);
connection.setConnectTimeout(2 * 1000);
// TODO: Actually fetch the version
String jsonInputString = "{\"context\": {\"client\": { \"clientName\": \"Android\", \"clientVersion\": \"16.49.37\" } }, \"videoId\": \"" + currentVideoId + "\"}";
try(OutputStream os = connection.getOutputStream()) {
byte[] input = jsonInputString.getBytes("utf-8");
os.write(input, 0, input.length);
}
if (connection.getResponseCode() == 200) {
JSONObject json = new JSONObject(parseJson(connection));
JSONObject videoInfo = json.getJSONObject("videoDetails");
ChannelModel channelModel = new ChannelModel(videoInfo.getString("author"), videoInfo.getString("channelId"));
if (debug) {
Log.d(TAG, "channelId " + channelModel.getChannelId() + " fetched for author " + channelModel.getAuthor());
}
boolean success = VideoAds.addToWhitelist(this.context, channelModel.getAuthor(), channelModel.getChannelId());
new Handler(Looper.getMainLooper()).post(() -> {
if (success) {
buttonIcon.setEnabled(true);
Toast.makeText(context, "Channel " + channelModel.getAuthor() + " whitelisted", Toast.LENGTH_SHORT).show();
}
else {
buttonIcon.setEnabled(false);
Toast.makeText(context, "Channel " + channelModel.getAuthor() + " failed to whitelist", Toast.LENGTH_SHORT).show();
}
view.setEnabled(true);
});
}
else {
if (debug) {
Log.d(TAG, "player fetch response was " + connection.getResponseCode());
}
buttonIcon.setEnabled(false);
this.view.setEnabled(true);
}
}
catch (Exception ex) {
Log.e(TAG, "Failed to fetch channelId", ex);
this.view.setEnabled(true);
return;
}
}).start();
}
}

View File

@ -0,0 +1,29 @@
package fi.vanced.libraries.youtube.ui;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import fi.vanced.libraries.youtube.player.VideoHelpers;
import fi.vanced.utils.SharedPrefUtils;
import fi.vanced.utils.VancedUtils;
public class CopyButton extends SlimButton {
public CopyButton(Context context, ViewGroup container) {
super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, SharedPrefUtils.getBoolean(context, "youtube", "pref_copy_video_url_button", false));
initialize();
}
private void initialize() {
this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_yt_copy_icon", "drawable"));
this.button_text.setText(str("action_copy"));
}
@Override
public void onClick(View view) {
VideoHelpers.copyVideoUrlToClipboard();
}
}

View File

@ -0,0 +1,29 @@
package fi.vanced.libraries.youtube.ui;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import fi.vanced.libraries.youtube.player.VideoHelpers;
import fi.vanced.utils.SharedPrefUtils;
import fi.vanced.utils.VancedUtils;
import static pl.jakubweg.StringRef.str;
public class CopyWithTimestamp extends SlimButton {
public CopyWithTimestamp(Context context, ViewGroup container) {
super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, SharedPrefUtils.getBoolean(context, "youtube", "pref_copy_video_url_timestamp_button", false));
initialize();
}
private void initialize() {
this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_yt_copy_icon_with_time", "drawable"));
this.button_text.setText(str("action_tcopy"));
}
@Override
public void onClick(View view) {
VideoHelpers.copyVideoUrlWithTimeStampToClipboard();
}
}

View File

@ -0,0 +1,61 @@
package fi.vanced.libraries.youtube.ui;
import static fi.razerman.youtube.XGlobals.debug;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import fi.vanced.utils.VancedUtils;
public abstract class SlimButton implements View.OnClickListener {
private static final String TAG = "VI - Slim - Button";
public static int SLIM_METADATA_BUTTON_ID;
public final View view;
public final Context context;
private final ViewGroup container;
protected final ImageView button_icon;
protected final TextView button_text;
static {
SLIM_METADATA_BUTTON_ID = VancedUtils.getIdentifier("slim_metadata_button", "layout");
}
public SlimButton(Context context, ViewGroup container, int id, boolean visible) {
if (debug) {
Log.d(TAG, "Adding button with id " + id + " and visibility of " + visible);
}
this.context = context;
this.container = container;
view = LayoutInflater.from(context).inflate(id, container, false);
button_icon = (ImageView)view.findViewById(VancedUtils.getIdentifier("button_icon", "id"));
button_text = (TextView)view.findViewById(VancedUtils.getIdentifier("button_text", "id"));
view.setOnClickListener(this);
setVisible(visible);
container.addView(view);
}
public void setVisible(boolean visible) {
view.setVisibility(visible ? View.VISIBLE : View.GONE);
setContainerVisibility();
}
private void setContainerVisibility() {
if (container == null) return;
for (int i = 0; i < container.getChildCount(); i++) {
if (container.getChildAt(i).getVisibility() == View.VISIBLE) {
container.setVisibility(View.VISIBLE);
return;
}
}
container.setVisibility(View.GONE);
}
}

View File

@ -0,0 +1,49 @@
package fi.vanced.libraries.youtube.ui;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ViewGroup;
import com.google.android.apps.youtube.app.ui.SlimMetadataScrollableButtonContainerLayout;
import fi.vanced.utils.VancedUtils;
public class SlimButtonContainer extends SlimMetadataScrollableButtonContainerLayout {
private static final String TAG = "VI - Slim - Container";
private ViewGroup container;
private CopyButton copyButton;
private CopyWithTimestamp copyWithTimestampButton;
public static AdBlock adBlockButton;
public SlimButtonContainer(Context context) {
super(context);
this.initialize(context);
}
public SlimButtonContainer(Context context, AttributeSet attrs) {
super(context, attrs);
this.initialize(context);
}
public SlimButtonContainer(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.initialize(context);
}
public void initialize(Context context) {
try {
container = this.findViewById(VancedUtils.getIdentifier("button_container_vanced", "id"));
if (container == null) throw new Exception("Unable to initialize the button container because the button_container_vanced couldn't be found");
copyButton = new CopyButton(context, this);
copyWithTimestampButton = new CopyWithTimestamp(context, this);
adBlockButton = new AdBlock(context, this);
new SponsorBlock(context, this);
new SponsorBlockVoting(context, this);
}
catch (Exception ex) {
Log.e(TAG, "Unable to initialize the button container", ex);
}
}
}

View File

@ -0,0 +1,31 @@
package fi.vanced.libraries.youtube.ui;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import fi.vanced.libraries.youtube.player.VideoHelpers;
import fi.vanced.utils.VancedUtils;
public class SponsorBlock extends SlimButton {
public SponsorBlock(Context context, ViewGroup container) {
super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, false);
initialize();
}
private void initialize() {
this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_sb_logo", "drawable"));
this.button_text.setText("SB");
}
@Override
public void onClick(View view) {
Toast.makeText(YouTubeTikTokRoot_Application.getAppContext(), "Nothing atm", Toast.LENGTH_SHORT).show();
}
}

View File

@ -0,0 +1,28 @@
package fi.vanced.libraries.youtube.ui;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import fi.vanced.utils.VancedUtils;
public class SponsorBlockVoting extends SlimButton {
public SponsorBlockVoting(Context context, ViewGroup container) {
super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, false);
initialize();
}
private void initialize() {
this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_sb_voting", "drawable"));
this.button_text.setText("SB Voting");
}
@Override
public void onClick(View view) {
Toast.makeText(YouTubeTikTokRoot_Application.getAppContext(), "Nothing atm", Toast.LENGTH_SHORT).show();
}
}

View File

@ -0,0 +1,83 @@
package fi.vanced.utils;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Modifications copyright (C) 2022 Vanced
*/
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class ObjectSerializer {
private static final String TAG = "VI - ObjectSerializer";
public static String serialize(Serializable obj) throws IOException {
if (obj == null) return "";
try {
ByteArrayOutputStream serialObj = new ByteArrayOutputStream();
ObjectOutputStream objStream = new ObjectOutputStream(serialObj);
objStream.writeObject(obj);
objStream.close();
return encodeBytes(serialObj.toByteArray());
} catch (Exception e) {
Log.e(TAG, "Serialization error: " + e.getMessage(), e);
throw new IOException(e);
}
}
public static Object deserialize(String str) throws IOException {
if (str == null || str.length() == 0) return null;
try {
ByteArrayInputStream serialObj = new ByteArrayInputStream(decodeBytes(str));
ObjectInputStream objStream = new ObjectInputStream(serialObj);
return objStream.readObject();
} catch (Exception e) {
Log.e(TAG, "Deserialization error: " + e.getMessage(), e);
throw new IOException(e);
}
}
public static String encodeBytes(byte[] bytes) {
StringBuffer strBuf = new StringBuffer();
for (int i = 0; i < bytes.length; i++) {
strBuf.append((char) (((bytes[i] >> 4) & 0xF) + ((int) 'a')));
strBuf.append((char) (((bytes[i]) & 0xF) + ((int) 'a')));
}
return strBuf.toString();
}
public static byte[] decodeBytes(String str) {
byte[] bytes = new byte[str.length() / 2];
for (int i = 0; i < str.length(); i+=2) {
char c = str.charAt(i);
bytes[i/2] = (byte) ((c - 'a') << 4);
c = str.charAt(i+1);
bytes[i/2] += (c - 'a');
}
return bytes;
}
}

View File

@ -0,0 +1,43 @@
package fi.vanced.utils;
import android.content.Context;
import android.content.SharedPreferences;
public class SharedPrefUtils {
public static void saveString(Context context, String preferenceName, String key, String value){
SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
sharedPreferences.edit().putString(key, value).apply();
}
public static void saveBoolean(Context context, String preferenceName, String key, Boolean value){
SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
sharedPreferences.edit().putBoolean(key, value).apply();
}
public static void saveInt(Context context, String preferenceName, String key, Integer value){
SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
sharedPreferences.edit().putInt(key, value).apply();
}
public static String getString(Context context, String preferenceName, String key){
return getString(context, preferenceName, key, null);
}
public static String getString(Context context, String preferenceName, String key, String _default){
SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
return (sharedPreferences.getString(key, _default));
}
public static Boolean getBoolean(Context context, String preferenceName, String key){
return getBoolean(context, preferenceName, key, false);
}
public static Boolean getBoolean(Context context, String preferenceName, String key, Boolean _default){
SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
return (sharedPreferences.getBoolean(key, _default));
}
public static Integer getInt(Context context, String preferenceName, String key){
return getInt(context, preferenceName, key, -1);
}
public static Integer getInt(Context context, String preferenceName, String key, Integer _default){
SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE);
return (sharedPreferences.getInt(key, _default));
}
}

View File

@ -0,0 +1,49 @@
package fi.vanced.utils;
import android.content.Context;
import android.content.SharedPreferences;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.security.SecureRandom;
public class VancedUtils {
public static SharedPreferences getPreferences(Context context, String preferencesName) {
if (context == null) return null;
return context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE);
}
public static String parseJson(HttpURLConnection connection) throws IOException {
StringBuilder jsonBuilder = new StringBuilder();
InputStream inputStream = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
jsonBuilder.append(line);
}
inputStream.close();
return jsonBuilder.toString();
}
public static int getIdentifier(String name, String defType) {
Context context = YouTubeTikTokRoot_Application.getAppContext();
return context.getResources().getIdentifier(name, defType, context.getPackageName());
}
// https://stackoverflow.com/a/157202
static final String AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
static SecureRandom rnd = new SecureRandom();
public static String randomString(int len){
StringBuilder sb = new StringBuilder(len);
for(int i = 0; i < len; i++)
sb.append(AB.charAt(rnd.nextInt(AB.length())));
return sb.toString();
}
}

View File

@ -64,8 +64,6 @@ public class PlayerController {
return;
}
VideoInformation.currentVideoId = videoId;
Context context = YouTubeTikTokRoot_Application.getAppContext();
if(context == null){
Log.e(TAG, "context is null");

View File

@ -333,4 +333,8 @@
<string name="color_invalid">Invalid hex code</string>
<string name="change">Change</string>
<string name="reset">Reset</string>
<string name="action_copy">Copy</string>
<string name="action_tcopy">TCopy</string>
<string name="action_ads">Ads</string>
</resources>