Merge branch 'ryd-refactor' into ryd-integration

This commit is contained in:
VancedOfficial 2022-01-25 23:29:08 +02:00
commit bf0483b970
34 changed files with 1399 additions and 918 deletions

View File

@ -5,12 +5,19 @@ android {
buildToolsVersion "30.0.2"
defaultConfig {
applicationId "pl.jakubweg"
applicationId "vanced.integrations"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
multiDexEnabled false
Properties properties = new Properties()
if (rootProject.file("local.properties").exists()) {
properties.load(rootProject.file("local.properties").newDataInputStream())
}
buildConfigField "String", "YT_API_KEY", "\"${properties.getProperty("youtubeAPIKey", "")}\""
}
buildTypes {

View File

@ -1,4 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="pl.jakubweg">
package="vanced.integrations">
</manifest>

View File

@ -1,237 +0,0 @@
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;
import fi.vanced.utils.SharedPrefUtils;
public class VideoAds {
public static final String TAG = "VI - VideoAds";
public static final String PREFERENCES_NAME = "channel-whitelist";
public static boolean isEnabled;
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());
isEnabled = SharedPrefUtils.getBoolean(YouTubeTikTokRoot_Application.getAppContext(), "youtube", "vanced_videoadwhitelisting_enabled", false);
}
// 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 (!isEnabled) return;
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 (!isEnabled) return false;
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 {
// Check that the channel doesn't exist already (can happen if for example the channel changes the name)
// If it exists, remove it
Iterator<ChannelModel> iterator = whiteList.iterator();
while(iterator.hasNext())
{
ChannelModel value = iterator.next();
if (value.getChannelId().equals(channelId))
{
if (XGlobals.debug) {
Log.d(TAG, String.format("Tried whitelisting an existing channel again. Old info (%1$s | %2$s) - New info (%3$s | %4$s)",
value.getAuthor(), value.getChannelId(), channelName, channelId));
}
iterator.remove();
break;
}
}
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,139 @@
package fi.vanced.libraries.youtube.dialog;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_KEY_RYD_ENABLED;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_KEY_RYD_HINT_SHOWN;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_NAME;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN;
import static pl.jakubweg.StringRef.str;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.LightingColorFilter;
import android.net.Uri;
import android.os.Build;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import fi.vanced.utils.SharedPrefUtils;
import fi.vanced.utils.VancedUtils;
import pl.jakubweg.SponsorBlockSettings;
public class Dialogs {
// Inject call from YT to this
public static void showDialogsAtStartup(Activity activity) {
rydFirstRun(activity);
sbFirstRun(activity);
}
private static void rydFirstRun(Activity activity) {
Context context = YouTubeTikTokRoot_Application.getAppContext();
boolean enabled = SharedPrefUtils.getBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_ENABLED, false);
boolean hintShown = SharedPrefUtils.getBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_HINT_SHOWN, false);
// If RYD is enabled or hint has been shown, exit
if (enabled || hintShown) {
// If RYD is enabled but hint hasn't been shown, mark it as shown
if (enabled && !hintShown) {
SharedPrefUtils.saveBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_HINT_SHOWN, true);
}
return;
}
AlertDialog.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder = new AlertDialog.Builder(activity, android.R.style.Theme_Material_Dialog_Alert);
} else {
builder = new AlertDialog.Builder(activity);
}
builder.setTitle(str("vanced_ryd"));
builder.setIcon(VancedUtils.getIdentifier("reel_dislike_icon", "drawable"));
builder.setCancelable(false);
builder.setMessage(str("vanced_ryd_firstrun"));
builder.setPositiveButton(str("vanced_enable"),
(dialog, id) -> {
SharedPrefUtils.saveBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_HINT_SHOWN, true);
SharedPrefUtils.saveBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_ENABLED, true);
dialog.dismiss();
});
builder.setNegativeButton(str("vanced_disable"),
(dialog, id) -> {
SharedPrefUtils.saveBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_HINT_SHOWN, true);
SharedPrefUtils.saveBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_ENABLED, false);
dialog.dismiss();
});
builder.setNeutralButton(str("vanced_learnmore"), null);
AlertDialog dialog = builder.create();
dialog.show();
// Set black background
dialog.getWindow().getDecorView().getBackground().setColorFilter(new LightingColorFilter(0xFF000000, VancedUtils.getIdentifier("ytBrandBackgroundSolid", "color")));
// Set learn more action (set here so clicking it doesn't dismiss the dialog)
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(v -> {
Uri uri = Uri.parse("https://www.returnyoutubedislike.com/");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
activity.startActivity(intent);
});
}
private static void sbFirstRun(Activity activity) {
Context context = YouTubeTikTokRoot_Application.getAppContext();
boolean enabled = SharedPrefUtils.getBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED, false);
boolean hintShown = SharedPrefUtils.getBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN, false);
// If SB is enabled or hint has been shown, exit
if (enabled || hintShown) {
// If SB is enabled but hint hasn't been shown, mark it as shown
if (enabled && !hintShown) {
SharedPrefUtils.saveBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN, true);
}
return;
}
AlertDialog.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder = new AlertDialog.Builder(activity, android.R.style.Theme_Material_Dialog_Alert);
} else {
builder = new AlertDialog.Builder(activity);
}
builder.setTitle(str("vanced_sb"));
builder.setIcon(VancedUtils.getIdentifier("ic_sb_logo", "drawable"));
builder.setCancelable(false);
builder.setMessage(str("vanced_sb_firstrun"));
builder.setPositiveButton(str("vanced_enable"),
(dialog, id) -> {
SharedPrefUtils.saveBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN, true);
SharedPrefUtils.saveBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED, true);
dialog.dismiss();
});
builder.setNegativeButton(str("vanced_disable"),
(dialog, id) -> {
SharedPrefUtils.saveBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN, true);
SharedPrefUtils.saveBoolean(context, SponsorBlockSettings.PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED, false);
dialog.dismiss();
});
builder.setNeutralButton(str("vanced_learnmore"), null);
AlertDialog dialog = builder.create();
dialog.show();
// Set black background
dialog.getWindow().getDecorView().getBackground().setColorFilter(new LightingColorFilter(0xFF000000, VancedUtils.getIdentifier("ytBrandBackgroundSolid", "color")));
// Set learn more action (set here so clicking it doesn't dismiss the dialog)
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener(v -> {
Uri uri = Uri.parse("https://sponsor.ajay.app/");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
activity.startActivity(intent);
});
}
}

View File

@ -1,6 +1,8 @@
package fi.vanced.libraries.youtube.ryd;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_KEY_RYD_ENABLED;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_KEY_RYD_HINT_SHOWN;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_NAME;
import static pl.jakubweg.StringRef.str;
@ -44,6 +46,18 @@ public class RYDFragment extends PreferenceFragment {
});
}
// Clear hint
if (debug) {
SwitchPreference preference = new SwitchPreference(context);
preferenceScreen.addPreference(preference);
preference.setKey(PREFERENCES_KEY_RYD_HINT_SHOWN);
preference.setDefaultValue(false);
preference.setChecked(SharedPrefUtils.getBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_RYD_HINT_SHOWN));
preference.setTitle("Hint debug");
preference.setSummary("Debug toggle for clearing the hint shown preference");
preference.setOnPreferenceChangeListener((pref, newValue) -> true);
}
// About category
addAboutCategory(context, preferenceScreen);
}

View File

@ -4,4 +4,5 @@ public class RYDSettings {
public static final String PREFERENCES_NAME = "ryd";
public static final String PREFERENCES_KEY_USERID = "userId";
public static final String PREFERENCES_KEY_RYD_ENABLED = "ryd-enabled";
public static final String PREFERENCES_KEY_RYD_HINT_SHOWN = "ryd_hint_shown";
}

View File

@ -4,19 +4,13 @@ import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_KEY_USERID;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_NAME;
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;
import fi.vanced.libraries.youtube.ryd.requests.RYDRequester;
public class Registration {
private static final String TAG = "VI - RYD - Registration";
@ -50,7 +44,7 @@ public class Registration {
return this.userId;
}
private void saveUserId(String userId) {
public void saveUserId(String userId) {
try {
if (this.context == null) throw new Exception("Unable to save userId because context was null");
@ -64,87 +58,10 @@ public class Registration {
}
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());
}
String userId = randomString(36);
if (debug) {
Log.d(TAG, "Trying to register the following userId: " + userId);
}
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;
return RYDRequester.register(userId, this);
}
}

View File

@ -2,37 +2,28 @@ 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.libraries.youtube.player.VideoInformation.dislikeCount;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_KEY_RYD_ENABLED;
import static fi.vanced.libraries.youtube.ryd.RYDSettings.PREFERENCES_NAME;
import static fi.vanced.utils.VancedUtils.getIdentifier;
import static fi.vanced.utils.VancedUtils.parseJson;
import android.content.Context;
import android.icu.text.CompactDecimalFormat;
import android.os.Build;
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 java.util.Locale;
import static fi.vanced.libraries.youtube.player.VideoInformation.dislikeCount;
import fi.vanced.libraries.youtube.ryd.requests.RYDRequester;
import fi.vanced.utils.SharedPrefUtils;
public class ReturnYouTubeDislikes {
public static final String RYD_API_URL = "https://returnyoutubedislikeapi.com";
public static boolean isEnabled;
private static final String TAG = "VI - RYD";
public static final String TAG = "VI - RYD";
private static View _dislikeView = null;
private static Thread _dislikeFetchThread = null;
private static Thread _votingThread = null;
@ -96,39 +87,7 @@ public class ReturnYouTubeDislikes {
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(formatDislikes(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 = new Thread(() -> RYDRequester.fetchDislikes(videoId));
_dislikeFetchThread.start();
}
@ -211,7 +170,7 @@ public class ReturnYouTubeDislikes {
return originalText;
}
private static void trySetDislikes(String dislikeCount) {
public static void trySetDislikes(String dislikeCount) {
if (!isEnabled) return;
try {
@ -280,8 +239,6 @@ public class ReturnYouTubeDislikes {
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) {
@ -345,7 +302,7 @@ public class ReturnYouTubeDislikes {
}
}
private static String formatDislikes(int dislikes) {
public static String formatDislikes(int dislikes) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && compactNumberFormatter != null) {
final String formatted = compactNumberFormatter.format(dislikes);
if (debug) {

View File

@ -1,13 +1,14 @@
package fi.vanced.libraries.youtube.ryd;
import android.util.Log;
import android.util.Base64;
import android.util.Log;
import java.security.MessageDigest;
public class Utils {
private static final String TAG = "VI - RYD - Utils";
static String solvePuzzle(String challenge, int difficulty) {
public static String solvePuzzle(String challenge, int difficulty) {
byte[] decodedChallenge = Base64.decode(challenge, Base64.NO_WRAP);
byte[] buffer = new byte[20];

View File

@ -1,17 +1,11 @@
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;
import fi.vanced.libraries.youtube.ryd.requests.RYDRequester;
public class Voting {
private static final String TAG = "VI - RYD - Voting";
@ -25,96 +19,10 @@ public class Voting {
}
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());
}
String userId = registration.getUserId();
if (debug) {
Log.d(TAG, "Trying to vote the following video: " + videoId + " with vote " + vote + " and userId: " + userId);
}
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;
return RYDRequester.sendVote(videoId, userId, vote);
}
}

View File

@ -0,0 +1,226 @@
package fi.vanced.libraries.youtube.ryd.requests;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.player.VideoInformation.dislikeCount;
import static fi.vanced.libraries.youtube.ryd.ReturnYouTubeDislikes.TAG;
import static fi.vanced.utils.requests.Requester.parseJson;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import org.json.JSONObject;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import fi.vanced.libraries.youtube.ryd.Registration;
import fi.vanced.libraries.youtube.ryd.ReturnYouTubeDislikes;
import fi.vanced.libraries.youtube.ryd.Utils;
import fi.vanced.utils.requests.Requester;
import fi.vanced.utils.requests.Route;
public class RYDRequester {
private static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/";
private RYDRequester() {}
public static void fetchDislikes(String videoId) {
try {
if (debug) {
Log.d(TAG, "Fetching dislikes for " + videoId);
}
HttpURLConnection connection = getConnectionFromRoute(RYDRoutes.GET_DISLIKES, videoId);
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced");
connection.setConnectTimeout(5 * 1000);
if (connection.getResponseCode() == 200) {
JSONObject json = getJSONObject(connection);
dislikeCount = json.getInt("dislikes");
if (debug) {
Log.d(TAG, "dislikes fetched - " + dislikeCount);
}
// Set the dislikes
new Handler(Looper.getMainLooper()).post(() -> ReturnYouTubeDislikes.trySetDislikes(ReturnYouTubeDislikes.formatDislikes(dislikeCount)));
}
else if (debug) {
Log.d(TAG, "dislikes fetch response was " + connection.getResponseCode());
}
connection.disconnect();
}
catch (Exception ex) {
dislikeCount = null;
Log.e(TAG, "Failed to fetch dislikes", ex);
}
}
public static String register(String userId, Registration registration) {
try {
HttpURLConnection connection = getConnectionFromRoute(RYDRoutes.GET_REGISTRATION, userId);
connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced");
connection.setConnectTimeout(5 * 1000);
if (connection.getResponseCode() == 200) {
JSONObject json = getJSONObject(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, registration);
}
else if (debug) {
Log.d(TAG, "Registration response was " + connection.getResponseCode());
}
connection.disconnect();
}
catch (Exception ex){
Log.e(TAG, "Failed to register userId", ex);
}
return null;
}
private static String confirmRegistration(String userId, String solution, Registration registration) {
try {
if (debug) {
Log.d(TAG, "Trying to confirm registration for the following userId: " + userId + " with solution: " + solution);
}
HttpURLConnection connection = getConnectionFromRoute(RYDRoutes.CONFIRM_REGISTRATION, userId);
applyCommonRequestSettings(connection);
String jsonInputString = "{\"solution\": \"" + solution + "\"}";
try(OutputStream os = connection.getOutputStream()) {
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
if (connection.getResponseCode() == 200) {
String result = parseJson(connection);
if (debug) {
Log.d(TAG, "Registration confirmation result was " + result);
}
if (result.equalsIgnoreCase("true")) {
registration.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 " + connection.getResponseCode());
}
connection.disconnect();
}
catch (Exception ex) {
Log.e(TAG, "Failed to confirm registration", ex);
}
return null;
}
public static boolean sendVote(String videoId, String userId, int vote) {
try {
HttpURLConnection connection = getConnectionFromRoute(RYDRoutes.SEND_VOTE);
applyCommonRequestSettings(connection);
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 = getJSONObject(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(videoId, userId, solution);
}
else if (debug) {
Log.d(TAG, "Vote response was " + connection.getResponseCode());
}
connection.disconnect();
}
catch (Exception ex) {
Log.e(TAG, "Failed to send vote", ex);
}
return false;
}
private static boolean confirmVote(String videoId, String userId, String solution) {
try {
HttpURLConnection connection = getConnectionFromRoute(RYDRoutes.CONFIRM_VOTE);
applyCommonRequestSettings(connection);
String jsonInputString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"solution\": \"" + solution + "\"}";
try(OutputStream os = connection.getOutputStream()) {
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
if (connection.getResponseCode() == 200) {
String result = parseJson(connection);
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 " + connection.getResponseCode());
}
connection.disconnect();
}
catch (Exception ex) {
Log.e(TAG, "Failed to confirm vote", ex);
}
return false;
}
// utils
private static void applyCommonRequestSettings(HttpURLConnection connection) throws Exception {
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);
}
// helpers
private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException {
return Requester.getConnectionFromRoute(RYD_API_URL, route, params);
}
private static JSONObject getJSONObject(HttpURLConnection connection) throws Exception {
return Requester.getJSONObject(connection);
}
}

View File

@ -0,0 +1,16 @@
package fi.vanced.libraries.youtube.ryd.requests;
import static fi.vanced.utils.requests.Route.Method.GET;
import static fi.vanced.utils.requests.Route.Method.POST;
import fi.vanced.utils.requests.Route;
public class RYDRoutes {
public static final Route SEND_VOTE = new Route(POST,"interact/vote");
public static final Route CONFIRM_VOTE = new Route(POST,"interact/confirmVote");
public static final Route GET_DISLIKES = new Route(GET, "votes?videoId={video_id}");
public static final Route GET_REGISTRATION = new Route(GET, "puzzle/registration?userId={user_id}");
public static final Route CONFIRM_REGISTRATION = new Route(POST,"puzzle/registration?userId={user_id}");
private RYDRoutes() {}
}

View File

@ -1,136 +0,0 @@
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.SharedPrefUtils;
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, SharedPrefUtils.getBoolean(context, "youtube", "vanced_videoadwhitelisting_enabled", false));
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,76 @@
package fi.vanced.libraries.youtube.ui;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.player.VideoInformation.currentVideoId;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import fi.vanced.libraries.youtube.player.VideoInformation;
import fi.vanced.libraries.youtube.whitelisting.Whitelist;
import fi.vanced.libraries.youtube.whitelisting.WhitelistType;
import fi.vanced.libraries.youtube.whitelisting.requests.WhitelistRequester;
import fi.vanced.utils.SharedPrefUtils;
import fi.vanced.utils.VancedUtils;
public class AdButton extends SlimButton {
public static final String TAG = "VI - AdButton - Button";
public AdButton(Context context, ViewGroup container) {
super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID,
SharedPrefUtils.getBoolean(context, WhitelistType.ADS.getSharedPreferencesName(), WhitelistType.ADS.getPreferenceEnabledName(), false));
initialize();
}
private void initialize() {
this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_yt_ad_button", "drawable"));
this.button_text.setText(str("action_ads"));
changeEnabled(Whitelist.shouldShowAds());
}
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 {
Whitelist.removeFromWhitelist(WhitelistType.ADS, this.context, VideoInformation.channelName);
changeEnabled(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(() -> {
if (debug) {
Log.d(TAG, "Fetching channelId for " + currentVideoId);
}
WhitelistRequester.addChannelToWhitelist(WhitelistType.ADS, view, buttonIcon, this.context);
}).start();
}
}

View File

@ -0,0 +1,76 @@
package fi.vanced.libraries.youtube.ui;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.player.VideoInformation.currentVideoId;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import fi.vanced.libraries.youtube.player.VideoInformation;
import fi.vanced.libraries.youtube.whitelisting.Whitelist;
import fi.vanced.libraries.youtube.whitelisting.WhitelistType;
import fi.vanced.libraries.youtube.whitelisting.requests.WhitelistRequester;
import fi.vanced.utils.SharedPrefUtils;
import fi.vanced.utils.VancedUtils;
public class SBWhitelistButton extends SlimButton {
public static final String TAG = "VI - SBWhitelistButton";
public SBWhitelistButton(Context context, ViewGroup container) {
super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID,
SharedPrefUtils.getBoolean(context, WhitelistType.SPONSORBLOCK.getSharedPreferencesName(), WhitelistType.SPONSORBLOCK.getPreferenceEnabledName(), false));
initialize();
}
private void initialize() {
this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_yt_sb_button", "drawable"));
this.button_text.setText(str("action_segments"));
changeEnabled(Whitelist.isChannelSBWhitelisted());
}
public void changeEnabled(boolean enabled) {
if (debug) {
Log.d(TAG, "changeEnabled " + enabled);
}
this.button_icon.setEnabled(!enabled); // enabled == true -> strikethrough (no segments), enabled == false -> clear (segments)
}
@Override
public void onClick(View view) {
this.view.setEnabled(false);
if (Whitelist.isChannelSBWhitelisted()) {
removeFromWhitelist();
return;
}
//this.button_icon.setEnabled(!this.button_icon.isEnabled());
addToWhiteList(this.view, this.button_icon);
}
private void removeFromWhitelist() {
try {
Whitelist.removeFromWhitelist(WhitelistType.SPONSORBLOCK, this.context, VideoInformation.channelName);
changeEnabled(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(() -> {
if (debug) {
Log.d(TAG, "Fetching channelId for " + currentVideoId);
}
WhitelistRequester.addChannelToWhitelist(WhitelistType.SPONSORBLOCK, view, buttonIcon, this.context);
}).start();
}
}

View File

@ -10,7 +10,8 @@ import android.view.ViewGroup;
import com.google.android.apps.youtube.app.ui.SlimMetadataScrollableButtonContainerLayout;
import fi.vanced.libraries.youtube.ads.VideoAds;
import fi.vanced.libraries.youtube.whitelisting.Whitelist;
import fi.vanced.libraries.youtube.whitelisting.WhitelistType;
import fi.vanced.utils.SharedPrefUtils;
import fi.vanced.utils.VancedUtils;
@ -19,7 +20,8 @@ public class SlimButtonContainer extends SlimMetadataScrollableButtonContainerLa
private ViewGroup container;
private CopyButton copyButton;
private CopyWithTimestamp copyWithTimestampButton;
public static AdBlock adBlockButton;
public static AdButton adBlockButton;
public static SBWhitelistButton sbWhitelistButton;
private final Context context;
SharedPreferences.OnSharedPreferenceChangeListener listener;
@ -48,8 +50,8 @@ public class SlimButtonContainer extends SlimMetadataScrollableButtonContainerLa
copyButton = new CopyButton(context, this);
copyWithTimestampButton = new CopyWithTimestamp(context, this);
adBlockButton = new AdBlock(context, this);
new SponsorBlock(context, this);
adBlockButton = new AdButton(context, this);
sbWhitelistButton = new SBWhitelistButton(context, this);
new SponsorBlockVoting(context, this);
addSharedPrefsChangeListener();
@ -73,9 +75,20 @@ public class SlimButtonContainer extends SlimMetadataScrollableButtonContainerLa
copyWithTimestampButton.setVisible(ButtonVisibility.isVisibleInContainer(context, "pref_copy_video_url_timestamp_button_list"));
return;
}
if ("vanced_videoadwhitelisting_enabled".equals(key) && adBlockButton != null) {
VideoAds.isEnabled = SharedPrefUtils.getBoolean(context, "youtube", "vanced_videoadwhitelisting_enabled", false);
adBlockButton.setVisible(VideoAds.isEnabled);
WhitelistType whitelistAds = WhitelistType.ADS;
String adsEnabledPreferenceName = whitelistAds.getPreferenceEnabledName();
if (adsEnabledPreferenceName.equals(key) && adBlockButton != null) {
boolean enabled = SharedPrefUtils.getBoolean(context, whitelistAds.getSharedPreferencesName(), adsEnabledPreferenceName, false);
Whitelist.setEnabled(whitelistAds, enabled);
adBlockButton.setVisible(enabled);
return;
}
WhitelistType whitelistSB = WhitelistType.SPONSORBLOCK;
String sbEnabledPreferenceName = whitelistSB.getPreferenceEnabledName();
if (sbEnabledPreferenceName.equals(key) && sbWhitelistButton != null) {
boolean enabled = SharedPrefUtils.getBoolean(context, whitelistSB.getSharedPreferencesName(), sbEnabledPreferenceName, false);
Whitelist.setEnabled(whitelistSB, enabled);
sbWhitelistButton.setVisible(enabled);
return;
}
}
@ -84,7 +97,9 @@ public class SlimButtonContainer extends SlimMetadataScrollableButtonContainerLa
}
};
context.getSharedPreferences("youtube", Context.MODE_PRIVATE)
context.getSharedPreferences(WhitelistType.ADS.getSharedPreferencesName(), Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(listener);
context.getSharedPreferences(WhitelistType.SPONSORBLOCK.getSharedPreferencesName(), Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(listener);
}
}

View File

@ -1,31 +0,0 @@
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,185 @@
package fi.vanced.libraries.youtube.whitelisting;
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.libraries.youtube.ui.SlimButtonContainer.sbWhitelistButton;
import static fi.vanced.utils.VancedUtils.getPreferences;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import android.widget.Toast;
import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import fi.vanced.libraries.youtube.player.ChannelModel;
import fi.vanced.libraries.youtube.player.VideoInformation;
import fi.vanced.utils.ObjectSerializer;
import fi.vanced.utils.SharedPrefUtils;
import fi.vanced.utils.VancedUtils;
public class Whitelist {
private static final String TAG = "VI - Whitelisting";
private static final Map<WhitelistType, ArrayList<ChannelModel>> whitelistMap = parseWhitelist(YouTubeTikTokRoot_Application.getAppContext());
private static final Map<WhitelistType, Boolean> enabledMap = parseEnabledMap(YouTubeTikTokRoot_Application.getAppContext());
private Whitelist() {}
// injected calls
public static boolean shouldShowAds() {
return isWhitelisted(WhitelistType.ADS);
}
public static void setChannelName(String channelName) {
if (debug) {
Log.d(TAG, "channel name set to " + channelName);
}
VideoInformation.channelName = channelName;
if (enabledMap.get(WhitelistType.ADS) && adBlockButton != null) {
adBlockButton.changeEnabled(shouldShowAds());
}
if (enabledMap.get(WhitelistType.SPONSORBLOCK) && sbWhitelistButton != null) {
sbWhitelistButton.changeEnabled(isChannelSBWhitelisted());
}
}
// the rest
public static boolean isChannelSBWhitelisted() {
return isWhitelisted(WhitelistType.SPONSORBLOCK);
}
private static Map<WhitelistType, ArrayList<ChannelModel>> parseWhitelist(Context context) {
if (context == null) {
return Collections.emptyMap();
}
WhitelistType[] whitelistTypes = WhitelistType.values();
Map<WhitelistType, ArrayList<ChannelModel>> whitelistMap = new EnumMap<>(WhitelistType.class);
for (WhitelistType whitelistType : whitelistTypes) {
SharedPreferences preferences = VancedUtils.getPreferences(context, whitelistType.getPreferencesName());
String serializedChannels = preferences.getString("channels", null);
if (serializedChannels == null) {
if (debug) {
Log.d(TAG, String.format("channels string was null for %s whitelisting", whitelistType));
}
whitelistMap.put(whitelistType, new ArrayList<>());
continue;
}
try {
ArrayList<ChannelModel> deserializedChannels = (ArrayList<ChannelModel>) ObjectSerializer.deserialize(serializedChannels);
if (debug) {
Log.d(TAG, serializedChannels);
for (ChannelModel channel : deserializedChannels) {
Log.d(TAG, String.format("Whitelisted channel %s (%s) for type %s", channel.getAuthor(), channel.getChannelId(), whitelistType));
}
}
whitelistMap.put(whitelistType, deserializedChannels);
}
catch (Exception ex) {
ex.printStackTrace();
}
}
return whitelistMap;
}
private static Map<WhitelistType, Boolean> parseEnabledMap(Context context) {
Map<WhitelistType, Boolean> enabledMap = new EnumMap<>(WhitelistType.class);
for (WhitelistType whitelistType : WhitelistType.values()) {
enabledMap.put(whitelistType, SharedPrefUtils.getBoolean(context, whitelistType.getSharedPreferencesName(), whitelistType.getPreferenceEnabledName()));
}
return enabledMap;
}
private static boolean isWhitelisted(WhitelistType whitelistType) {
boolean isEnabled = enabledMap.get(whitelistType);
if (!isEnabled) {
return false;
}
if (channelName == null || channelName.trim().isEmpty()) {
if (debug) {
Log.d(TAG, String.format("Can't check whitelist status for %s because channel name was missing", whitelistType));
}
return false;
}
List<ChannelModel> whitelistedChannels = whitelistMap.get(whitelistType);
for (ChannelModel channel : whitelistedChannels) {
if (channel.getAuthor().equals(channelName)) {
if (debug) {
Log.d(TAG, String.format("Whitelist for channel %s for type %s", channelName, whitelistType));
}
return true;
}
}
return false;
}
public static boolean addToWhitelist(WhitelistType whitelistType, Context context, ChannelModel channel) {
ArrayList<ChannelModel> whitelisted = whitelistMap.get(whitelistType);
for (ChannelModel whitelistedChannel : whitelisted) {
String channelId = channel.getChannelId();
if (whitelistedChannel.getChannelId().equals(channelId)) {
if (debug) {
Log.d(TAG, String.format("Tried whitelisting an existing channel again. Old info (%1$s | %2$s) - New info (%3$s | %4$s)",
whitelistedChannel.getAuthor(), channelId, channelName, channelId));
}
return true;
}
}
whitelisted.add(channel);
return updateWhitelist(whitelistType, whitelisted, context);
}
public static void removeFromWhitelist(WhitelistType whitelistType, Context context, String channelName) {
ArrayList<ChannelModel> channels = whitelistMap.get(whitelistType);
Iterator<ChannelModel> iterator = channels.iterator();
while (iterator.hasNext()) {
ChannelModel channel = iterator.next();
if (channel.getAuthor().equals(channelName)) {
iterator.remove();
break;
}
}
boolean success = updateWhitelist(whitelistType, channels, context);
String friendlyName = whitelistType.getFriendlyName();
if (success) {
Toast.makeText(context, str("vanced_whitelisting_removed", channelName, friendlyName), Toast.LENGTH_SHORT).show();
}
else {
Toast.makeText(context, str("vanced_whitelisting_remove_failed", channelName, friendlyName), Toast.LENGTH_SHORT).show();
}
}
private static boolean updateWhitelist(WhitelistType whitelistType, ArrayList<ChannelModel> channels, Context context) {
if (context == null) {
return false;
}
SharedPreferences preferences = getPreferences(context, whitelistType.getPreferencesName());
SharedPreferences.Editor editor = preferences.edit();
try {
editor.putString("channels", ObjectSerializer.serialize(channels));
editor.apply();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
public static void setEnabled(WhitelistType whitelistType, boolean enabled) {
enabledMap.put(whitelistType, enabled);
}
}

View File

@ -0,0 +1,38 @@
package fi.vanced.libraries.youtube.whitelisting;
import static pl.jakubweg.StringRef.str;
import pl.jakubweg.SponsorBlockSettings;
public enum WhitelistType {
ADS("youtube", "vanced_whitelist_ads_enabled"),
SPONSORBLOCK(SponsorBlockSettings.PREFERENCES_NAME, "vanced_whitelist_sb_enabled");
private final String friendlyName;
private final String preferencesName;
private final String sharedPreferencesName;
private final String preferenceEnabledName;
WhitelistType(String sharedPreferencesName, String preferenceEnabledName) {
this.friendlyName = str("vanced_whitelisting_" + name().toLowerCase());
this.sharedPreferencesName = sharedPreferencesName;
this.preferencesName = "whitelist_" + name();
this.preferenceEnabledName = preferenceEnabledName;
}
public String getFriendlyName() {
return friendlyName;
}
public String getSharedPreferencesName() {
return sharedPreferencesName;
}
public String getPreferencesName() {
return preferencesName;
}
public String getPreferenceEnabledName() {
return preferenceEnabledName;
}
}

View File

@ -0,0 +1,100 @@
package fi.vanced.libraries.youtube.whitelisting.requests;
import static fi.razerman.youtube.XGlobals.debug;
import static fi.vanced.libraries.youtube.player.VideoInformation.currentVideoId;
import static fi.vanced.libraries.youtube.ui.AdButton.TAG;
import static fi.vanced.utils.VancedUtils.runOnMainThread;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;
import org.json.JSONObject;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import fi.vanced.libraries.youtube.player.ChannelModel;
import fi.vanced.libraries.youtube.whitelisting.Whitelist;
import fi.vanced.libraries.youtube.whitelisting.WhitelistType;
import fi.vanced.utils.VancedUtils;
import fi.vanced.utils.requests.Requester;
import fi.vanced.utils.requests.Route;
import vanced.integrations.BuildConfig;
public class WhitelistRequester {
private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1/";
private WhitelistRequester() {}
public static void addChannelToWhitelist(WhitelistType whitelistType, View view, ImageView buttonIcon, Context context) {
try {
HttpURLConnection connection = getConnectionFromRoute(WhitelistRoutes.GET_CHANNEL_DETAILS, BuildConfig.YT_API_KEY);
connection.setRequestProperty("Content-Type", "application/json; utf-8");
connection.setRequestProperty("Accept", "application/json");
connection.setDoOutput(true);
connection.setConnectTimeout(2 * 1000);
String versionName = VancedUtils.getVersionName(context);
String jsonInputString = "{\"context\": {\"client\": { \"clientName\": \"Android\", \"clientVersion\": \"" + versionName + "\" } }, \"videoId\": \"" + currentVideoId + "\"}";
try(OutputStream os = connection.getOutputStream()) {
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
JSONObject json = getJSONObject(connection);
JSONObject videoInfo = json.getJSONObject("videoDetails");
ChannelModel channelModel = new ChannelModel(videoInfo.getString("author"), videoInfo.getString("channelId"));
String author = channelModel.getAuthor();
if (debug) {
Log.d(TAG, "channelId " + channelModel.getChannelId() + " fetched for author " + author);
}
boolean success = Whitelist.addToWhitelist(whitelistType, context, channelModel);
String whitelistTypeName = whitelistType.getFriendlyName();
runOnMainThread(() -> {
if (success) {
buttonIcon.setEnabled(whitelistType != WhitelistType.SPONSORBLOCK);
Toast.makeText(context, str("vanced_whitelisting_added", author, whitelistTypeName), Toast.LENGTH_SHORT).show();
}
else {
buttonIcon.setEnabled(whitelistType == WhitelistType.SPONSORBLOCK);
Toast.makeText(context, str("vanced_whitelisting_add_failed", author, whitelistTypeName), Toast.LENGTH_SHORT).show();
}
view.setEnabled(true);
});
}
else {
if (debug) {
Log.d(TAG, "player fetch response was " + responseCode);
}
runOnMainThread(() -> {
Toast.makeText(context, str("vanced_whitelisting_fetch_failed", responseCode), Toast.LENGTH_SHORT).show();
buttonIcon.setEnabled(true);
view.setEnabled(true);
});
}
connection.disconnect();
}
catch (Exception ex) {
Log.e(TAG, "Failed to fetch channelId", ex);
runOnMainThread(() -> view.setEnabled(true));
}
}
// helpers
private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException {
return Requester.getConnectionFromRoute(YT_API_URL, route, params);
}
private static JSONObject getJSONObject(HttpURLConnection connection) throws Exception {
return Requester.getJSONObject(connection);
}
}

View File

@ -0,0 +1,11 @@
package fi.vanced.libraries.youtube.whitelisting.requests;
import static fi.vanced.utils.requests.Route.Method.POST;
import fi.vanced.utils.requests.Route;
public class WhitelistRoutes {
public static final Route GET_CHANNEL_DETAILS = new Route(POST, "player?key={api_key}");
private WhitelistRoutes() {}
}

View File

@ -2,35 +2,24 @@ package fi.vanced.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Looper;
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 {
private 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());
@ -46,4 +35,29 @@ public class VancedUtils {
sb.append(AB.charAt(rnd.nextInt(AB.length())));
return sb.toString();
}
public static int countMatches(CharSequence seq, char c) {
int count = 0;
for (int i = 0; i < seq.length(); i++) {
if (seq.charAt(i) == c)
count++;
}
return count;
}
public static String getVersionName(Context context) {
try {
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
String version = pInfo.versionName;
return (version);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return ("17.03.35");
}
public static void runOnMainThread(Runnable runnable) {
new Handler(Looper.getMainLooper()).post(runnable);
}
}

View File

@ -0,0 +1,51 @@
package fi.vanced.utils.requests;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class Requester {
private Requester() {}
public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException {
String url = apiUrl + route.compile(params).getCompiledRoute();
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod(route.getMethod().name());
return connection;
}
public static String parseJson(HttpURLConnection connection) throws IOException {
return parseJson(connection.getInputStream());
}
public static String parseJson(InputStream inputStream) throws IOException {
StringBuilder jsonBuilder = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
jsonBuilder.append(line).append("\n");
}
inputStream.close();
return jsonBuilder.toString();
}
public static JSONObject getJSONObject(HttpURLConnection connection) throws Exception {
return new JSONObject(parseJsonAndDisconnect(connection));
}
public static JSONArray getJSONArray(HttpURLConnection connection) throws Exception {
return new JSONArray(parseJsonAndDisconnect(connection));
}
private static String parseJsonAndDisconnect(HttpURLConnection connection) throws IOException {
String json = parseJson(connection);
connection.disconnect();
return json;
}
}

View File

@ -0,0 +1,59 @@
package fi.vanced.utils.requests;
import fi.vanced.utils.VancedUtils;
public class Route {
private final String route;
private final Route.Method method;
private final int paramCount;
public Route(Route.Method method, String route) {
this.method = method;
this.route = route;
this.paramCount = VancedUtils.countMatches(route, '{');
if (paramCount != VancedUtils.countMatches(route, '}'))
throw new IllegalArgumentException("Not enough parameters");
}
public Route.Method getMethod() {
return method;
}
public Route.CompiledRoute compile(String... params) {
if (params.length != paramCount)
throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " +
"Expected: " + paramCount + ", provided: " + params.length);
StringBuilder compiledRoute = new StringBuilder(route);
for (int i = 0; i < paramCount; i++) {
int paramStart = compiledRoute.indexOf("{");
int paramEnd = compiledRoute.indexOf("}");
compiledRoute.replace(paramStart, paramEnd + 1, params[i]);
}
return new Route.CompiledRoute(this, compiledRoute.toString());
}
public static class CompiledRoute {
private final Route baseRoute;
private final String compiledRoute;
private CompiledRoute(Route baseRoute, String compiledRoute) {
this.baseRoute = baseRoute;
this.compiledRoute = compiledRoute;
}
public String getCompiledRoute() {
return compiledRoute;
}
public Route.Method getMethod() {
return baseRoute.method;
}
}
public enum Method {
GET,
POST
}
}

View File

@ -1,5 +1,10 @@
package pl.jakubweg;
import static pl.jakubweg.SponsorBlockSettings.skippedSegments;
import static pl.jakubweg.SponsorBlockSettings.skippedTime;
import static pl.jakubweg.SponsorBlockUtils.timeWithoutSegments;
import static pl.jakubweg.SponsorBlockUtils.videoHasSegments;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
@ -22,11 +27,9 @@ import java.util.Timer;
import java.util.TimerTask;
import fi.vanced.libraries.youtube.player.VideoInformation;
import fi.vanced.libraries.youtube.whitelisting.Whitelist;
import pl.jakubweg.objects.SponsorSegment;
import pl.jakubweg.requests.Requester;
import static pl.jakubweg.SponsorBlockSettings.skippedSegments;
import static pl.jakubweg.SponsorBlockSettings.skippedTime;
import pl.jakubweg.requests.SBRequester;
@SuppressLint({"LongLogTag"})
public class PlayerController {
@ -123,7 +126,11 @@ public class PlayerController {
}
public static void executeDownloadSegments(String videoId) {
SponsorSegment[] segments = Requester.getSegments(videoId);
videoHasSegments = false;
timeWithoutSegments = "";
if (Whitelist.isChannelSBWhitelisted())
return;
SponsorSegment[] segments = SBRequester.getSegments(videoId);
Arrays.sort(segments);
if (VERBOSE)
@ -281,7 +288,7 @@ public class PlayerController {
segment.category != SponsorBlockSettings.SegmentInfo.UNSUBMITTED &&
millis - segment.start < 2000) {
// Only skips from the start should count as a view
Requester.sendViewCountRequest(segment);
SBRequester.sendViewCountRequest(segment);
}
}).start();
}

View File

@ -1,17 +1,20 @@
package pl.jakubweg;
import static pl.jakubweg.SponsorBlockSettings.DefaultBehaviour;
import static fi.razerman.youtube.XGlobals.debug;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_COUNT_SKIPS;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_MIN_DURATION;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_NEW_SEGMENT_ENABLED;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SHOW_TIME_WITHOUT_SEGMENTS;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SHOW_TOAST_WHEN_SKIP;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_UUID;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_VOTING_ENABLED;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_NAME;
import static pl.jakubweg.SponsorBlockSettings.adjustNewSegmentMillis;
import static pl.jakubweg.SponsorBlockSettings.countSkips;
import static pl.jakubweg.SponsorBlockSettings.minDuration;
import static pl.jakubweg.SponsorBlockSettings.setSeenGuidelines;
import static pl.jakubweg.SponsorBlockSettings.showTimeWithoutSegments;
import static pl.jakubweg.SponsorBlockSettings.showToastWhenSkippedAutomatically;
@ -26,7 +29,6 @@ import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
@ -38,7 +40,10 @@ import android.widget.Toast;
import java.text.DecimalFormat;
import java.util.ArrayList;
import pl.jakubweg.requests.Requester;
import fi.vanced.libraries.youtube.whitelisting.WhitelistType;
import fi.vanced.utils.SharedPrefUtils;
import pl.jakubweg.objects.EditTextListPreference;
import pl.jakubweg.requests.SBRequester;
@SuppressWarnings({"unused", "deprecation"}) // injected
public class SponsorBlockPreferenceFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
@ -75,6 +80,18 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
});
}
// Clear hint
if (debug) {
SwitchPreference preference = new SwitchPreference(context);
preferenceScreen.addPreference(preference);
preference.setKey(PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN);
preference.setDefaultValue(false);
preference.setChecked(SharedPrefUtils.getBoolean(context, PREFERENCES_NAME, PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN));
preference.setTitle("Hint debug");
preference.setSummary("Debug toggle for clearing the hint shown preference");
preference.setOnPreferenceChangeListener((pref, newValue) -> true);
}
{
SwitchPreference preference = new SwitchPreference(context);
preferenceScreen.addPreference(preference);
@ -143,7 +160,6 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
preferencesToDisableWhenSBDisabled.add(category);
category.setTitle(str("diff_segments"));
String defaultValue = DefaultBehaviour.key;
SponsorBlockSettings.SegmentBehaviour[] segmentBehaviours = SponsorBlockSettings.SegmentBehaviour.values();
String[] entries = new String[segmentBehaviours.length];
String[] entryValues = new String[segmentBehaviours.length];
@ -156,33 +172,21 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
SponsorBlockSettings.SegmentInfo[] categories = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted();
for (SponsorBlockSettings.SegmentInfo segmentInfo : categories) {
ListPreference preference = new ListPreference(context);
EditTextListPreference preference = new EditTextListPreference(context);
preference.setTitle(segmentInfo.getTitleWithDot());
preference.setSummary(segmentInfo.description.toString());
preference.setKey(segmentInfo.key);
preference.setDefaultValue(defaultValue);
preference.setDefaultValue(segmentInfo.behaviour.key);
preference.setEntries(entries);
preference.setEntryValues(entryValues);
category.addPreference(preference);
}
Preference colorPreference = new Preference(context);
Preference colorPreference = new Preference(context); // TODO remove this after the next major update
screen.addPreference(colorPreference);
colorPreference.setTitle(str("color_change"));
colorPreference.setOnPreferenceClickListener(preference1 -> {
CharSequence[] items = new CharSequence[categories.length];
for (int i = 0; i < items.length; i++) {
items[i] = categories[i].getTitleWithDot();
}
new AlertDialog.Builder(context)
.setTitle(str("color_choose_category"))
.setItems(items, SponsorBlockUtils.categoryColorChangeClickListener)
.show();
return true;
});
colorPreference.setSummary(str("color_change_sum"));
preferencesToDisableWhenSBDisabled.add(colorPreference);
}
@ -197,7 +201,7 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
category.addPreference(preference);
preference.setTitle(str("stats_loading"));
Requester.retrieveUserStats(category, preference);
SBRequester.retrieveUserStats(category, preference);
}
}
@ -278,9 +282,18 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
screen.addPreference(preference);
}
{
Preference preference = new SwitchPreference(context);
preference.setTitle(str("general_whitelisting"));
preference.setSummary(str("general_whitelisting_sum"));
preference.setKey(WhitelistType.SPONSORBLOCK.getPreferenceEnabledName());
preferencesToDisableWhenSBDisabled.add(preference);
screen.addPreference(preference);
}
{
EditTextPreference preference = new EditTextPreference(context);
preference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED);
preference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
preference.setTitle(str("general_adjusting"));
preference.setSummary(str("general_adjusting_sum"));
preference.setKey(PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP);
@ -289,6 +302,17 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment implement
preferencesToDisableWhenSBDisabled.add(preference);
}
{
EditTextPreference preference = new EditTextPreference(context);
preference.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
preference.setTitle(str("general_min_duration"));
preference.setSummary(str("general_min_duration_sum"));
preference.setKey(PREFERENCES_KEY_MIN_DURATION);
preference.setDefaultValue(String.valueOf(minDuration));
screen.addPreference(preference);
preferencesToDisableWhenSBDisabled.add(preference);
}
{
Preference preference = new EditTextPreference(context);
preference.setTitle(str("general_uuid"));

View File

@ -22,7 +22,9 @@ public class SponsorBlockSettings {
public static final String PREFERENCES_KEY_COUNT_SKIPS = "count-skips";
public static final String PREFERENCES_KEY_UUID = "uuid";
public static final String PREFERENCES_KEY_ADJUST_NEW_SEGMENT_STEP = "new-segment-step-accuracy";
public static final String PREFERENCES_KEY_MIN_DURATION = "sb-min-duration";
public static final String PREFERENCES_KEY_SPONSOR_BLOCK_ENABLED = "sb-enabled";
public static final String PREFERENCES_KEY_SPONSOR_BLOCK_HINT_SHOWN = "sb_hint_shown";
public static final String PREFERENCES_KEY_SEEN_GUIDELINES = "sb-seen-gl";
public static final String PREFERENCES_KEY_NEW_SEGMENT_ENABLED = "sb-new-segment-enabled";
public static final String PREFERENCES_KEY_VOTING_ENABLED = "sb-voting-enabled";
@ -41,6 +43,7 @@ public class SponsorBlockSettings {
public static boolean countSkips = true;
public static boolean showTimeWithoutSegments = true;
public static int adjustNewSegmentMillis = 150;
public static float minDuration = 0f;
public static String uuid = "<invalid>";
public static String sponsorBlockUrlCategories = "[]";
public static int skippedSegments;
@ -100,9 +103,7 @@ public class SponsorBlockSettings {
SegmentBehaviour behaviour = null;
String value = preferences.getString(segment.key, null);
if (value == null)
behaviour = DefaultBehaviour;
else {
if (value != null) {
for (SegmentBehaviour possibleBehaviour : possibleBehaviours) {
if (possibleBehaviour.key.equals(value)) {
behaviour = possibleBehaviour;
@ -110,10 +111,13 @@ public class SponsorBlockSettings {
}
}
}
if (behaviour == null)
behaviour = DefaultBehaviour;
if (behaviour != null) {
segment.behaviour = behaviour;
}
else {
behaviour = segment.behaviour;
}
segment.behaviour = behaviour;
if (behaviour.showOnTimeBar && segment != SegmentInfo.UNSUBMITTED)
enabledCategories.add(segment.key);
}
@ -132,6 +136,10 @@ public class SponsorBlockSettings {
if (tmp1 != null)
adjustNewSegmentMillis = Integer.parseInt(tmp1);
String minTmp = preferences.getString(PREFERENCES_KEY_MIN_DURATION, null);
if (minTmp != null)
minDuration = Float.parseFloat(minTmp);
countSkips = preferences.getBoolean(PREFERENCES_KEY_COUNT_SKIPS, countSkips);
showTimeWithoutSegments = preferences.getBoolean(PREFERENCES_KEY_SHOW_TIME_WITHOUT_SEGMENTS, showTimeWithoutSegments);
@ -179,13 +187,14 @@ public class SponsorBlockSettings {
}
public enum SegmentInfo {
SPONSOR("sponsor", sf("segments_sponsor"), sf("skipped_sponsor"), sf("segments_sponsor_sum"), null, 0xFF00d400),
INTRO("intro", sf("segments_intermission"), sf("skipped_intermission"), sf("segments_intermission_sum"), null, 0xFF00ffff),
OUTRO("outro", sf("segments_endcards"), sf("skipped_endcard"), sf("segments_endcards_sum"), null, 0xFF0202ed),
INTERACTION("interaction", sf("segments_subscribe"), sf("skipped_subscribe"), sf("segments_subscribe_sum"), null, 0xFFcc00ff),
SELF_PROMO("selfpromo", sf("segments_selfpromo"), sf("skipped_selfpromo"), sf("segments_selfpromo_sum"), null, 0xFFffff00),
MUSIC_OFFTOPIC("music_offtopic", sf("segments_nomusic"), sf("skipped_nomusic"), sf("segments_nomusic_sum"), null, 0xFFff9900),
PREVIEW("preview", sf("segments_preview"), sf("skipped_preview"), sf("segments_preview_sum"), null, 0xFF008fd6),
SPONSOR("sponsor", sf("segments_sponsor"), sf("skipped_sponsor"), sf("segments_sponsor_sum"), DefaultBehaviour, 0xFF00d400),
INTRO("intro", sf("segments_intermission"), sf("skipped_intermission"), sf("segments_intermission_sum"), DefaultBehaviour, 0xFF00ffff),
OUTRO("outro", sf("segments_endcards"), sf("skipped_endcard"), sf("segments_endcards_sum"), DefaultBehaviour, 0xFF0202ed),
INTERACTION("interaction", sf("segments_subscribe"), sf("skipped_subscribe"), sf("segments_subscribe_sum"), DefaultBehaviour, 0xFFcc00ff),
SELF_PROMO("selfpromo", sf("segments_selfpromo"), sf("skipped_selfpromo"), sf("segments_selfpromo_sum"), DefaultBehaviour, 0xFFffff00),
MUSIC_OFFTOPIC("music_offtopic", sf("segments_nomusic"), sf("skipped_nomusic"), sf("segments_nomusic_sum"), DefaultBehaviour, 0xFFff9900),
PREVIEW("preview", sf("segments_preview"), sf("skipped_preview"), sf("segments_preview_sum"), DefaultBehaviour, 0xFF008fd6),
FILLER("filler", sf("segments_filler"), sf("skipped_filler"), sf("segments_filler_sum"), SegmentBehaviour.IGNORE, 0xFF7300FF),
UNSUBMITTED("unsubmitted", StringRef.empty, sf("skipped_unsubmitted"), StringRef.empty, SegmentBehaviour.SKIP_AUTOMATICALLY, 0xFFFFFFFF);
private static final SegmentInfo[] mValuesWithoutUnsubmitted = new SegmentInfo[]{
@ -195,7 +204,8 @@ public class SponsorBlockSettings {
INTERACTION,
SELF_PROMO,
MUSIC_OFFTOPIC,
PREVIEW
PREVIEW,
FILLER
};
private static final Map<String, SegmentInfo> mValuesMap = new HashMap<>(values().length);

View File

@ -12,19 +12,21 @@ import static pl.jakubweg.SponsorBlockPreferenceFragment.FORMATTER;
import static pl.jakubweg.SponsorBlockPreferenceFragment.SAVED_TEMPLATE;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_CATEGORY_COLOR_SUFFIX;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_COUNT_SKIPS;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_MIN_DURATION;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SHOW_TIME_WITHOUT_SEGMENTS;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_SHOW_TOAST_WHEN_SKIP;
import static pl.jakubweg.SponsorBlockSettings.PREFERENCES_KEY_UUID;
import static pl.jakubweg.SponsorBlockSettings.countSkips;
import static pl.jakubweg.SponsorBlockSettings.getPreferences;
import static pl.jakubweg.SponsorBlockSettings.isSponsorBlockEnabled;
import static pl.jakubweg.SponsorBlockSettings.minDuration;
import static pl.jakubweg.SponsorBlockSettings.showTimeWithoutSegments;
import static pl.jakubweg.SponsorBlockSettings.showToastWhenSkippedAutomatically;
import static pl.jakubweg.SponsorBlockSettings.skippedSegments;
import static pl.jakubweg.SponsorBlockSettings.skippedTime;
import static pl.jakubweg.SponsorBlockSettings.uuid;
import static pl.jakubweg.StringRef.str;
import static pl.jakubweg.requests.Requester.voteForSegment;
import static pl.jakubweg.requests.SBRequester.voteForSegment;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
@ -32,13 +34,11 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.net.Uri;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.text.Html;
import android.text.InputType;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
@ -61,7 +61,7 @@ import java.util.TimeZone;
import pl.jakubweg.objects.SponsorSegment;
import pl.jakubweg.objects.UserStats;
import pl.jakubweg.requests.Requester;
import pl.jakubweg.requests.SBRequester;
@SuppressWarnings({"LongLogTag"})
public abstract class SponsorBlockUtils {
@ -223,10 +223,10 @@ public abstract class SponsorBlockUtils {
appContext = new WeakReference<>(context.getApplicationContext());
switch (voteOptions[which1]) {
case UPVOTE:
voteForSegment(segment, VoteOption.UPVOTE, appContext.get(), toastRunnable);
voteForSegment(segment, VoteOption.UPVOTE, appContext.get());
break;
case DOWNVOTE:
voteForSegment(segment, VoteOption.DOWNVOTE, appContext.get(), toastRunnable);
voteForSegment(segment, VoteOption.DOWNVOTE, appContext.get());
break;
case CATEGORY_CHANGE:
onNewCategorySelect(segment, context);
@ -235,40 +235,6 @@ public abstract class SponsorBlockUtils {
})
.show();
};
public static final DialogInterface.OnClickListener categoryColorChangeClickListener = (dialog, which) -> {
SponsorBlockSettings.SegmentInfo segmentInfo = SponsorBlockSettings.SegmentInfo.valuesWithoutUnsubmitted()[which];
String key = segmentInfo.key + PREFERENCES_KEY_CATEGORY_COLOR_SUFFIX;
Context context = ((AlertDialog) dialog).getContext();
EditText editText = new EditText(context);
editText.setInputType(InputType.TYPE_CLASS_TEXT);
editText.setText(formatColorString(segmentInfo.color));
Context applicationContext = context.getApplicationContext();
SharedPreferences preferences = SponsorBlockSettings.getPreferences(context);
new AlertDialog.Builder(context)
.setView(editText)
.setPositiveButton(str("change"), (dialog1, which1) -> {
try {
int color = Color.parseColor(editText.getText().toString());
segmentInfo.setColor(color);
Toast.makeText(applicationContext, str("color_changed"), Toast.LENGTH_SHORT).show();
preferences.edit().putString(key, formatColorString(color)).apply();
}
catch (Exception ex) {
Toast.makeText(applicationContext, str("color_invalid"), Toast.LENGTH_SHORT).show();
}
})
.setNeutralButton(str("reset"), (dialog1, which1) -> {
int defaultColor = segmentInfo.defaultColor;
segmentInfo.setColor(defaultColor);
Toast.makeText(applicationContext, str("color_reset"), Toast.LENGTH_SHORT).show();
preferences.edit().putString(key, formatColorString(defaultColor)).apply();
})
.setNegativeButton(android.R.string.cancel, null)
.show();
};
private static final Runnable submitRunnable = () -> {
messageToToast = null;
final String uuid = SponsorBlockSettings.uuid;
@ -281,7 +247,7 @@ public abstract class SponsorBlockUtils {
Log.e(TAG, "Unable to submit times, invalid parameters");
return;
}
Requester.submitSegments(videoId, uuid, ((float) start) / 1000f, ((float) end) / 1000f, segmentType.key, toastRunnable);
SBRequester.submitSegments(videoId, uuid, ((float) start) / 1000f, ((float) end) / 1000f, segmentType.key, toastRunnable);
newSponsorSegmentEndMillis = newSponsorSegmentStartMillis = -1;
} catch (Exception e) {
Log.e(TAG, "Unable to submit segment", e);
@ -401,7 +367,7 @@ public abstract class SponsorBlockUtils {
new AlertDialog.Builder(context)
.setTitle(str("new_segment_choose_category"))
.setItems(titles, (dialog, which) -> voteForSegment(segment, VoteOption.CATEGORY_CHANGE, appContext.get(), toastRunnable, values[which].key))
.setItems(titles, (dialog, which) -> voteForSegment(segment, VoteOption.CATEGORY_CHANGE, appContext.get(), values[which].key))
.show();
}
@ -485,15 +451,6 @@ public abstract class SponsorBlockUtils {
}
}
public static int countMatches(CharSequence seq, char c) {
int count = 0;
for (int i = 0; i < seq.length(); i++) {
if (seq.charAt(i) == c)
count++;
}
return count;
}
public static String formatColorString(int color) {
return String.format("#%06X", color);
}
@ -514,7 +471,7 @@ public abstract class SponsorBlockUtils {
preference.setText(userName);
preference.setOnPreferenceChangeListener((preference1, newUsername) -> {
appContext = new WeakReference<>(context.getApplicationContext());
Requester.setUsername((String) newUsername, toastRunnable);
SBRequester.setUsername((String) newUsername, preference, toastRunnable);
return false;
});
}
@ -597,6 +554,7 @@ public abstract class SponsorBlockUtils {
editor.putBoolean(PREFERENCES_KEY_SHOW_TOAST_WHEN_SKIP, !settingsJson.getBoolean("dontShowNotice"));
editor.putBoolean(PREFERENCES_KEY_SHOW_TIME_WITHOUT_SEGMENTS, settingsJson.getBoolean("showTimeWithSkips"));
editor.putBoolean(PREFERENCES_KEY_COUNT_SKIPS, settingsJson.getBoolean("trackViewCount"));
editor.putString(PREFERENCES_KEY_MIN_DURATION, settingsJson.getString("minDuration"));
editor.putString(PREFERENCES_KEY_UUID, settingsJson.getString("userID"));
editor.apply();
@ -633,6 +591,7 @@ public abstract class SponsorBlockUtils {
json.put("dontShowNotice", !showToastWhenSkippedAutomatically);
json.put("barTypes", barTypesObject);
json.put("showTimeWithSkips", showTimeWithoutSegments);
json.put("minDuration", minDuration);
json.put("trackViewCount", countSkips);
json.put("categorySelections", categorySelectionsArray);
json.put("userID", uuid);

View File

@ -0,0 +1,97 @@
package pl.jakubweg.objects;
import static pl.jakubweg.SponsorBlockUtils.formatColorString;
import static pl.jakubweg.StringRef.str;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.preference.ListPreference;
import android.text.InputType;
import android.util.AttributeSet;
import android.widget.EditText;
import android.widget.Toast;
import pl.jakubweg.SponsorBlockSettings;
@SuppressWarnings("deprecation")
public class EditTextListPreference extends ListPreference {
private EditText mEditText;
private int mClickedDialogEntryIndex;
public EditTextListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public EditTextListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public EditTextListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public EditTextListPreference(Context context) {
super(context);
}
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
SponsorBlockSettings.SegmentInfo category = getCategoryBySelf();
mEditText = new EditText(builder.getContext());
mEditText.setInputType(InputType.TYPE_CLASS_TEXT);
mEditText.setText(formatColorString(category.color));
builder.setView(mEditText);
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
EditTextListPreference.this.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
});
builder.setNeutralButton(str("reset"), (dialog, which) -> {
//EditTextListPreference.this.onClick(dialog, DialogInterface.BUTTON_NEUTRAL);
int defaultColor = category.defaultColor;
category.setColor(defaultColor);
Toast.makeText(getContext().getApplicationContext(), str("color_reset"), Toast.LENGTH_SHORT).show();
getSharedPreferences().edit().putString(getColorPreferenceKey(), formatColorString(defaultColor)).apply();
});
builder.setNegativeButton(android.R.string.cancel, null);
mClickedDialogEntryIndex = findIndexOfValue(getValue());
builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) {
String value = getEntryValues()[mClickedDialogEntryIndex].toString();
if (callChangeListener(value)) {
setValue(value);
}
String colorString = mEditText.getText().toString();
SponsorBlockSettings.SegmentInfo category = getCategoryBySelf();
if (colorString.equals(formatColorString(category.color))) {
return;
}
Context applicationContext = getContext().getApplicationContext();
try {
int color = Color.parseColor(colorString);
category.setColor(color);
Toast.makeText(applicationContext, str("color_changed"), Toast.LENGTH_SHORT).show();
getSharedPreferences().edit().putString(getColorPreferenceKey(), formatColorString(color)).apply();
}
catch (Exception ex) {
Toast.makeText(applicationContext, str("color_invalid"), Toast.LENGTH_SHORT).show();
}
}
}
private SponsorBlockSettings.SegmentInfo getCategoryBySelf() {
return SponsorBlockSettings.SegmentInfo.byCategoryKey(getKey());
}
private String getColorPreferenceKey() {
return getKey() + SponsorBlockSettings.PREFERENCES_KEY_CATEGORY_COLOR_SUFFIX;
}
}

View File

@ -1,70 +0,0 @@
package pl.jakubweg.requests;
import static pl.jakubweg.requests.Route.Method.GET;
import static pl.jakubweg.requests.Route.Method.POST;
import pl.jakubweg.SponsorBlockUtils;
public class Route {
public static final Route GET_SEGMENTS = new Route(GET, "skipSegments?videoID={video_id}&categories={categories}");
public static final Route VIEWED_SEGMENT = new Route(POST, "viewedVideoSponsorTime?UUID={segment_id}");
public static final Route GET_USER_STATS = new Route(GET, "userInfo?userID={user_id}&values=[\"userName\", \"minutesSaved\", \"segmentCount\", \"viewCount\"]");
public static final Route CHANGE_USERNAME = new Route(POST, "setUsername?userID={user_id}&username={username}");
public static final Route SUBMIT_SEGMENTS = new Route(POST, "skipSegments?videoID={video_id}&userID={user_id}&startTime={start_time}&endTime={end_time}&category={category}");
public static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "voteOnSponsorTime?UUID={segment_id}&userID={user_id}&type={type}");
public static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "voteOnSponsorTime?UUID={segment_id}&userID={user_id}&category={category}");
private final String route;
private final Method method;
private final int paramCount;
private Route(Method method, String route) {
this.method = method;
this.route = route;
this.paramCount = SponsorBlockUtils.countMatches(route, '{');
if (paramCount != SponsorBlockUtils.countMatches(route, '}'))
throw new IllegalArgumentException("Not enough parameters");
}
public Method getMethod() {
return method;
}
public CompiledRoute compile(String... params) {
if (params.length != paramCount)
throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " +
"Expected: " + paramCount + ", provided: " + params.length);
StringBuilder compiledRoute = new StringBuilder(route);
for (int i = 0; i < paramCount; i++) {
int paramStart = compiledRoute.indexOf("{");
int paramEnd = compiledRoute.indexOf("}");
compiledRoute.replace(paramStart, paramEnd + 1, params[i]);
}
return new CompiledRoute(this, compiledRoute.toString());
}
public static class CompiledRoute {
private final Route baseRoute;
private final String compiledRoute;
private CompiledRoute(Route baseRoute, String compiledRoute) {
this.baseRoute = baseRoute;
this.compiledRoute = compiledRoute;
}
public String getCompiledRoute() {
return compiledRoute;
}
public Method getMethod() {
return baseRoute.method;
}
}
public enum Method {
GET,
POST
}
}

View File

@ -1,12 +1,13 @@
package pl.jakubweg.requests;
import static android.text.Html.fromHtml;
import static fi.vanced.utils.VancedUtils.runOnMainThread;
import static pl.jakubweg.SponsorBlockUtils.timeWithoutSegments;
import static pl.jakubweg.SponsorBlockUtils.videoHasSegments;
import static pl.jakubweg.StringRef.str;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.preference.EditTextPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.widget.Toast;
@ -14,44 +15,45 @@ import android.widget.Toast;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import fi.vanced.utils.requests.Requester;
import fi.vanced.utils.requests.Route;
import pl.jakubweg.SponsorBlockSettings;
import pl.jakubweg.SponsorBlockUtils;
import pl.jakubweg.SponsorBlockUtils.VoteOption;
import pl.jakubweg.objects.SponsorSegment;
import pl.jakubweg.objects.UserStats;
public class Requester {
public class SBRequester {
private static final String SPONSORBLOCK_API_URL = "https://sponsor.ajay.app/api/";
private static final String TIME_TEMPLATE = "%.3f";
private Requester() {}
private SBRequester() {}
public static synchronized SponsorSegment[] getSegments(String videoId) {
List<SponsorSegment> segments = new ArrayList<>();
try {
HttpURLConnection connection = getConnectionFromRoute(Route.GET_SEGMENTS, videoId, SponsorBlockSettings.sponsorBlockUrlCategories);
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.GET_SEGMENTS, videoId, SponsorBlockSettings.sponsorBlockUrlCategories);
int responseCode = connection.getResponseCode();
videoHasSegments = false;
timeWithoutSegments = "";
if (responseCode == 200) {
JSONArray responseArray = new JSONArray(parseJson(connection));
JSONArray responseArray = Requester.getJSONArray(connection);
int length = responseArray.length();
for (int i = 0; i < length; i++) {
JSONObject obj = ((JSONObject) responseArray.get(i));
JSONObject obj = (JSONObject) responseArray.get(i);
JSONArray segment = obj.getJSONArray("segment");
long start = (long) (segment.getDouble(0) * 1000);
long end = (long) (segment.getDouble(1) * 1000);
long minDuration = (long) (SponsorBlockSettings.minDuration * 1000);
if ((end - start) < minDuration)
continue;
String category = obj.getString("category");
String uuid = obj.getString("UUID");
@ -61,8 +63,10 @@ public class Requester {
segments.add(sponsorSegment);
}
}
videoHasSegments = true;
timeWithoutSegments = SponsorBlockUtils.getTimeWithoutSegments(segments.toArray(new SponsorSegment[0]));
if (!segments.isEmpty()) {
videoHasSegments = true;
timeWithoutSegments = SponsorBlockUtils.getTimeWithoutSegments(segments.toArray(new SponsorSegment[0]));
}
}
connection.disconnect();
}
@ -76,7 +80,7 @@ public class Requester {
try {
String start = String.format(Locale.US, TIME_TEMPLATE, startTime);
String end = String.format(Locale.US, TIME_TEMPLATE, endTime);
HttpURLConnection connection = getConnectionFromRoute(Route.SUBMIT_SEGMENTS, videoId, uuid, start, end, category);
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, videoId, uuid, start, end, category);
int responseCode = connection.getResponseCode();
switch (responseCode) {
@ -87,7 +91,7 @@ public class Requester {
SponsorBlockUtils.messageToToast = str("submit_failed_duplicate");
break;
case 403:
SponsorBlockUtils.messageToToast = str("submit_failed_forbidden");
SponsorBlockUtils.messageToToast = str("submit_failed_forbidden", Requester.parseJson(connection.getErrorStream()));
break;
case 429:
SponsorBlockUtils.messageToToast = str("submit_failed_rate_limit");
@ -96,7 +100,7 @@ public class Requester {
SponsorBlockUtils.messageToToast = str("submit_failed_unknown_error", responseCode, connection.getResponseMessage());
break;
}
new Handler(Looper.getMainLooper()).post(toastRunnable);
runOnMainThread(toastRunnable);
connection.disconnect();
}
catch (Exception ex) {
@ -106,7 +110,7 @@ public class Requester {
public static void sendViewCountRequest(SponsorSegment segment) {
try {
HttpURLConnection connection = getConnectionFromRoute(Route.VIEWED_SEGMENT, segment.UUID);
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.VIEWED_SEGMENT, segment.UUID);
connection.disconnect();
}
catch (Exception ex) {
@ -114,36 +118,38 @@ public class Requester {
}
}
public static void voteForSegment(SponsorSegment segment, VoteOption voteOption, Context context, Runnable toastRunnable, String... args) {
try {
String segmentUuid = segment.UUID;
String uuid = SponsorBlockSettings.uuid;
String vote = Integer.toString(voteOption == VoteOption.UPVOTE ? 1 : 0);
public static void voteForSegment(SponsorSegment segment, VoteOption voteOption, Context context, String... args) {
new Thread(() -> {
try {
String segmentUuid = segment.UUID;
String uuid = SponsorBlockSettings.uuid;
String vote = Integer.toString(voteOption == VoteOption.UPVOTE ? 1 : 0);
Toast.makeText(context, str("vote_started"), Toast.LENGTH_SHORT).show();
runOnMainThread(() -> Toast.makeText(context, str("vote_started"), Toast.LENGTH_SHORT).show());
HttpURLConnection connection = voteOption == VoteOption.CATEGORY_CHANGE
? getConnectionFromRoute(Route.VOTE_ON_SEGMENT_CATEGORY, segmentUuid, uuid, args[0])
: getConnectionFromRoute(Route.VOTE_ON_SEGMENT_QUALITY, segmentUuid, uuid, vote);
int responseCode = connection.getResponseCode();
HttpURLConnection connection = voteOption == VoteOption.CATEGORY_CHANGE
? getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_CATEGORY, segmentUuid, uuid, args[0])
: getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_QUALITY, segmentUuid, uuid, vote);
int responseCode = connection.getResponseCode();
switch (responseCode) {
case 200:
SponsorBlockUtils.messageToToast = str("vote_succeeded");
break;
case 403:
SponsorBlockUtils.messageToToast = str("vote_failed_forbidden");
break;
default:
SponsorBlockUtils.messageToToast = str("vote_failed_unknown_error", responseCode, connection.getResponseMessage());
break;
switch (responseCode) {
case 200:
SponsorBlockUtils.messageToToast = str("vote_succeeded");
break;
case 403:
SponsorBlockUtils.messageToToast = str("vote_failed_forbidden", Requester.parseJson(connection.getErrorStream()));
break;
default:
SponsorBlockUtils.messageToToast = str("vote_failed_unknown_error", responseCode, connection.getResponseMessage());
break;
}
runOnMainThread(() -> Toast.makeText(context, SponsorBlockUtils.messageToToast, Toast.LENGTH_LONG).show());
connection.disconnect();
}
new Handler(Looper.getMainLooper()).post(toastRunnable);
connection.disconnect();
}
catch (Exception ex) {
ex.printStackTrace();
}
catch (Exception ex) {
ex.printStackTrace();
}
}).start();
}
public static void retrieveUserStats(PreferenceCategory category, Preference loadingPreference) {
@ -154,9 +160,7 @@ public class Requester {
new Thread(() -> {
try {
HttpURLConnection connection = getConnectionFromRoute(Route.GET_USER_STATS, SponsorBlockSettings.uuid);
JSONObject json = new JSONObject(parseJson(connection));
connection.disconnect();
JSONObject json = getJSONObject(SBRoutes.GET_USER_STATS, SponsorBlockSettings.uuid);
UserStats stats = new UserStats(json.getString("userName"), json.getDouble("minutesSaved"), json.getInt("segmentCount"),
json.getInt("viewCount"));
SponsorBlockUtils.addUserStats(category, loadingPreference, stats);
@ -167,41 +171,38 @@ public class Requester {
}).start();
}
public static void setUsername(String username, Runnable toastRunnable) {
try {
HttpURLConnection connection = getConnectionFromRoute(Route.CHANGE_USERNAME, SponsorBlockSettings.uuid, username);
int responseCode = connection.getResponseCode();
public static void setUsername(String username, EditTextPreference preference, Runnable toastRunnable) {
new Thread(() -> {
try {
HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SponsorBlockSettings.uuid, username);
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
SponsorBlockUtils.messageToToast = str("stats_username_changed");
if (responseCode == 200) {
SponsorBlockUtils.messageToToast = str("stats_username_changed");
runOnMainThread(() -> {
preference.setTitle(fromHtml(str("stats_username", username)));
preference.setText(username);
});
}
else {
SponsorBlockUtils.messageToToast = str("stats_username_change_unknown_error", responseCode, connection.getResponseMessage());
}
runOnMainThread(toastRunnable);
connection.disconnect();
}
else {
SponsorBlockUtils.messageToToast = str("stats_username_change_unknown_error", responseCode, connection.getResponseMessage());
catch (Exception ex) {
ex.printStackTrace();
}
new Handler(Looper.getMainLooper()).post(toastRunnable);
connection.disconnect();
}
catch (Exception ex) {
ex.printStackTrace();
}
}).start();
}
// helpers
private static HttpURLConnection getConnectionFromRoute(Route route, String... params) throws IOException {
String url = SPONSORBLOCK_API_URL + route.compile(params).getCompiledRoute();
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod(route.getMethod().name());
return connection;
return Requester.getConnectionFromRoute(SPONSORBLOCK_API_URL, route, params);
}
private 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();
private static JSONObject getJSONObject(Route route, String... params) throws Exception {
return Requester.getJSONObject(getConnectionFromRoute(route, params));
}
}

View File

@ -0,0 +1,18 @@
package pl.jakubweg.requests;
import static fi.vanced.utils.requests.Route.Method.GET;
import static fi.vanced.utils.requests.Route.Method.POST;
import fi.vanced.utils.requests.Route;
public class SBRoutes {
public static final Route GET_SEGMENTS = new Route(GET, "skipSegments?videoID={video_id}&categories={categories}");
public static final Route VIEWED_SEGMENT = new Route(POST, "viewedVideoSponsorTime?UUID={segment_id}");
public static final Route GET_USER_STATS = new Route(GET, "userInfo?userID={user_id}&values=[\"userName\", \"minutesSaved\", \"segmentCount\", \"viewCount\"]");
public static final Route CHANGE_USERNAME = new Route(POST, "setUsername?userID={user_id}&username={username}");
public static final Route SUBMIT_SEGMENTS = new Route(POST, "skipSegments?videoID={video_id}&userID={user_id}&startTime={start_time}&endTime={end_time}&category={category}");
public static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "voteOnSponsorTime?UUID={segment_id}&userID={user_id}&type={type}");
public static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "voteOnSponsorTime?UUID={segment_id}&userID={user_id}&category={category}");
private SBRoutes() {}
}

View File

@ -151,6 +151,8 @@
<string name="general_skipcount_sum">This lets SponsorBlock leaderboard system know how much time people have saved. The extension sends a message to the server each time you skip a segment</string>
<string name="general_adjusting">Adjusting new segment step</string>
<string name="general_adjusting_sum">This is the number of milliseconds you can move when you use the time adjustment buttons while adding new segment</string>
<string name="general_min_duration">Minimum segment duration</string>
<string name="general_min_duration_sum">Segments shorter than the set value (in seconds) will not be skipped or show in the player</string>
<string name="general_uuid">Your unique user id</string>
<string name="general_uuid_sum">This should be kept private. This is like a password and should not be shared with anyone. If someone has this, they can impersonate you</string>
<string name="settings_ie">Import/Export settings</string>
@ -173,6 +175,8 @@
<string name="segments_selfpromo_sum">Similar to "sponsor" except for unpaid or self promotion. This includes sections about merchandise, donations, or information about who they collaborated with</string>
<string name="segments_nomusic">Music: Non-Music Section</string>
<string name="segments_nomusic_sum">Only for use in music videos. This includes introductions or outros in music videos</string>
<string name="segments_filler">Filler/Tangent</string>
<string name="segments_filler_sum">Tangential scenes added only for filler or humor that are not required to understand the main content of the video. This should not include segments providing context or background details</string>
<string name="skipped_segment">Skipped a sponsor segment</string>
<string name="skipped_sponsor">Skipped sponsor</string>
<string name="skipped_intermission">Skipped intro</string>
@ -181,6 +185,7 @@
<string name="skipped_selfpromo">Skipped self promotion</string>
<string name="skipped_nomusic">Skipped silence</string>
<string name="skipped_preview">Skipped preview</string>
<string name="skipped_filler">Skipped filler</string>
<string name="skipped_unsubmitted">Skipped unsubmitted segment</string>
<string name="skip_automatically">Skip automatically</string>
<string name="skip_showbutton">Show a skip button</string>
@ -193,14 +198,14 @@
<string name="submit_failed_unknown_error" formatted="false">Unable to submit segments: Status: %d %s</string>
<string name="submit_failed_rate_limit">Can\'t submit the segment.\nRate Limited (Too many from the same user or IP)</string>
<string name="submit_failed_forbidden">Can\'t submit the segment.\nRejected by auto moderator</string>
<string name="submit_failed_forbidden" formatted="false">Can\'t submit the segment.\n\n%s</string>
<string name="submit_failed_duplicate">Can\'t submit the segment.\nAlready exists</string>
<string name="submit_succeeded">Segment submitted successfully</string>
<string name="submit_started">Submitting segment…</string>
<string name="vote_failed_unknown_error" formatted="false">Unable to vote for segment: Status: %d %s</string>
<string name="vote_failed_rate_limit">Can\'t vote for segment.\nRate Limited (Too many from the same user or IP)</string>
<string name="vote_failed_forbidden">Can\'t vote for segment.\nA moderator has decided that this segment is correct</string>
<string name="vote_failed_forbidden" formatted="false">Can\'t vote for segment.\n\n%s</string>
<string name="vote_succeeded">Voted successfully</string>
<string name="vote_started">Voting for segment…</string>
<string name="vote_upvote">Upvote</string>
@ -311,14 +316,16 @@
<string name="general_time_without_sb">Show time without segments</string>
<string name="general_time_without_sb_sum">This time appears in brackets next to the current time. This shows the total video duration minus any segments.</string>
<string name="general_whitelisting">Channel whitelisting</string>
<string name="general_whitelisting_sum">Use the Segments button under the player to whitelist a channel</string>
<string name="segments_preview">Preview/Recap</string>
<string name="segments_preview_sum">Quick recap of previous episodes, or a preview of what\'s coming up later in the current video. Meant for edited together clips, not for spoken summaries.</string>
<string name="stats">Stats</string>
<string name="stats_loading">Loading..</string>
<string name="stats_sb_disabled">SponsorBlock is disabled</string>
<string name="stats_username">Your username: &lt;b&gt;%s&lt;/b&gt;</string>
<string name="stats_username" formatted="false">Your username: &lt;b&gt;%s&lt;/b&gt;</string>
<string name="stats_username_change">Click to change your username</string>
<string name="stats_username_change_unknown_error">Unable to change username: Status: %d %s</string>
<string name="stats_username_change_unknown_error" formatted="false">Unable to change username: Status: %d %s</string>
<string name="stats_username_changed">Username successfully changed</string>
<string name="stats_submissions">Submissions: &lt;b&gt;%s&lt;/b&gt;</string>
<string name="stats_saved">You\'ve saved people from &lt;b&gt;%s&lt;/b&gt; segments.</string>
@ -326,7 +333,8 @@
<string name="stats_self_saved">You\'ve skipped &lt;b&gt;%s&lt;/b&gt; segments.</string>
<string name="stats_self_saved_sum">That\'s &lt;b&gt;%s&lt;/b&gt;.</string>
<string name="minutes">minutes</string>
<string name="color_change">Change colors</string>
<string name="color_change">Are you looking for changing colors?</string>
<string name="color_change_sum">You can now change a category\'s color by clicking on it above.</string>
<string name="color_choose_category">Choose the category</string>
<string name="color_changed">Color changed</string>
<string name="color_reset">Color reset</string>
@ -334,22 +342,42 @@
<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_copy">Copy link</string>
<string name="action_tcopy">Timestamp</string>
<string name="action_ads">Ads</string>
<string name="action_segments">Segments</string>
<string name="vanced_video_ad_settings_title">Video ad settings</string>
<string name="vanced_videoadwhitelisting_title">Video ad whitelisting</string>
<string name="vanced_videoadwhitelisting_summary_off">Video ad whitelisting is turned off</string>
<string name="vanced_videoadwhitelisting_summary_on">Video ad whitelisting is turned on. Use the ad button under the player to whitelist a channel</string>
<string name="vanced_videoadwhitelisting_summary_on">Video ad whitelisting is turned on. Use the Ads button under the player to whitelist a channel</string>
<string name="vanced_whitelisting_ads">Ads</string>
<string name="vanced_whitelisting_sponsorblock">SponsorBlock</string>
<string name="vanced_whitelisting_added" formatted="false">Channel %s was added to the %s whitelist</string>
<string name="vanced_whitelisting_removed" formatted="false">Channel %s was removed from the %s whitelist</string>
<string name="vanced_whitelisting_add_failed" formatted="false">Failed to add channel %s to the %s whitelist</string>
<string name="vanced_whitelisting_remove_failed" formatted="false">Failed to remove channel %s from the %s whitelist</string>
<string name="vanced_whitelisting_fetch_failed" formatted="false">Failed to retrieve channel details, received code %d</string>
<string name="vanced_button_location_entry_none">Hidden</string>
<string name="vanced_button_location_entry_player">In player</string>
<string name="vanced_button_location_entry_buttoncontainer">Under player</string>
<string name="vanced_button_location_entry_both">Both</string>
<string name="vanced_ryd_settings_title">RYD settings</string>
<string name="vanced_ryd_settings_title">Return YouTube Dislike settings</string>
<string name="vanced_ryd_settings_summary">Uses the RYD API</string>
<string name="vanced_ryd_title">Enable RYD</string>
<string name="vanced_ryd_summary">Switch this on to see the dislike counts again</string>
<string name="vanced_ryd_attribution_title">Return YouTube Dislike Integration</string>
<string name="vanced_ryd_attribution_summary">This integration uses the RYD API from https://returnyoutubedislike.com. Tap to learn more</string>
<string name="xfile_xfenster_tablet_title">Tablet style</string>
<string name="xfile_xfenster_tablet_summary_on">Tablet style is turned on. For example suggested videos are only partially working</string>
<string name="xfile_xfenster_tablet_summary_off">Tablet style is turned off</string>
<string name="vanced_ryd">Return YouTube Dislike</string>
<string name="vanced_ryd_firstrun">Want to enable Return YouTube Dislikes to see dislikes again? Your likes/dislikes will be sent to RYD API (anonymously) after enabling RYD integration. You can enable/disable this in the settings at any time.</string>
<string name="vanced_sb">SponsorBlock</string>
<string name="vanced_sb_firstrun">Are you aware of the SponsorBlock integration in Vanced? With it you can skip sponsored segments in the videos. You can enable/disable this in the settings at any time.</string>
<string name="vanced_learnmore">Learn more</string>
<string name="vanced_disable">Disable</string>
<string name="vanced_enable">Enable</string>
</resources>

View File

@ -5,7 +5,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.0'
classpath 'com.android.tools.build:gradle:7.0.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files