diff --git a/.gitignore b/.gitignore
index fc25521ac..1d1c8aee6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,6 @@ nb*.xml
/.cursor/
dependency-reduced-pom.xml
*.lock
+*.dll
+*.so
+*.dylib
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index ca431babf..4814bca5a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -316,6 +316,12 @@
com.fasterxml.jackson.core
jackson-databind
+
+
+ net.java.dev.jna
+ jna
+ 5.13.0
+
diff --git a/src/main/java/com/jagrosh/jmusicbot/BotConfig.java b/src/main/java/com/jagrosh/jmusicbot/BotConfig.java
index 76eac9c0a..277743f05 100644
--- a/src/main/java/com/jagrosh/jmusicbot/BotConfig.java
+++ b/src/main/java/com/jagrosh/jmusicbot/BotConfig.java
@@ -58,7 +58,7 @@ public class BotConfig {
private String token, prefix, altprefix, helpWord, playlistsFolder, logLevel,
successEmoji, warningEmoji, errorEmoji, loadingEmoji, searchingEmoji,
evalEngine;
- private boolean stayInChannel, songInGame, npImages, updatealerts, useEval, dbots, useYouTubeOauth;
+ private boolean stayInChannel, songInGame, npImages, updatealerts, useEval, dbots, useYouTubeOauth, generatePOT;
private long owner, maxSeconds, aloneTimeUntilStop;
private int maxYTPlaylistPages;
private double skipratio;
@@ -259,6 +259,7 @@ private void loadConfigValues(Config config, Config migratedUserConfig) {
maxSeconds = MAX_SECONDS.getLong(config);
maxYTPlaylistPages = MAX_YT_PLAYLIST_PAGES.getInt(config);
useYouTubeOauth = USE_YOUTUBE_OAUTH.getBoolean(config);
+ generatePOT = GENERATE_POT.getBoolean(config);
aloneTimeUntilStop = ALONE_TIME_UNTIL_STOP.getLong(config);
playlistsFolder = PLAYLISTS_FOLDER.getString(config);
aliases = ALIASES.getConfig(config);
@@ -456,6 +457,10 @@ public boolean useYouTubeOauth() {
return useYouTubeOauth;
}
+ public boolean generatePOT() {
+ return generatePOT;
+ }
+
public String getMaxTime() {
return TimeUtil.formatTime(maxSeconds * 1000);
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/audio/AudioSource.java b/src/main/java/com/jagrosh/jmusicbot/audio/AudioSource.java
index b3191b746..dee234e44 100644
--- a/src/main/java/com/jagrosh/jmusicbot/audio/AudioSource.java
+++ b/src/main/java/com/jagrosh/jmusicbot/audio/AudioSource.java
@@ -19,9 +19,11 @@
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.util.Arrays;
+import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
+import com.jagrosh.jmusicbot.utils.BgUtil;
import com.sedmelluq.discord.lavaplayer.container.MediaContainerRegistry;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers;
@@ -34,6 +36,7 @@
import com.sedmelluq.discord.lavaplayer.source.twitch.TwitchStreamAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoAudioSourceManager;
import dev.lavalink.youtube.YoutubeAudioSourceManager;
+import dev.lavalink.youtube.YoutubeSource;
import dev.lavalink.youtube.YoutubeSourceOptions;
import dev.lavalink.youtube.clients.AndroidVr;
import dev.lavalink.youtube.clients.ClientOptions;
@@ -42,6 +45,7 @@
import dev.lavalink.youtube.clients.TvHtml5Embedded;
import dev.lavalink.youtube.clients.Web;
import dev.lavalink.youtube.clients.skeleton.Client;
+import dev.lavalink.youtube.http.YoutubeAccessTokenTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -75,6 +79,7 @@ public enum AudioSource
(manager, config) -> {
YoutubeAudioSourceManager yt = setupYoutubeAudioSourceManager(
config.useYouTubeOauth(),
+ config.generatePOT(),
config.getMaxYTPlaylistPages()
);
manager.registerSourceManager(yt);
@@ -228,10 +233,11 @@ public void register(DefaultAudioPlayerManager manager, BotConfig config)
* Sets up and configures a YouTube audio source manager.
*
* @param useOauth whether to use OAuth2 authentication
+ * @param generatePOT whether to use a POT provider
* @param maxYTPlaylistPages maximum number of playlist pages to load
* @return the configured YouTube audio source manager
*/
- private static YoutubeAudioSourceManager setupYoutubeAudioSourceManager(boolean useOauth, int maxYTPlaylistPages)
+ private static YoutubeAudioSourceManager setupYoutubeAudioSourceManager(boolean useOauth, boolean generatePOT, int maxYTPlaylistPages)
{
final Logger logger = LoggerFactory.getLogger(AudioSource.class);
@@ -245,6 +251,33 @@ private static YoutubeAudioSourceManager setupYoutubeAudioSourceManager(boolean
{
applyOAuth(yt, logger);
}
+
+ if (generatePOT)
+ {
+ yt.getContextFilter().setTokenTracker(new YoutubeAccessTokenTracker(yt.getHttpInterfaceManager()) {
+ private static final Logger log = LoggerFactory.getLogger(YoutubeAccessTokenTracker.class);
+ public String lastVisitorId;
+
+ @Override
+ public String getVisitorId() {
+ String visitorId = super.getVisitorId();
+ if (!Objects.equals(visitorId, lastVisitorId)) {
+ lastVisitorId = visitorId;
+ if (BgUtil.isPresent()) {
+ BgUtil.PotResult potResult = BgUtil.parsePOT(BgUtil.generateJson(visitorId));
+ if (potResult.poToken() != null) {
+ YoutubeSource.setPoTokenAndVisitorData(potResult.poToken(), potResult.contentBinding());
+ log.info("PoToken generated: {}", potResult.poToken());
+ }
+ } else {
+ log.warn("Couldn't use POT provider. The library may be missing.");
+ }
+ }
+ return visitorId;
+ }
+ });
+ }
+
return yt;
}
diff --git a/src/main/java/com/jagrosh/jmusicbot/config/model/ConfigOption.java b/src/main/java/com/jagrosh/jmusicbot/config/model/ConfigOption.java
index ccacbada5..727beac84 100644
--- a/src/main/java/com/jagrosh/jmusicbot/config/model/ConfigOption.java
+++ b/src/main/java/com/jagrosh/jmusicbot/config/model/ConfigOption.java
@@ -54,6 +54,7 @@ public enum ConfigOption {
UPDATE_ALERTS("updates.alerts", ConfigType.BOOLEAN, false, "Whether to alert owner about updates"),
USE_EVAL("dangerous.eval", ConfigType.BOOLEAN, false, "Whether to enable eval command (DANGEROUS)"),
USE_YOUTUBE_OAUTH("playback.youtube.useOAuth", ConfigType.BOOLEAN, false, "Whether to use YouTube OAuth2 for playback"),
+ GENERATE_POT("playback.youtube.generatePOT", ConfigType.BOOLEAN, false, "Whether to generate PoToken through BGUtil"),
// Numeric options
MAX_SECONDS("playback.maxTrackSeconds", ConfigType.LONG, false, "Maximum track length in seconds (0 = no limit)"),
diff --git a/src/main/java/com/jagrosh/jmusicbot/utils/BgUtil.java b/src/main/java/com/jagrosh/jmusicbot/utils/BgUtil.java
new file mode 100644
index 000000000..a53e32dff
--- /dev/null
+++ b/src/main/java/com/jagrosh/jmusicbot/utils/BgUtil.java
@@ -0,0 +1,91 @@
+package com.jagrosh.jmusicbot.utils;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.jna.Library;
+import com.sun.jna.Native;
+import com.sun.jna.Pointer;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+/**
+ * JNA binding for BgUtils POT Provider.
+ */
+public interface BgUtil extends Library {
+ Pointer ffi_generate(String content_binding, String proxy, boolean bypass_cache, String source_address, boolean disable_tls);
+ void ffi_free_string(Pointer ptr);
+
+ class Factory {
+ static BgUtil INSTANCE;
+ static Boolean isPresent;
+
+ private static boolean tryInstantiate(String path) {
+ try {
+ if (Files.exists(Paths.get(path))) {
+ INSTANCE = Native.load(path, BgUtil.class);
+ isPresent = true;
+ return false;
+ }
+ } catch (Exception _) {}
+ return true;
+ }
+
+ public static BgUtil getInstance() {
+ if (INSTANCE == null && isPresent == null) {
+ isPresent = false;
+
+ String libbase = "./libbgutil_ytdlp_pot_provider";
+ String libext = "-linux-.so";
+ String osname = System.getProperty("os.name").toLowerCase();
+ if (osname.contains("win")) {
+ libbase = "./bgutil_ytdlp_pot_provider";
+ libext = "-windows-.dll";
+ } else if (osname.contains("mac")) {
+ libext = "-macos-.dylib";
+ }
+
+ String libpath = libbase + libext.replace(".", System.getProperty("os.arch") + ".");
+ if (tryInstantiate(libpath)) {
+ // x86_64 fallback
+ libpath = libbase + libext.replace(".", "x86_64.");
+ if (tryInstantiate(libpath)) {
+ // generic fallback
+ libpath = libbase + libext.substring(libext.indexOf("."));
+ tryInstantiate(libpath);
+ }
+ }
+ }
+ return INSTANCE;
+ }
+ }
+
+ static boolean isPresent() {
+ Factory.getInstance();
+ return Boolean.TRUE.equals(Factory.isPresent);
+ }
+
+ static String generateJson(String content_binder) {
+ Pointer ptr = Factory.getInstance().ffi_generate(content_binder, null, false, null, false);
+ String result = ptr.getString(0);
+ Factory.getInstance().ffi_free_string(ptr);
+ return result;
+ }
+
+ static PotResult parsePOT(String json) {
+ PotResult result = PotResult.NULL;
+ try {
+ JsonNode node = PotResult.mapper.readTree(json);
+ if (!node.isEmpty()) {
+ result = new PotResult(node.get("poToken").asText(), node.get("contentBinding").asText(), node.get("expiresAt").asText());
+ }
+ } catch (JsonProcessingException _) {}
+ return result;
+ }
+
+ record PotResult(String poToken, String contentBinding, String expiresAt) {
+ public static final PotResult NULL = new PotResult(null, null, "1970-01-01T00:00:00.000000000Z");
+ public static final ObjectMapper mapper = new ObjectMapper();
+ }
+}
diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf
index c99e1e667..31eee5341 100644
--- a/src/main/resources/reference.conf
+++ b/src/main/resources/reference.conf
@@ -45,6 +45,11 @@ playback {
# When enabled, a code is printed in console/DM'd to the owner for https://www.google.com/device
# Only needs to be done once (refresh token persists unless changed).
useOAuth = false
+
+ # Alternative to YouTube OAuth to avoid "Video unavailable" issues.
+ # When enabled, the bot searches for a bgutil_pot_provider-{os}-{arch}.dll/.so/.dylib file to generate a PoToken.
+ # The file search always fallbacks to bgutil_ytdlp_pot_provider.dll/.so/.dylib
+ generatePOT = false
}
# Audio sources toggles.
diff --git a/src/test/java/com/jagrosh/jmusicbot/unit/AudioSourceOAuthTest.java b/src/test/java/com/jagrosh/jmusicbot/unit/AudioSourceOAuthTest.java
index 2902be3f1..699e5eebc 100644
--- a/src/test/java/com/jagrosh/jmusicbot/unit/AudioSourceOAuthTest.java
+++ b/src/test/java/com/jagrosh/jmusicbot/unit/AudioSourceOAuthTest.java
@@ -178,7 +178,7 @@ void oauthFlowTriggeredWhenUseOauthIsTrue() throws Exception {
// but we can verify the method completes without throwing
assertDoesNotThrow(() -> {
YoutubeAudioSourceManager result = (YoutubeAudioSourceManager)
- setupMethod.invoke(null, true, 10);
+ setupMethod.invoke(null, true, false, 10);
assertNotNull(result, "Should return a configured YoutubeAudioSourceManager");
}, "setupYoutubeAudioSourceManager should complete without throwing when OAuth is enabled");
}
@@ -191,7 +191,7 @@ void oauthFlowNotTriggeredWhenUseOauthIsFalse() throws Exception {
// Call with useOauth=false - this should NOT trigger OAuth
assertDoesNotThrow(() -> {
YoutubeAudioSourceManager result = (YoutubeAudioSourceManager)
- setupMethod.invoke(null, false, 10);
+ setupMethod.invoke(null, false, false, 10);
assertNotNull(result, "Should return a configured YoutubeAudioSourceManager");
}, "setupYoutubeAudioSourceManager should complete without OAuth when disabled");
}
@@ -199,7 +199,7 @@ void oauthFlowNotTriggeredWhenUseOauthIsFalse() throws Exception {
private Method getSetupYoutubeAudioSourceManagerMethod() throws NoSuchMethodException {
Class> audioSourceClass = com.jagrosh.jmusicbot.audio.AudioSource.class;
Method method = audioSourceClass.getDeclaredMethod("setupYoutubeAudioSourceManager",
- boolean.class, int.class);
+ boolean.class, boolean.class, int.class);
method.setAccessible(true);
return method;
}