This repository has been archived by the owner on Dec 11, 2024. It is now read-only.
forked from ReVanced/revanced-integrations
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(YouTube & YouTube Music): Add
Return YouTube Username
patch
- Loading branch information
Showing
14 changed files
with
532 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
app/src/main/java/app/revanced/integrations/shared/patches/ReturnYouTubeUsernamePatch.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
147 changes: 147 additions & 0 deletions
147
.../java/app/revanced/integrations/shared/returnyoutubeusername/requests/ChannelRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 + '\'' + '}'; | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
...n/java/app/revanced/integrations/shared/returnyoutubeusername/requests/ChannelRoutes.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.