Skip to content

Commit 46daa33

Browse files
Merge remote-tracking branch 'upstream/main'
2 parents 4b8f541 + 294d3c5 commit 46daa33

File tree

11 files changed

+172
-57
lines changed

11 files changed

+172
-57
lines changed

.github/workflows/publish.yml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,37 @@ jobs:
1111
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
1212
steps:
1313
- name: Checkout
14-
uses: actions/checkout@v3
14+
uses: actions/checkout@v4
1515
with:
1616
fetch-depth: 0
1717

1818
- name: Setup Java
19-
uses: actions/setup-java@v3
19+
uses: actions/setup-java@v4
2020
with:
2121
distribution: zulu
2222
java-version: 17
2323
cache: gradle
2424

2525
- name: Setup Gradle
26-
uses: gradle/gradle-build-action@v2
26+
uses: gradle/actions/setup-gradle@v4
2727

2828
- name: Build and Publish
2929
run: ./gradlew build publish --no-daemon -PMAVEN_USERNAME=$MAVEN_USERNAME -PMAVEN_PASSWORD=$MAVEN_PASSWORD
3030

3131
- name: Upload common Artifact
32-
uses: actions/upload-artifact@v3
32+
uses: actions/upload-artifact@v4
3333
with:
3434
name: youtube-common.jar
3535
path: common/build/libs/youtube-common-*.jar
3636

3737
- name: Upload lldevs Artifact
38-
uses: actions/upload-artifact@v3
38+
uses: actions/upload-artifact@v4
3939
with:
4040
name: youtube-v2.jar
4141
path: v2/build/libs/youtube-v2-*.jar
4242

4343
- name: Upload plugin Artifact
44-
uses: actions/upload-artifact@v3
44+
uses: actions/upload-artifact@v4
4545
with:
4646
name: youtube-plugin.jar
4747
path: plugin/build/libs/youtube-plugin-*.jar
@@ -52,20 +52,20 @@ jobs:
5252
if: github.event_name == 'release'
5353
steps:
5454
- name: Checkout
55-
uses: actions/checkout@v3
55+
uses: actions/checkout@v4
5656

5757
- name: Download youtube-common Artifact
58-
uses: actions/download-artifact@v3
58+
uses: actions/download-artifact@v4
5959
with:
6060
name: youtube-common.jar
6161

6262
- name: Download youtube-v2 Artifact
63-
uses: actions/download-artifact@v3
63+
uses: actions/download-artifact@v4
6464
with:
6565
name: youtube-v2.jar
6666

6767
- name: Download youtube-plugin Artifact
68-
uses: actions/download-artifact@v3
68+
uses: actions/download-artifact@v4
6969
with:
7070
name: youtube-plugin.jar
7171

README.md

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -185,43 +185,23 @@ plugins:
185185
## Available Clients
186186
Currently, the following clients are available for use:
187187

188-
- `MUSIC`
189-
- ✔ Provides support for searching YouTube music (`ytmsearch:`).
190-
- ❌ Cannot be used for playback, or playlist/mix/livestream loading.
191-
- `WEB`
192-
- ✔ Opus formats.
193-
- `MWEB`
194-
- ✔ Opus formats.
195-
- `WEBEMBEDDED`
196-
- ✔ Opus formats.
197-
- ✔ Limited age-restricted video playback.
198-
- ❌ No mix/playlist/search support.
199-
- `ANDROID`
200-
- ❌ Heavily restricted, frequently dysfunctional.
201-
- `ANDROID_MUSIC`
202-
- ✔ Opus formats.
203-
- ❌ No playlist/livestream support.
204-
- `ANDROID_VR`
205-
- ✔ Opus formats.
206-
- `IOS`
207-
- ❌ No Opus formats (requires transcoding).
208-
- `TV`
209-
- ✔ Opus formats.
210-
- ✔ OAuth compatibility.
211-
- ❌ No mix/playlist/search/video *lookup* support.
212-
- ❌ Playback only.
213-
- `TVHTML5EMBEDDED`
214-
- ✔ Opus formats.
215-
- ✔ OAuth compatibility.
216-
- ❌ No playlist support.
217-
- ❌ Playback requires sign-in.
188+
| Identifier | Opus Formats | OAuth | Age-restriction Support | Playback Support | Metadata Support | Additional Notes |
189+
|-------------------|--------------|-------|-------------------------|------------------|------------------------------|------------------------------------------------------|
190+
| `MUSIC` | No | No | No | No | Search | YouTube music search support via `ytmsearch:` prefix |
191+
| `WEB` | Yes | No | No | Yes + Livestream | Video, Search, Playlist, Mix | |
192+
| `MWEB` | Yes | No | No | Yes + Livestream | Video, Search, Playlist, Mix | |
193+
| `WEBEMBEDDED` | Yes | No | Limited | Yes + Livestream | Video | |
194+
| `ANDROID` | Yes | No | No | Yes + Livestream | Video, Search, Playlist, Mix | Heavily restricted, frequently dysfunctional |
195+
| `ANDROID_MUSIC` | Yes | No | No | Yes | Video, Search, Mix | |
196+
| `ANDROID_VR` | Yes | No | No | Yes + Livestream | Video, Search, Playlist, Mix | |
197+
| `IOS` | No | No | No | Yes + Livestream | Video, Search, Playlist, Mix | |
198+
| `TV` | Yes | Yes | With OAuth | Yes + Livestream | None | Playback requires sign-in |
199+
| `TVHTML5EMBEDDED` | Yes | Yes | With OAuth | Yes + Livestream | Video, Search, Mix | Playback requires sign-in |
218200

219201
> [!NOTE]
202+
> Clients that do not return Opus formats will require transcoding.
220203
> Livestreams do not yield Opus formats so will always require transcoding.
221204

222-
> [!NOTE]
223-
> Assume clients do not work with OAuth unless stated.
224-
225205

226206
## Using OAuth Tokens
227207
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.
@@ -280,6 +260,17 @@ plugins:
280260
# cookie: "paste your google account cookie here which your exported from browser"
281261
```
282262

263+
### Passing an oauth token from your client
264+
Another option to use oauth is by using oauth access tokens that are managed from your client. In this case your
265+
bot/client provides LavaLink with the token to use when playing a track. To do this simply add the oauth access token
266+
to a track's [userData](https://lavalink.dev/api/rest#track) field in a json format when updating the player to
267+
play a track like:
268+
```json
269+
{
270+
"oauth-token": "access token to use"
271+
}
272+
```
273+
283274
## Using a `poToken`
284275
A `poToken`, also known as a "Proof of Origin Token" is a way to identify what requests originate from.
285276
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.io.DataOutput;
3232
import java.io.IOException;
3333
import java.net.URI;
34+
import java.util.Arrays;
3435
import java.util.List;
3536
import java.util.regex.Matcher;
3637
import java.util.regex.Pattern;
@@ -169,6 +170,12 @@ public void setPlaylistPageCount(int count) {
169170
*/
170171
public void useOauth2(@Nullable String refreshToken, boolean skipInitialization) {
171172
oauth2Handler.setRefreshToken(refreshToken, skipInitialization);
173+
174+
if (Arrays.stream(clients).noneMatch(Client::supportsOAuth)) {
175+
log.warn("OAuth has been enabled without registering any OAuth-compatible clients. " +
176+
"Please consult https://github.com/lavalink-devs/youtube-source?tab=readme-ov-file#available-clients for a list of " +
177+
"OAuth-compatible clients.");
178+
}
172179
}
173180

174181
@Nullable

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package dev.lavalink.youtube;
22

3+
import dev.lavalink.youtube.clients.Web;
4+
import dev.lavalink.youtube.clients.WebEmbedded;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
38
import java.io.IOException;
49
import java.io.InputStream;
510

611
public class YoutubeSource {
12+
private static final Logger log = LoggerFactory.getLogger(YoutubeSource.class);
13+
714
public static String VERSION = "Unknown";
815

916
static {
@@ -22,4 +29,18 @@ public class YoutubeSource {
2229

2330
}
2431
}
32+
33+
/**
34+
* Sets the given PoToken and VisitorData pair on all POT-supporting clients.
35+
* This is a convenience method to allow for setting this from one method call.
36+
* @param poToken The poToken to use. This must be paired to the specified visitorData.
37+
* You may specify {@code null} to unset.
38+
* @param visitorData The visitorData to use. This must be paired to the specified poToken.
39+
* You may specify {@code null} to unset.
40+
*/
41+
public static void setPoTokenAndVisitorData(String poToken, String visitorData) {
42+
log.debug("Applying pot: {} vd: {} to WEB, WEBEMBEDDED", poToken, visitorData);
43+
Web.setPoTokenAndVisitorData(poToken, visitorData);
44+
WebEmbedded.setPoTokenAndVisitorData(poToken, visitorData);
45+
}
2546
}

common/src/main/java/dev/lavalink/youtube/cipher/SignatureCipher.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,19 @@ public class SignatureCipher {
1717
public final String scriptTimestamp;
1818
public final String rawScript;
1919

20+
public final boolean tceScript;
21+
public final String tceVars;
22+
2023
public SignatureCipher(@NotNull String nFunction,
2124
@NotNull String timestamp,
22-
@NotNull String rawScript) {
25+
@NotNull String rawScript,
26+
boolean tceScript,
27+
@NotNull String tceVars) {
2328
this.nFunction = nFunction;
2429
this.scriptTimestamp = timestamp;
2530
this.rawScript = rawScript;
31+
this.tceScript = tceScript;
32+
this.tceVars = tceVars;
2633
}
2734

2835
/**
@@ -63,7 +70,7 @@ public String apply(@NotNull String text) {
6370
public String transform(@NotNull String text, @NotNull ScriptEngine scriptEngine) throws ScriptException, NoSuchMethodException {
6471
String transformed;
6572

66-
scriptEngine.eval("n=" + nFunction);
73+
scriptEngine.eval("n=" + nFunction + (tceScript ? tceVars : ""));
6774
transformed = (String) ((Invocable) scriptEngine).invokeFunction("n", text);
6875

6976
return transformed;

common/src/main/java/dev/lavalink/youtube/cipher/SignatureCipherManager.java

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
public class SignatureCipherManager {
4646
private static final Logger log = LoggerFactory.getLogger(SignatureCipherManager.class);
4747

48-
private static final String VARIABLE_PART = "[a-zA-Z_\\$][a-zA-Z_0-9]*";
48+
private static final String VARIABLE_PART = "[a-zA-Z_\\$][a-zA-Z_0-9\\$]*";
4949
private static final String VARIABLE_PART_DEFINE = "\\\"?" + VARIABLE_PART + "\\\"?";
5050
private static final String BEFORE_ACCESS = "(?:\\[\\\"|\\.)";
5151
private static final String AFTER_ACCESS = "(?:\\\"\\]|)";
@@ -89,6 +89,24 @@ public class SignatureCipherManager {
8989
"\\s*return\"[\\w-]+([A-z0-9-]+)\"\\s*\\+\\s*\\1\\s*}" +
9090
"\\s*return\\s*(\\2\\.join\\(\"\"\\)|Array\\.prototype\\.join\\.call\\(\\2,.*?\\))};", Pattern.DOTALL);
9191

92+
private static final Pattern tceGlobalVarsPattern = Pattern.compile(
93+
"(?:^|[;,])\\s*(var\\s+([\\w$]+)\\s*=\\s*\"(?:[^\"\\\\]|\\\\.)+\"\\s*\\.\\s*split\\(\"([^\"\\\\]|\\\\.)\"\\))(?=\\s*[,;])"
94+
);
95+
96+
private static final Pattern functionTcePattern = Pattern.compile(
97+
"function(?:\\s+[a-zA-Z_\\$][a-zA-Z0-9_\\$]*)?\\(\\w\\)\\{" +
98+
"\\w=\\w\\.split\\((?:\"\"|[a-zA-Z0-9_$]*\\[\\d+])\\);" +
99+
"\\s*((?:(?:\\w=)?[a-zA-Z_\\$][a-zA-Z0-9_\\$]*(?:\\[\\\"|\\.)[a-zA-Z_\\$][a-zA-Z0-9_\\$]*(?:\\\"\\]|)\\(\\w,\\d+\\);)+)" +
100+
"return \\w\\.join\\((?:\"\"|[a-zA-Z0-9_$]*\\[\\d+])\\)}"
101+
);
102+
103+
private static final Pattern nFunctionTcePattern = Pattern.compile(
104+
"function\\(\\s*(\\w+)\\s*\\)\\s*\\{" +
105+
"\\s*var\\s*(\\w+)=\\1\\.split\\(\\1\\.slice\\(0,0\\)\\),\\s*(\\w+)=\\[.*?];" +
106+
".*?catch\\(\\s*(\\w+)\\s*\\)\\s*\\{" +
107+
"\\s*return(?:\"[^\"]+\"|\\s*[a-zA-Z_0-9$]*\\[\\d+])\\s*\\+\\s*\\1\\s*}" +
108+
"\\s*return\\s*\\2\\.join\\((?:\"\"|[a-zA-Z_0-9$]*\\[\\d+])\\)};", Pattern.DOTALL);
109+
92110
private final ConcurrentMap<String, SignatureCipher> cipherCache;
93111
private final Set<String> dumpedScriptUrls;
94112
private final ScriptEngine scriptEngine;
@@ -244,9 +262,10 @@ private void dumpProblematicScript(@NotNull String script, @NotNull String sourc
244262

245263
private SignatureCipher extractFromScript(@NotNull String script, @NotNull String sourceUrl) {
246264
Matcher actions = actionsPattern.matcher(script);
247-
Matcher nFunctionMatcher = nFunctionPattern.matcher(script);
248265
Matcher scriptTimestamp = timestampPattern.matcher(script);
249266

267+
boolean matchedTce = false;
268+
250269
if (!actions.find()) {
251270
dumpProblematicScript(script, sourceUrl, "no actions match");
252271
throw new IllegalStateException("Must find action functions from script: " + sourceUrl);
@@ -267,28 +286,62 @@ private SignatureCipher extractFromScript(@NotNull String script, @NotNull Strin
267286

268287
Matcher functions = functionPattern.matcher(script);
269288
if (!functions.find()) {
270-
dumpProblematicScript(script, sourceUrl, "no decipher function match");
271-
throw new IllegalStateException("Must find decipher function from script.");
289+
functions = functionTcePattern.matcher(script);
290+
291+
if (!functions.find()) {
292+
dumpProblematicScript(script, sourceUrl, "no decipher function match");
293+
throw new IllegalStateException("Must find decipher function from script.");
294+
}
295+
296+
matchedTce = true;
272297
}
273298

274-
Matcher matcher = extractor.matcher(functions.group(2));
299+
Matcher matcher = extractor.matcher(functions.group(matchedTce ? 1 : 2));
275300

276301
if (!scriptTimestamp.find()) {
277302
dumpProblematicScript(script, sourceUrl, "no timestamp match");
278303
throw new IllegalStateException("Must find timestamp from script: " + sourceUrl);
279304
}
280305

306+
// use matchedTce hint to determine which regex we should use to parse the script.
307+
Matcher nFunctionMatcher = matchedTce ? nFunctionTcePattern.matcher(script) : nFunctionPattern.matcher(script);
308+
281309
if (!nFunctionMatcher.find()) {
282-
dumpProblematicScript(script, sourceUrl, "no n function match");
283-
throw new IllegalStateException("Must find n function from script: " + sourceUrl);
310+
// fall back to the opposite of what we used above.
311+
nFunctionMatcher = matchedTce ? nFunctionPattern.matcher(script) : nFunctionTcePattern.matcher(script);
312+
313+
if (!nFunctionMatcher.find()) {
314+
dumpProblematicScript(script, sourceUrl, "no n function match");
315+
throw new IllegalStateException("Must find n function from script: " + sourceUrl);
316+
}
317+
318+
// unconditionally set this to true.
319+
// we either start with the non-tce regex and then fall back to the tce regex,
320+
// in which case we have matched a tce script.
321+
// otherwise, we first checked with the tce regex but didn't match and defaulted to
322+
// the legacy regex, but in this case the variable can only have a value of true.
323+
matchedTce = true;
324+
}
325+
326+
Matcher tceVars = tceGlobalVarsPattern.matcher(script);
327+
String tceText = "";
328+
329+
if (!tceVars.find()) {
330+
if (matchedTce) {
331+
dumpProblematicScript(script, sourceUrl, "no tce variables match");
332+
log.warn("Got tce player script but could not find global variables: {}", sourceUrl);
333+
}
334+
} else {
335+
tceText = tceVars.group(1);
284336
}
285337

286338
String nFunction = nFunctionMatcher.group(0);
287339
String nfParameterName = DataFormatTools.extractBetween(nFunction, "(", ")");
288340
// remove short-circuit that prevents n challenge transformation
289-
nFunction = nFunction.replaceAll("if\\s*\\(\\s*typeof\\s*\\w+\\s*===?.*?\\)\\s*return\\s+" + nfParameterName + "\\s*;?", "");
341+
// nFunction = nFunction.replaceAll("if\\s*\\(\\s*typeof\\s*[\\w$]+\\s*===?.*?\\)\\s*return\\s+" + nfParameterName + "\\s*;?", "");
342+
nFunction = nFunction.replaceAll("if\\s*\\(typeof\\s*[^\\s()]+\\s*===?.*?\\)return " + nfParameterName + "\\s*;?", "");
290343

291-
SignatureCipher cipherKey = new SignatureCipher(nFunction, scriptTimestamp.group(2), script);
344+
SignatureCipher cipherKey = new SignatureCipher(nFunction, scriptTimestamp.group(2), script, matchedTce, tceText);
292345

293346
while (matcher.find()) {
294347
String type = matcher.group(1);

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.slf4j.LoggerFactory;
1515

1616
import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.COMMON;
17+
import static dev.lavalink.youtube.http.YoutubeOauth2Handler.OAUTH_INJECT_CONTEXT_ATTRIBUTE;
1718

1819
public class YoutubeHttpContextFilter extends BaseYoutubeHttpContextFilter {
1920
private static final Logger log = LoggerFactory.getLogger(YoutubeHttpContextFilter.class);
@@ -84,7 +85,12 @@ public void onRequest(HttpClientContext context,
8485
boolean isRequestFromOauthedClient = context.getAttribute(Client.OAUTH_CLIENT_ATTRIBUTE) == Boolean.TRUE;
8586

8687
if (isRequestFromOauthedClient && request.getURI().toString().contains("/youtubei/v1/player")) {
87-
oauth2Handler.applyToken(request);
88+
String oauthToken = context.getAttribute(OAUTH_INJECT_CONTEXT_ATTRIBUTE, String.class);
89+
if (oauthToken != null && !oauthToken.isEmpty()) {
90+
oauth2Handler.applyToken(request, oauthToken);
91+
} else {
92+
oauth2Handler.applyToken(request);
93+
}
8894
oauthApplied = true;
8995
}
9096

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class YoutubeOauth2Handler {
4040
private static final String CLIENT_SECRET = "SboVhoG9s0rNafixCSGGKXAT";
4141
private static final String SCOPES = "http://gdata.youtube.com https://www.googleapis.com/auth/youtube";
4242
private static final String OAUTH_FETCH_CONTEXT_ATTRIBUTE = "yt-oauth";
43+
public static final String OAUTH_INJECT_CONTEXT_ATTRIBUTE = "yt-oauth-token";
4344

4445
private final HttpInterfaceManager httpInterfaceManager;
4546

@@ -359,6 +360,10 @@ public void applyToken(HttpUriRequest request) {
359360
}
360361
}
361362

363+
public void applyToken(HttpUriRequest request, String token) {
364+
request.setHeader("Authorization", String.format("%s %s", "Bearer", token));
365+
}
366+
362367
private HttpInterface getHttpInterface() {
363368
HttpInterface httpInterface = httpInterfaceManager.getInterface();
364369
httpInterface.getContext().setAttribute(OAUTH_FETCH_CONTEXT_ATTRIBUTE, true);

0 commit comments

Comments
 (0)