Skip to content
This repository has been archived by the owner on Dec 11, 2024. It is now read-only.

Commit

Permalink
feat(YouTube & YouTube Music): Add Return YouTube Username patch
Browse files Browse the repository at this point in the history
  • Loading branch information
anddea committed Oct 15, 2024
1 parent f530872 commit 835c4c7
Show file tree
Hide file tree
Showing 14 changed files with 532 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ public class Settings extends BaseSettings {
public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("revanced_ryd_estimated_like", FALSE, true);
public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("revanced_ryd_toast_on_connection_error", FALSE);

// PreferenceScreen: Return YouTube Username
public static final BooleanSetting RETURN_YOUTUBE_USERNAME_ABOUT = new BooleanSetting("revanced_return_youtube_username_youtube_data_api_v3_about", FALSE, false);


// PreferenceScreen: SponsorBlock
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
Expand Down Expand Up @@ -230,6 +233,8 @@ public class Settings extends BaseSettings {
SB_API_URL.key,
SETTINGS_IMPORT_EXPORT.key,
SPOOF_APP_VERSION_TARGET.key,
RETURN_YOUTUBE_USERNAME_ABOUT.key,
RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY.key,
OPEN_DEFAULT_APP_SETTINGS,
OPTIONAL_SPONSOR_BLOCK_SETTINGS_PREFIX
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
import static app.revanced.integrations.music.settings.Settings.HIDE_ACCOUNT_MENU_FILTER_STRINGS;
import static app.revanced.integrations.music.settings.Settings.OPEN_DEFAULT_APP_SETTINGS;
import static app.revanced.integrations.music.settings.Settings.OPTIONAL_SPONSOR_BLOCK_SETTINGS_PREFIX;
import static app.revanced.integrations.music.settings.Settings.RETURN_YOUTUBE_USERNAME_ABOUT;
import static app.revanced.integrations.music.settings.Settings.SB_API_URL;
import static app.revanced.integrations.music.settings.Settings.SETTINGS_IMPORT_EXPORT;
import static app.revanced.integrations.music.settings.Settings.SPOOF_APP_VERSION_TARGET;
import static app.revanced.integrations.music.utils.ExtendedUtils.getDialogBuilder;
import static app.revanced.integrations.music.utils.ExtendedUtils.getLayoutParams;
import static app.revanced.integrations.music.utils.RestartUtils.showRestartDialog;
import static app.revanced.integrations.shared.settings.BaseSettings.RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY;
import static app.revanced.integrations.shared.settings.Setting.getSettingFromPath;
import static app.revanced.integrations.shared.utils.ResourceUtils.getStringArray;
import static app.revanced.integrations.shared.utils.StringRef.str;
Expand Down Expand Up @@ -53,6 +55,7 @@
import app.revanced.integrations.shared.settings.BooleanSetting;
import app.revanced.integrations.shared.settings.Setting;
import app.revanced.integrations.shared.settings.StringSetting;
import app.revanced.integrations.shared.settings.preference.YouTubeDataAPIDialogBuilder;
import app.revanced.integrations.shared.utils.Logger;
import app.revanced.integrations.shared.utils.Utils;

Expand Down Expand Up @@ -132,7 +135,8 @@ public void onCreate(Bundle savedInstanceState) {
} else if (settings.equals(BYPASS_IMAGE_REGION_RESTRICTIONS_DOMAIN)
|| settings.equals(CUSTOM_FILTER_STRINGS)
|| settings.equals(CUSTOM_PLAYBACK_SPEEDS)
|| settings.equals(HIDE_ACCOUNT_MENU_FILTER_STRINGS)) {
|| settings.equals(HIDE_ACCOUNT_MENU_FILTER_STRINGS)
|| settings.equals(RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY)) {
ResettableEditTextPreference.showDialog(mActivity, stringSetting);
} else if (settings.equals(EXTERNAL_DOWNLOADER_PACKAGE_NAME)) {
ExternalDownloaderPreference.showDialog(mActivity);
Expand All @@ -146,6 +150,8 @@ public void onCreate(Bundle savedInstanceState) {
} else if (settings instanceof BooleanSetting) {
if (settings.equals(SETTINGS_IMPORT_EXPORT)) {
importExportListDialogBuilder();
} else if (settings.equals(RETURN_YOUTUBE_USERNAME_ABOUT)) {
YouTubeDataAPIDialogBuilder.showDialog(mActivity);
} else {
Logger.printDebug(() -> "Failed to find the right value: " + dataString);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package app.revanced.integrations.shared.patches;

import android.text.SpannableString;
import android.text.Spanned;

import androidx.annotation.NonNull;

import app.revanced.integrations.shared.returnyoutubeusername.requests.ChannelRequest;
import app.revanced.integrations.shared.settings.BaseSettings;
import app.revanced.integrations.shared.utils.Logger;

@SuppressWarnings("unused")
public class ReturnYouTubeUsernamePatch {
private static final boolean RETURN_YOUTUBE_USERNAME_ENABLED = BaseSettings.RETURN_YOUTUBE_USERNAME_ENABLED.get();
private static final String YOUTUBE_API_KEY = BaseSettings.RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY.get();

private static final String AUTHOR_BADGE_PATH = "|author_badge.eml|";
private static volatile String lastFetchedHandle = "";

/**
* Injection point.
*
* @param original The original string before the SpannableString is built.
*/
public static CharSequence preFetchLithoText(@NonNull Object conversionContext,
@NonNull CharSequence original) {
onLithoTextLoaded(conversionContext, original, true);
return original;
}

/**
* Injection point.
*
* @param original The original string after the SpannableString is built.
*/
@NonNull
public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
@NonNull CharSequence original) {
return onLithoTextLoaded(conversionContext, original, false);
}

@NonNull
private static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
@NonNull CharSequence original,
boolean fetchNeeded) {
try {
if (!RETURN_YOUTUBE_USERNAME_ENABLED) {
return original;
}
if (YOUTUBE_API_KEY.isEmpty()) {
Logger.printDebug(() -> "API key is empty");
return original;
}
// In comments, the path to YouTube Handle(@youtube) always includes [AUTHOR_BADGE_PATH].
if (!conversionContext.toString().contains(AUTHOR_BADGE_PATH)) {
return original;
}
String handle = original.toString();
if (fetchNeeded && !handle.equals(lastFetchedHandle)) {
lastFetchedHandle = handle;
// Get the original username using YouTube Data API v3.
ChannelRequest.fetchRequestIfNeeded(handle, YOUTUBE_API_KEY);
return original;
}
// If the username is not in the cache, put it in the cache.
ChannelRequest channelRequest = ChannelRequest.getRequestForHandle(handle);
if (channelRequest == null) {
Logger.printDebug(() -> "ChannelRequest is null, handle:" + handle);
return original;
}
final String userName = channelRequest.getStream();
if (userName == null) {
Logger.printDebug(() -> "ChannelRequest Stream is null, handle:" + handle);
return original;
}
final CharSequence copiedSpannableString = copySpannableString(original, userName);
Logger.printDebug(() -> "Replaced: '" + original + "' with: '" + copiedSpannableString + "'");
return copiedSpannableString;
} catch (Exception ex) {
Logger.printException(() -> "onLithoTextLoaded failure", ex);
}
return original;
}

private static CharSequence copySpannableString(CharSequence original, String userName) {
if (original instanceof Spanned spanned) {
SpannableString newString = new SpannableString(userName);
Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
for (Object span : spans) {
int flags = spanned.getSpanFlags(span);
newString.setSpan(span, 0, newString.length(), flags);
}
return newString;
}
return original;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package app.revanced.integrations.shared.returnyoutubeusername.requests;

import static app.revanced.integrations.shared.returnyoutubeusername.requests.ChannelRoutes.GET_CHANNEL_DETAILS;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

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

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import app.revanced.integrations.shared.requests.Requester;
import app.revanced.integrations.shared.utils.Logger;
import app.revanced.integrations.shared.utils.Utils;

public class ChannelRequest {
/**
* TCP connection and HTTP read timeout.
*/
private static final int HTTP_TIMEOUT_MILLISECONDS = 3 * 1000;

/**
* Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS}
*/
private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 6 * 1000;

@GuardedBy("itself")
private static final Map<String, ChannelRequest> cache = Collections.synchronizedMap(
new LinkedHashMap<>(200) {
private static final int CACHE_LIMIT = 100;

@Override
protected boolean removeEldestEntry(Entry eldest) {
return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit.
}
});

public static void fetchRequestIfNeeded(@NonNull String handle, @NonNull String apiKey) {
if (!cache.containsKey(handle)) {
cache.put(handle, new ChannelRequest(handle, apiKey));
}
}

@Nullable
public static ChannelRequest getRequestForHandle(@NonNull String handle) {
return cache.get(handle);
}

private static void handleConnectionError(String toastMessage, @Nullable Exception ex) {
Logger.printInfo(() -> toastMessage, ex);
}

@Nullable
private static JSONObject send(String handle, String apiKey) {
Objects.requireNonNull(handle);
Objects.requireNonNull(apiKey);

final long startTime = System.currentTimeMillis();
Logger.printDebug(() -> "Fetching channel handle for: " + handle);

try {
HttpURLConnection connection = ChannelRoutes.getChannelConnectionFromRoute(GET_CHANNEL_DETAILS, handle, apiKey);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Connection", "keep-alive"); // keep-alive is on by default with http 1.1, but specify anyways
connection.setRequestProperty("Pragma", "no-cache");
connection.setRequestProperty("Cache-Control", "no-cache");
connection.setUseCaches(false);
connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS);
connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS);

final int responseCode = connection.getResponseCode();
if (responseCode == 200) return Requester.parseJSONObject(connection);

handleConnectionError("API not available with response code: "
+ responseCode + " message: " + connection.getResponseMessage(),
null);
} catch (SocketTimeoutException ex) {
handleConnectionError("Connection timeout", ex);
} catch (IOException ex) {
handleConnectionError("Network error", ex);
} catch (Exception ex) {
Logger.printException(() -> "send failed", ex);
} finally {
Logger.printDebug(() -> "handle: " + handle + " took: " + (System.currentTimeMillis() - startTime) + "ms");
}

return null;
}

private static String fetch(@NonNull String handle, @NonNull String apiKey) {
final JSONObject channelJsonObject = send(handle, apiKey);
if (channelJsonObject != null) {
try {
return channelJsonObject
.getJSONArray("items")
.getJSONObject(0)
.getJSONObject("snippet")
.getString("title");
} catch (JSONException e) {
Logger.printDebug(() -> "Fetch failed while processing response data for response: " + channelJsonObject);
}
}
return null;
}

private final String handle;
private final Future<String> future;

private ChannelRequest(String handle, String apiKey) {
this.handle = handle;
this.future = Utils.submitOnBackgroundThread(() -> fetch(handle, apiKey));
}

@Nullable
public String getStream() {
try {
return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS);
} catch (TimeoutException ex) {
Logger.printInfo(() -> "getStream timed out", ex);
} catch (InterruptedException ex) {
Logger.printException(() -> "getStream interrupted", ex);
Thread.currentThread().interrupt(); // Restore interrupt status flag.
} catch (ExecutionException ex) {
Logger.printException(() -> "getStream failure", ex);
}

return null;
}

@NonNull
@Override
public String toString() {
return "ChannelRequest{" + "handle='" + handle + '\'' + '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package app.revanced.integrations.shared.returnyoutubeusername.requests;

import static app.revanced.integrations.shared.requests.Route.Method.GET;

import java.io.IOException;
import java.net.HttpURLConnection;

import app.revanced.integrations.shared.requests.Requester;
import app.revanced.integrations.shared.requests.Route;

public class ChannelRoutes {
public static final String YOUTUBEI_V3_GAPIS_URL = "https://www.googleapis.com/youtube/v3/";

public static final Route GET_CHANNEL_DETAILS = new Route(GET, "channels?part=snippet&forHandle={handle}&key={api_key}");

public ChannelRoutes() {
}

public static HttpURLConnection getChannelConnectionFromRoute(Route route, String... params) throws IOException {
return Requester.getConnectionFromRoute(YOUTUBEI_V3_GAPIS_URL, route, params);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,8 @@ public class BaseSettings {
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
public static final StringSetting BYPASS_IMAGE_REGION_RESTRICTIONS_DOMAIN = new StringSetting("revanced_bypass_image_region_restrictions_domain", "yt4.ggpht.com", true);

public static final BooleanSetting RETURN_YOUTUBE_USERNAME_ENABLED = new BooleanSetting("revanced_return_youtube_username_enabled", FALSE, true);
public static final StringSetting RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY = new StringSetting("revanced_return_youtube_username_youtube_data_api_v3_developer_key", "", true, false);

public static final BooleanSetting SANITIZE_SHARING_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE, true);
}
Loading

0 comments on commit 835c4c7

Please sign in to comment.