mirror of
https://github.com/chrisb09/MusicBot.git
synced 2026-11-05 17:02:35 -05:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3886ccbe65 | ||
|
|
fddde60e23 | ||
|
|
838ff69090 | ||
|
|
66cc718d30 | ||
|
|
ef134be9c0 |
No files matched your search
@@ -6,6 +6,8 @@ A self-hosted Discord music bot fork based on JMusicBot, updated to keep working
|
||||
|
||||
This repository keeps the bot practical to run today, while also adding fork-specific features such as status messages and playback analytics logging. Possibly more.
|
||||
|
||||
Also, we somewhat regularly bump [lavaplayer](https://mvnrepository.com/artifact/dev.arbjerg/lavaplayer)/[lavalink](https://maven.lavalink.dev/#/releases/dev/lavalink/youtube/common) to their new versions. I will try to update this automatically in the future.
|
||||
|
||||
## Demo
|
||||
|
||||
Temporary invite: [discord.gg/4cp9CvjwaW](https://discord.gg/4cp9CvjwaW)
|
||||
@@ -25,9 +27,9 @@ Permanent invite: [discord.gg/hK95396uB3](https://discord.gg/hK95396uB3)
|
||||
|
||||
## Quick Links
|
||||
|
||||
- Fork changes and version notes: [docs/fork-changelog.md](/git/MusicBot/docs/fork-changelog.md)
|
||||
- Setup guide: [docs/setup.md](/git/MusicBot/docs/setup.md)
|
||||
- Docker usage: [docs/docker.md](/git/MusicBot/docs/docker.md)
|
||||
- Fork changes and version notes: [docs/fork-changelog.md](/docs/fork-changelog.md)
|
||||
- Setup guide: [docs/setup.md](/docs/setup.md)
|
||||
- Docker usage: [docs/docker.md](/docs/docker.md)
|
||||
|
||||
## Features
|
||||
|
||||
@@ -70,13 +72,13 @@ If you only need the essentials:
|
||||
3. Configure `config.txt`.
|
||||
4. Run the jar directly or use Docker.
|
||||
|
||||
For the detailed version, see [docs/setup.md](/git/MusicBot/docs/setup.md).
|
||||
For the detailed version, see [docs/setup.md](/docs/setup.md).
|
||||
|
||||
## Docker
|
||||
|
||||
A Docker image is published here: [docker.io/chrisb09/jmusicbot](https://hub.docker.com/r/chrisb09/jmusicbot)
|
||||
|
||||
For compose examples and deployment notes, see [docs/docker.md](/git/MusicBot/docs/docker.md).
|
||||
For compose examples and deployment notes, see [docs/docker.md](/docs/docker.md).
|
||||
|
||||
## YouTube Login
|
||||
|
||||
@@ -90,15 +92,15 @@ youtubeoauth2=true
|
||||
|
||||
Do not use your main Google account. Use a burner or otherwise disposable account instead.
|
||||
|
||||
More details are in [docs/setup.md](/git/MusicBot/docs/setup.md).
|
||||
More details are in [docs/setup.md](/docs/setup.md).
|
||||
|
||||
If you use PO tokens or visitor data for YouTube, those details are also covered in [docs/setup.md](/git/MusicBot/docs/setup.md).
|
||||
If you use PO tokens or visitor data for YouTube, those details are also covered in [docs/setup.md](/docs/setup.md).
|
||||
|
||||
## DAVE / Voice Encryption
|
||||
|
||||
Discord currently requires DAVE for voice on modern API versions. This fork configures `libdave-jvm` during startup and bundles native binaries for common Linux, Windows, and macOS targets.
|
||||
|
||||
If you deploy on an unsupported architecture, add the matching native dependency in [pom.xml](/git/MusicBot/pom.xml). You probably need to build the library from source for your architecture, so have fun with that.
|
||||
If you deploy on an unsupported architecture, add the matching native dependency in [pom.xml](/pom.xml). You probably need to build the library from source for your architecture, so have fun with that.
|
||||
|
||||
## Upstream And Related Work
|
||||
|
||||
@@ -119,4 +121,4 @@ If you run into issues, please include:
|
||||
- relevant config changes
|
||||
- relevant logs
|
||||
|
||||
Fork-specific version notes live in [docs/fork-changelog.md](/git/MusicBot/docs/fork-changelog.md), which should help with troubleshooting and migration as the fork evolves.
|
||||
Fork-specific version notes live in [docs/fork-changelog.md](/docs/fork-changelog.md), which should help with troubleshooting and migration as the fork evolves.
|
||||
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.jagrosh</groupId>
|
||||
<artifactId>JMusicBot</artifactId>
|
||||
<version>0.6.6</version>
|
||||
<version>0.6.7</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<repositories>
|
||||
@@ -151,7 +151,7 @@
|
||||
<dependency>
|
||||
<groupId>dev.arbjerg</groupId>
|
||||
<artifactId>lavaplayer</artifactId>
|
||||
<version>2.2.4</version>
|
||||
<version>2.2.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.arbjerg</groupId>
|
||||
@@ -161,7 +161,7 @@
|
||||
<dependency>
|
||||
<groupId>dev.lavalink.youtube</groupId>
|
||||
<artifactId>common</artifactId>
|
||||
<version>1.18.1</version>
|
||||
<version>f45bbb7aebfcbc1c553769e04af6cd43afa8b7c3-SNAPSHOT</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.jagrosh</groupId>
|
||||
@@ -203,7 +203,7 @@
|
||||
<dependency>
|
||||
<groupId>org.seleniumhq.selenium</groupId>
|
||||
<artifactId>selenium-java</artifactId>
|
||||
<version>4.27.0</version>
|
||||
<version>4.47.0</version>
|
||||
<!-- https://github.com/SeleniumHQ/selenium/issues/13791 -->
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
|
||||
@@ -176,7 +176,7 @@ public class Bot
|
||||
return;
|
||||
shuttingDown = true;
|
||||
threadpool.shutdownNow();
|
||||
if(jda.getStatus()!=JDA.Status.SHUTTING_DOWN)
|
||||
if(jda != null && jda.getStatus()!=JDA.Status.SHUTTING_DOWN)
|
||||
{
|
||||
jda.getGuilds().stream().forEach(g ->
|
||||
{
|
||||
|
||||
@@ -40,7 +40,17 @@ import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.IpBlock;
|
||||
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv4Block;
|
||||
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv6Block;
|
||||
import dev.lavalink.youtube.YoutubeAudioSourceManager;
|
||||
import dev.lavalink.youtube.clients.Android;
|
||||
import dev.lavalink.youtube.clients.AndroidMusic;
|
||||
import dev.lavalink.youtube.clients.AndroidVr;
|
||||
import dev.lavalink.youtube.clients.ClientOptions;
|
||||
import dev.lavalink.youtube.clients.Ios;
|
||||
import dev.lavalink.youtube.clients.MWeb;
|
||||
import dev.lavalink.youtube.clients.Music;
|
||||
import dev.lavalink.youtube.clients.Tv;
|
||||
import dev.lavalink.youtube.clients.TvHtml5Simply;
|
||||
import dev.lavalink.youtube.clients.Web;
|
||||
import dev.lavalink.youtube.clients.WebEmbedded;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -68,7 +78,7 @@ public class PlayerManager extends DefaultAudioPlayerManager
|
||||
{
|
||||
TransformativeAudioSourceManager.createTransforms(bot.getConfig().getTransforms()).forEach(t -> registerSourceManager(t));
|
||||
|
||||
YoutubeAudioSourceManager yt = new YoutubeAudioSourceManager(true);
|
||||
YoutubeAudioSourceManager yt = setupYoutubeAudioSourceManager();
|
||||
if (config.getYTRoutingPlanner() != YouTubeUtil.RoutingPlanner.NONE)
|
||||
{
|
||||
AbstractRoutePlanner routePlanner = YouTubeUtil.createRouterPlanner(config.getYTRoutingPlanner(), config.getYTIpBlocks());
|
||||
@@ -85,7 +95,15 @@ public class PlayerManager extends DefaultAudioPlayerManager
|
||||
registerSourceManager(SoundCloudAudioSourceManager.createDefault());
|
||||
registerSourceManager(new BandcampAudioSourceManager());
|
||||
registerSourceManager(new VimeoAudioSourceManager());
|
||||
registerSourceManager(new TwitchStreamAudioSourceManager());
|
||||
try
|
||||
{
|
||||
registerSourceManager(new TwitchStreamAudioSourceManager());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOGGER.warn("Skipping Twitch source initialization because Twitch lookup failed during startup: {}", e.getMessage());
|
||||
LOGGER.debug("Twitch source initialization failure:", e);
|
||||
}
|
||||
registerSourceManager(new BeamAudioSourceManager());
|
||||
registerSourceManager(new GetyarnAudioSourceManager());
|
||||
registerSourceManager(new NicoAudioSourceManager());
|
||||
@@ -98,7 +116,20 @@ public class PlayerManager extends DefaultAudioPlayerManager
|
||||
|
||||
private YoutubeAudioSourceManager setupYoutubeAudioSourceManager()
|
||||
{
|
||||
YoutubeAudioSourceManager yt = new YoutubeAudioSourceManager(true);
|
||||
ClientOptions playbackOnly = new ClientOptions();
|
||||
playbackOnly.setSearching(false);
|
||||
|
||||
YoutubeAudioSourceManager yt = new YoutubeAudioSourceManager(true,
|
||||
new Ios(playbackOnly),
|
||||
new Music(playbackOnly),
|
||||
new AndroidMusic(playbackOnly),
|
||||
new TvHtml5Simply(playbackOnly),
|
||||
new Tv(playbackOnly),
|
||||
new Web(),
|
||||
new MWeb(),
|
||||
new WebEmbedded(),
|
||||
new AndroidVr(playbackOnly),
|
||||
new Android(playbackOnly));
|
||||
yt.setPlaylistPageCount(bot.getConfig().getMaxYTPlaylistPages());
|
||||
|
||||
// OAuth2 setup
|
||||
|
||||
@@ -4,28 +4,37 @@ import com.sedmelluq.lava.extensions.youtuberotator.planner.*;
|
||||
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.IpBlock;
|
||||
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv4Block;
|
||||
import com.sedmelluq.lava.extensions.youtuberotator.tools.ip.Ipv6Block;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import dev.lavalink.youtube.clients.Web;
|
||||
import dev.lavalink.youtube.clients.WebEmbedded;
|
||||
import org.json.JSONObject;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.Cookie;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.chrome.ChromeDriver;
|
||||
import org.openqa.selenium.chrome.ChromeDriverService;
|
||||
import org.openqa.selenium.chrome.ChromeOptions;
|
||||
import org.openqa.selenium.chromium.ChromiumDriverLogLevel;
|
||||
import org.openqa.selenium.devtools.NetworkInterceptor;
|
||||
import org.openqa.selenium.logging.LogEntries;
|
||||
import org.openqa.selenium.logging.LogEntry;
|
||||
import org.openqa.selenium.logging.LogType;
|
||||
import org.openqa.selenium.logging.LoggingPreferences;
|
||||
import org.openqa.selenium.os.ExecutableFinder;
|
||||
import org.openqa.selenium.remote.http.Contents;
|
||||
import org.openqa.selenium.remote.http.Filter;
|
||||
import org.openqa.selenium.remote.http.HttpMethod;
|
||||
import org.openqa.selenium.support.ui.ExpectedConditions;
|
||||
import org.openqa.selenium.support.ui.WebDriverWait;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import io.github.bonigarcia.wdm.WebDriverManager;
|
||||
|
||||
@@ -105,6 +114,21 @@ public class YouTubeUtil {
|
||||
|
||||
chromeOptions.setBinary(userChromePath != null ? userChromePath : wdm.getBrowserPath().get().toString());
|
||||
|
||||
// Essential flags for headless/containerized environments (Docker)
|
||||
chromeOptions.addArguments("--no-sandbox");
|
||||
chromeOptions.addArguments("--disable-dev-shm-usage");
|
||||
chromeOptions.addArguments("--disable-gpu");
|
||||
chromeOptions.addArguments("--window-size=1920,1080");
|
||||
chromeOptions.addArguments("--disable-blink-features=AutomationControlled");
|
||||
chromeOptions.addArguments("--autoplay-policy=no-user-gesture-required");
|
||||
chromeOptions.addArguments("--disable-features=IsolateOrigins,site-per-process");
|
||||
chromeOptions.addArguments("--disable-site-isolation-trials");
|
||||
|
||||
// Enable Chrome performance logging to capture Network.requestWillBeSent events
|
||||
LoggingPreferences logPrefs = new LoggingPreferences();
|
||||
logPrefs.enable(LogType.PERFORMANCE, Level.ALL);
|
||||
chromeOptions.setCapability("goog:loggingPrefs", logPrefs);
|
||||
|
||||
if (!headless)
|
||||
chromeOptions.addArguments("--auto-open-devtools-for-tabs");
|
||||
else
|
||||
@@ -117,77 +141,247 @@ public class YouTubeUtil {
|
||||
WebDriver driver = new ChromeDriver(chromeDriverBuilder.build(), chromeOptions);
|
||||
|
||||
//
|
||||
// 2. Setup a network interceptor to intercept the player API request & obtain po token and visitor data
|
||||
//
|
||||
|
||||
// as the network interceptor below runs on a different thread, we'll have it notify us on this/current thread
|
||||
// whether retrieving the data failed (completes with the Exception) or succeeded (completes with null)
|
||||
CompletableFuture<Exception> interceptorFuture = new CompletableFuture<>();
|
||||
|
||||
NetworkInterceptor interceptor = new NetworkInterceptor(driver,
|
||||
(Filter) next -> req -> {
|
||||
|
||||
if (req.getMethod() != HttpMethod.POST || !req.getUri().contains("/youtubei/v1/player"))
|
||||
return next.execute(req);
|
||||
|
||||
try {
|
||||
String rawBody = Contents.string(req);
|
||||
JSONObject body = new JSONObject(rawBody);
|
||||
|
||||
String visitorData = body
|
||||
.getJSONObject("context")
|
||||
.getJSONObject("client")
|
||||
.getString("visitorData");
|
||||
|
||||
String poToken = body
|
||||
.getJSONObject("serviceIntegrityDimensions")
|
||||
.getString("poToken");
|
||||
|
||||
|
||||
LOGGER.info("Successfully retrieved PO token & visitor data for YouTube playback.");
|
||||
LOGGER.debug("PO token: {}", poToken);
|
||||
LOGGER.debug("Visitor Data: {}", visitorData);
|
||||
|
||||
if (poToken.length() < 160)
|
||||
LOGGER.warn("There is a high chance that the retrieved PO token will not work. Try on a different network.");
|
||||
|
||||
Web.setPoTokenAndVisitorData(poToken, visitorData);
|
||||
|
||||
interceptorFuture.complete(null);
|
||||
} catch (Exception e) {
|
||||
interceptorFuture.complete(e);
|
||||
}
|
||||
|
||||
return next.execute(req);
|
||||
});
|
||||
|
||||
//
|
||||
// 3. Navigate to YouTube & start the player
|
||||
// 2. Start local HTTP server to host iframe embed & extract PO token from performance logs
|
||||
//
|
||||
HttpServer server = null;
|
||||
try {
|
||||
driver.get("https://www.youtube.com/embed/aqz-KE-bpKQ");
|
||||
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(15));
|
||||
WebElement moviePlayer = driver.findElement(By.cssSelector("#movie_player"));
|
||||
moviePlayer.click();
|
||||
// Start a lightweight local HTTP server to host the YouTube embed in an iframe.
|
||||
// This provides a valid origin/Referer (http://127.0.0.1:PORT) so YouTube does not
|
||||
// reject the embed with Error 153 "Video player configuration error".
|
||||
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext("/embed", exchange -> {
|
||||
String html = "<!DOCTYPE html>" +
|
||||
"<html>" +
|
||||
"<head>" +
|
||||
"<meta name=\"referrer\" content=\"strict-origin-when-cross-origin\">" +
|
||||
"<style>html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#000;}" +
|
||||
"iframe{width:100%;height:100%;border:none;}</style>" +
|
||||
"</head>" +
|
||||
"<body>" +
|
||||
"<iframe id=\"player\" " +
|
||||
"src=\"https://www.youtube-nocookie.com/embed/aqz-KE-bpKQ?autoplay=1&mute=1&enablejsapi=1\" " +
|
||||
"referrerpolicy=\"strict-origin-when-cross-origin\" " +
|
||||
"allow=\"autoplay; encrypted-media; picture-in-picture\" " +
|
||||
"allowfullscreen></iframe>" +
|
||||
"</body>" +
|
||||
"</html>";
|
||||
byte[] bytes = html.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
|
||||
exchange.getResponseHeaders().set("Referrer-Policy", "strict-origin-when-cross-origin");
|
||||
exchange.sendResponseHeaders(200, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
}
|
||||
});
|
||||
server.start();
|
||||
int serverPort = server.getAddress().getPort();
|
||||
|
||||
// First, navigate to youtube.com to inject consent cookies to bypass EU/GDPR consent walls
|
||||
try {
|
||||
// Wait for the network interceptor to tell us if it got the data
|
||||
Exception result = interceptorFuture.get(20, TimeUnit.SECONDS);
|
||||
if (result != null)
|
||||
throw new Exception("Failed to read YouTube player request body", result);
|
||||
} catch (TimeoutException e) {
|
||||
throw new Exception("Timed out waiting for YouTube player request");
|
||||
driver.get("https://www.youtube.com");
|
||||
driver.manage().addCookie(new Cookie.Builder("SOCS", "CAESEwgDEgk0ODE3Nzk3MjQaAmVuIAEaBgiA_LyaBg")
|
||||
.domain(".youtube.com")
|
||||
.path("/")
|
||||
.build());
|
||||
driver.manage().addCookie(new Cookie.Builder("CONSENT", "PENDING+999")
|
||||
.domain(".youtube.com")
|
||||
.path("/")
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("Could not pre-set consent cookies: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Navigate to local embed wrapper page
|
||||
driver.get("http://127.0.0.1:" + serverPort + "/embed");
|
||||
|
||||
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
|
||||
|
||||
// Wait for iframe and switch to it to interact with player
|
||||
try {
|
||||
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(By.id("player")));
|
||||
|
||||
// Dismiss any consent dialog inside the iframe if present
|
||||
try {
|
||||
List<WebElement> consentButtons = driver.findElements(By.xpath(
|
||||
"//button[contains(@aria-label, 'Agree') or contains(@aria-label, 'Accept') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'agree') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'accept') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'alle akzeptieren') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'ich stimme zu') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'tout accepter') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'accepter tout') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'accetta tutto') or " +
|
||||
"contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'aceptar todo')]"
|
||||
));
|
||||
for (WebElement btn : consentButtons) {
|
||||
if (btn.isDisplayed()) {
|
||||
LOGGER.info("Dismissing consent dialog inside iframe...");
|
||||
btn.click();
|
||||
Thread.sleep(1000);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// Try to click player inside iframe to trigger playback and /player request
|
||||
try {
|
||||
WebElement moviePlayer = wait.until(ExpectedConditions.presenceOfElementLocated(
|
||||
By.cssSelector("#movie_player, .ytp-large-play-button, .html5-video-player, video")
|
||||
));
|
||||
moviePlayer.click();
|
||||
} catch (Exception ignored) {
|
||||
((JavascriptExecutor) driver).executeScript(
|
||||
"var p = document.getElementById('movie_player') || document.querySelector('video'); " +
|
||||
"if (p) { if (p.playVideo) p.playVideo(); else if (p.play) p.play(); }"
|
||||
);
|
||||
}
|
||||
|
||||
// Switch back to default context so performance logging and main window operations work
|
||||
driver.switchTo().defaultContent();
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("Could not interact with embed iframe: {}", e.getMessage());
|
||||
try {
|
||||
driver.switchTo().defaultContent();
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// Poll performance logs to capture the /youtubei/v1/player request
|
||||
boolean success = false;
|
||||
long deadline = System.currentTimeMillis() + 25_000;
|
||||
long nextActionTime = System.currentTimeMillis() + 5_000;
|
||||
|
||||
while (System.currentTimeMillis() < deadline && !success) {
|
||||
LogEntries logEntries = driver.manage().logs().get(LogType.PERFORMANCE);
|
||||
for (LogEntry entry : logEntries) {
|
||||
try {
|
||||
JSONObject json = new JSONObject(entry.getMessage());
|
||||
JSONObject message = json.optJSONObject("message");
|
||||
if (message == null) continue;
|
||||
|
||||
String method = message.optString("method");
|
||||
if ("Network.requestWillBeSent".equals(method)) {
|
||||
JSONObject params = message.optJSONObject("params");
|
||||
if (params == null) continue;
|
||||
JSONObject req = params.optJSONObject("request");
|
||||
if (req == null) continue;
|
||||
String url = req.optString("url", "");
|
||||
if (url.contains("/youtubei/v1/player")) {
|
||||
String postData = req.optString("postData", null);
|
||||
|
||||
// In Chrome 130+, request payload may be in postDataEntries as Base64 bytes
|
||||
if ((postData == null || postData.isEmpty()) && req.has("postDataEntries")) {
|
||||
org.json.JSONArray entries = req.optJSONArray("postDataEntries");
|
||||
if (entries != null) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < entries.length(); i++) {
|
||||
JSONObject entryObj = entries.optJSONObject(i);
|
||||
if (entryObj != null && entryObj.has("bytes")) {
|
||||
try {
|
||||
byte[] decoded = java.util.Base64.getDecoder().decode(entryObj.getString("bytes"));
|
||||
sb.append(new String(decoded, StandardCharsets.UTF_8));
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
if (sb.length() > 0) {
|
||||
postData = sb.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postData == null || postData.isEmpty()) continue;
|
||||
|
||||
JSONObject body = new JSONObject(postData);
|
||||
|
||||
String visitorData = null;
|
||||
if (body.has("context") && body.getJSONObject("context").has("client")) {
|
||||
visitorData = body.getJSONObject("context").getJSONObject("client").optString("visitorData", null);
|
||||
}
|
||||
|
||||
String poToken = null;
|
||||
if (body.has("serviceIntegrityDimensions")) {
|
||||
poToken = body.getJSONObject("serviceIntegrityDimensions").optString("poToken", null);
|
||||
}
|
||||
|
||||
if (visitorData != null && poToken != null) {
|
||||
LOGGER.info("Successfully retrieved PO token & visitor data for YouTube playback.");
|
||||
LOGGER.debug("PO token: {}", poToken);
|
||||
LOGGER.debug("Visitor Data: {}", visitorData);
|
||||
|
||||
// Set PO token and visitor data on both Web and WebEmbedded clients
|
||||
Web.setPoTokenAndVisitorData(poToken, visitorData);
|
||||
WebEmbedded.setPoTokenAndVisitorData(poToken, visitorData);
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug("Error parsing performance log entry: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (success) break;
|
||||
|
||||
// If still not captured after a while, try re-triggering play inside iframe
|
||||
if (System.currentTimeMillis() > nextActionTime) {
|
||||
try {
|
||||
driver.switchTo().frame("player");
|
||||
((JavascriptExecutor) driver).executeScript(
|
||||
"var p = document.getElementById('movie_player') || document.querySelector('video'); " +
|
||||
"if (p) { if (p.playVideo) p.playVideo(); else if (p.play) p.play(); }"
|
||||
);
|
||||
driver.switchTo().defaultContent();
|
||||
} catch (Exception ignored) {}
|
||||
nextActionTime = System.currentTimeMillis() + 5_000;
|
||||
}
|
||||
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
saveDebugPage(driver);
|
||||
throw new Exception("Timed out waiting for YouTube player request. Current URL: " + driver.getCurrentUrl());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Failed to obtain PO token for YouTube playback: {}", e.getMessage());
|
||||
LOGGER.debug("Exception:", e);
|
||||
} finally {
|
||||
interceptor.close();
|
||||
// Only quit the browser when not in headless mode. Helps with troubleshooting.
|
||||
if (server != null) {
|
||||
try {
|
||||
server.stop(0);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
// Only quit the browser when in headless mode. Helps with troubleshooting.
|
||||
if (headless)
|
||||
driver.quit();
|
||||
}
|
||||
}
|
||||
|
||||
private static void saveDebugPage(WebDriver driver) {
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String currentUrl = driver.getCurrentUrl();
|
||||
sb.append("<!-- Current URL: ").append(currentUrl).append(" -->\n");
|
||||
sb.append("<!-- MAIN PAGE SOURCE -->\n");
|
||||
try {
|
||||
sb.append(driver.getPageSource()).append("\n\n");
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
try {
|
||||
driver.switchTo().frame("player");
|
||||
sb.append("<!-- IFRAME SOURCE -->\n");
|
||||
sb.append(driver.getPageSource()).append("\n");
|
||||
driver.switchTo().defaultContent();
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
File debugFile = new File("youtube_debug.html");
|
||||
try (FileWriter writer = new FileWriter(debugFile)) {
|
||||
writer.write(sb.toString());
|
||||
}
|
||||
LOGGER.info("Saved YouTube debug page source to {} (Current URL: {})", debugFile.getAbsolutePath(), currentUrl);
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to save YouTube debug page source: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user