Skip to content

Commit e3f00f4

Browse files
authored
Support OAuth2 Account Integration (#33)
* OAuth2 integration part 1 * implement token refreshing * cleanups, usability, etc. * Update log message, use Android.BASE_CONFIG for visitor ID fetching * Reapply context filter. * Don't apply token/UA/visitor ID to googlevideo URLs * Allow skipping OAuth initialisation on empty tokens * Fix incorrect number of arguments in YoutubeRestHandler * Correctly extract interval, handle slow_down and access_denied * Note usage of #getContextFilter with rotator. * Slight refactor to oauth token handling and refreshing. * README updates * rename oauthConfig -> oauth * Some minor changes. * Remove todo and clear access token when setting new refresh token
1 parent d97bf92 commit e3f00f4

File tree

9 files changed

+509
-21
lines changed

9 files changed

+509
-21
lines changed

README.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Which clients are used is entirely configurable.
1414
- Information about the `plugin` module and usage of.
1515
- [Available Clients](#available-clients)
1616
- Information about the clients provided by `youtube-source`, as well as their advantages/disadvantages.
17+
- [Using OAuth tokens](#using-oauth-tokens)
18+
- Information on using OAuth tokens with `youtube-source`.
1719
- [Using a poToken](#using-a-potoken)
1820
- Information on using a `poToken` with `youtube-source`.
1921
- [Migration Information](#migration-from-lavaplayers-built-in-youtube-source)
@@ -58,8 +60,9 @@ Support for IP rotation has been included, and can be achieved using the followi
5860
AbstractRoutePlanner routePlanner = new ...
5961
YoutubeIpRotatorSetup rotator = new YoutubeIpRotatorSetup(routePlanner);
6062

63+
// 'youtube' is the variable holding your YoutubeAudioSourceManager instance.
6164
rotator.forConfiguration(youtube.getHttpInterfaceManager(), false)
62-
.withMainDelegateFilter(null) // This is important, otherwise you may get NullPointerExceptions.
65+
.withMainDelegateFilter(youtube.getContextFilter()) // IMPORTANT
6366
.setup();
6467
```
6568

@@ -206,6 +209,52 @@ Currently, the following clients are available for use:
206209
- ✔ Age-restricted video playback.
207210
- ❌ No playlist support.
208211

212+
## Using OAuth Tokens
213+
You may notice that some requests are flagged by YouTube, causing an error message asking you to sign in to confirm you're not a bot.
214+
With OAuth integration, you can request that `youtube-source` use your account credentials to appear as a normal user, with varying degrees
215+
of efficacy. You can instruct `youtube-source` to use OAuth with the following:
216+
217+
> [!WARNING]
218+
> Similar to the `poToken` method, this is NOT a silver bullet solution, and worst case could get your account terminated!
219+
> For this reason, it is advised that **you use burner accounts and NOT your primary!**.
220+
> This method may also trigger ratelimit errors if used in a high traffic environment.
221+
> USE WITH CAUTION!
222+
223+
### Lavaplayer
224+
```java
225+
YoutubeAudioSourceManager source = new YoutubeAudioSourceManager();
226+
// This will trigger an OAuth flow, where you will be instructed to head to YouTube's OAuth page and input a code.
227+
// This is safe, as it only uses YouTube's official OAuth flow. No tokens are seen or stored by us.
228+
source.useOauth2(null, false);
229+
230+
// If you already have a refresh token, you can instruct the source to use it, skipping the OAuth flow entirely.
231+
// You can also set the `skipInitialization` parameter, which skips the OAuth flow. This should only be used
232+
// if you intend to supply a refresh token later on. You **must** either complete the OAuth flow or supply
233+
// a refresh token for OAuth integration to work.
234+
source.useOauth2("your refresh token", true);
235+
```
236+
237+
<!-- TODO document rest routes -->
238+
239+
### Lavalink
240+
```yaml
241+
plugins:
242+
youtube:
243+
enabled: true
244+
oauth:
245+
# setting "enabled: true" is the bare minimum to get OAuth working.
246+
enabled: true
247+
248+
# you may optionally set your refresh token if you have one, which skips the OAuth flow entirely.
249+
# once you have completed the oauth flow at least once, you should see your refresh token within your
250+
# lavalink logs, which can be used here.
251+
refreshToken: "your refresh token, only supply this if you have one!"
252+
253+
# Set this if you don't want the OAuth flow to be triggered, if you intend to supply a refresh token
254+
# later on via REST routes. Initialization is skipped automatically if a valid refresh token is supplied.
255+
skipInitialization: true
256+
```
257+
209258
## Using a `poToken`
210259
A `poToken`, also known as a "Proof of Origin Token" is a way to identify what requests originate from.
211260
In YouTube's case, this is sent as a JavaScript challenge that browsers must evaluate, and send back the resolved

common/src/main/java/dev/lavalink/youtube/YoutubeAudioSourceManager.java

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import dev.lavalink.youtube.clients.skeleton.Client;
1919
import dev.lavalink.youtube.http.YoutubeAccessTokenTracker;
2020
import dev.lavalink.youtube.http.YoutubeHttpContextFilter;
21+
import dev.lavalink.youtube.http.YoutubeOauth2Handler;
2122
import dev.lavalink.youtube.track.YoutubeAudioTrack;
2223
import org.apache.http.client.methods.CloseableHttpResponse;
2324
import org.apache.http.client.methods.HttpGet;
@@ -58,12 +59,15 @@ public class YoutubeAudioSourceManager implements AudioSourceManager {
5859
private static final Pattern shortHandPattern = Pattern.compile("^" + PROTOCOL_REGEX + "(?:" + DOMAIN_REGEX + "/(?:live|embed|shorts)|" + SHORT_DOMAIN_REGEX + ")/(?<videoId>.*)");
5960

6061
protected final HttpInterfaceManager httpInterfaceManager;
62+
6163
protected final boolean allowSearch;
6264
protected final boolean allowDirectVideoIds;
6365
protected final boolean allowDirectPlaylistIds;
6466
protected final Client[] clients;
6567

66-
protected final SignatureCipherManager cipherManager;
68+
protected YoutubeHttpContextFilter contextFilter;
69+
protected YoutubeOauth2Handler oauth2Handler;
70+
protected SignatureCipherManager cipherManager;
6771

6872
public YoutubeAudioSourceManager() {
6973
this(true);
@@ -133,11 +137,13 @@ public YoutubeAudioSourceManager(YoutubeSourceOptions options,
133137
this.allowDirectPlaylistIds = options.isAllowDirectPlaylistIds();
134138
this.clients = clients;
135139
this.cipherManager = new SignatureCipherManager();
140+
this.oauth2Handler = new YoutubeOauth2Handler(httpInterfaceManager);
141+
142+
contextFilter = new YoutubeHttpContextFilter();
143+
contextFilter.setTokenTracker(new YoutubeAccessTokenTracker(httpInterfaceManager));
144+
contextFilter.setOauth2Handler(oauth2Handler);
136145

137-
YoutubeAccessTokenTracker tokenTracker = new YoutubeAccessTokenTracker(httpInterfaceManager);
138-
YoutubeHttpContextFilter youtubeHttpContextFilter = new YoutubeHttpContextFilter();
139-
youtubeHttpContextFilter.setTokenTracker(tokenTracker);
140-
httpInterfaceManager.setHttpContextFilter(youtubeHttpContextFilter);
146+
httpInterfaceManager.setHttpContextFilter(contextFilter);
141147
}
142148

143149
@Override
@@ -151,6 +157,25 @@ public void setPlaylistPageCount(int count) {
151157
}
152158
}
153159

160+
/**
161+
* Instructs this source to use Oauth2 integration.
162+
* {@code null} is valid and will kickstart the oauth process.
163+
* Providing a refresh token will likely skip having to authenticate your account prior to making requests,
164+
* as long as the provided token is still valid.
165+
* @param refreshToken The token to use for generating access tokens. Can be null.
166+
* @param skipInitialization Whether linking of an account should be skipped, if you intend to provide a
167+
* refresh token later. This only applies on null/empty/invalid refresh tokens.
168+
* Valid refresh tokens will not be presented with an initialization prompt.
169+
*/
170+
public void useOauth2(@Nullable String refreshToken, boolean skipInitialization) {
171+
oauth2Handler.setRefreshToken(refreshToken, skipInitialization);
172+
}
173+
174+
@Nullable
175+
public String getOauth2RefreshToken() {
176+
return oauth2Handler.getRefreshToken();
177+
}
178+
154179
@Override
155180
@Nullable
156181
public AudioItem loadItem(@NotNull AudioPlayerManager manager, @NotNull AudioReference reference) {
@@ -348,6 +373,16 @@ public Client[] getClients() {
348373
return clients;
349374
}
350375

376+
@NotNull
377+
public YoutubeHttpContextFilter getContextFilter() {
378+
return contextFilter;
379+
}
380+
381+
@NotNull
382+
public YoutubeOauth2Handler getOauth2Handler() {
383+
return oauth2Handler;
384+
}
385+
351386
@NotNull
352387
public HttpInterfaceManager getHttpInterfaceManager() {
353388
return httpInterfaceManager;

common/src/main/java/dev/lavalink/youtube/http/YoutubeAccessTokenTracker.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools;
55
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface;
66
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager;
7+
import dev.lavalink.youtube.clients.Android;
78
import dev.lavalink.youtube.clients.ClientConfig;
89
import org.apache.http.client.methods.CloseableHttpResponse;
910
import org.apache.http.client.methods.HttpPost;
@@ -69,16 +70,10 @@ private String fetchVisitorId() throws IOException {
6970
try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) {
7071
httpInterface.getContext().setAttribute(TOKEN_FETCH_CONTEXT_ATTRIBUTE, true);
7172

72-
ClientConfig clientConfig = new ClientConfig()
73-
.withUserAgent("com.google.android.youtube/19.07.39 (Linux; U; Android 11) gzip")
74-
.withClientName("ANDROID")
75-
.withClientField("clientVersion", "19.07.39")
76-
.withClientField("androidSdkVersion", 30)
77-
.withUserField("lockedSafetyMode", false)
78-
.setAttributes(httpInterface);
73+
ClientConfig client = Android.BASE_CONFIG.setAttributes(httpInterface);
7974

8075
HttpPost visitorIdPost = new HttpPost("https://youtubei.googleapis.com/youtubei/v1/visitor_id");
81-
visitorIdPost.setEntity(new StringEntity(clientConfig.toJsonString(), "UTF-8"));
76+
visitorIdPost.setEntity(new StringEntity(client.toJsonString(), "UTF-8"));
8277

8378
try (CloseableHttpResponse response = httpInterface.execute(visitorIdPost)) {
8479
HttpClientTools.assertSuccessWithContent(response, "youtube visitor id");

common/src/main/java/dev/lavalink/youtube/http/YoutubeHttpContextFilter.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,16 @@ public class YoutubeHttpContextFilter extends BaseYoutubeHttpContextFilter {
2424
private static final HttpContextRetryCounter retryCounter = new HttpContextRetryCounter("yt-token-retry");
2525

2626
private YoutubeAccessTokenTracker tokenTracker;
27+
private YoutubeOauth2Handler oauth2Handler;
2728

2829
public void setTokenTracker(@NotNull YoutubeAccessTokenTracker tokenTracker) {
2930
this.tokenTracker = tokenTracker;
3031
}
3132

33+
public void setOauth2Handler(@NotNull YoutubeOauth2Handler oauth2Handler) {
34+
this.oauth2Handler = oauth2Handler;
35+
}
36+
3237
@Override
3338
public void onContextOpen(HttpClientContext context) {
3439
CookieStore cookieStore = context.getCookieStore();
@@ -57,16 +62,24 @@ public void onRequest(HttpClientContext context,
5762
return;
5863
}
5964

65+
if (oauth2Handler.isOauthFetchContext(context)) {
66+
return;
67+
}
68+
6069
String userAgent = context.getAttribute(ATTRIBUTE_USER_AGENT_SPECIFIED, String.class);
6170

62-
if (userAgent != null) {
63-
request.setHeader("User-Agent", userAgent);
71+
if (!request.getURI().getHost().contains("googlevideo")) {
72+
if (userAgent != null) {
73+
request.setHeader("User-Agent", userAgent);
74+
75+
String visitorData = context.getAttribute(ATTRIBUTE_VISITOR_DATA_SPECIFIED, String.class);
76+
request.setHeader("X-Goog-Visitor-Id", visitorData != null ? visitorData : tokenTracker.getVisitorId());
6477

65-
String visitorData = context.getAttribute(ATTRIBUTE_VISITOR_DATA_SPECIFIED, String.class);
66-
request.setHeader("X-Goog-Visitor-Id", visitorData != null ? visitorData : tokenTracker.getVisitorId());
78+
context.removeAttribute(ATTRIBUTE_VISITOR_DATA_SPECIFIED);
79+
context.removeAttribute(ATTRIBUTE_USER_AGENT_SPECIFIED);
80+
}
6781

68-
context.removeAttribute(ATTRIBUTE_VISITOR_DATA_SPECIFIED);
69-
context.removeAttribute(ATTRIBUTE_USER_AGENT_SPECIFIED);
82+
oauth2Handler.applyToken(request);
7083
}
7184

7285
// try {

0 commit comments

Comments
 (0)