Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ nb*.xml
/.cursor/
dependency-reduced-pom.xml
*.lock
*.dll
*.so
*.dylib
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,12 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Source: https://github.com/java-native-access/jna -->
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.13.0</version>
</dependency>

<!-- Testing Dependencies -->
<!-- Version managed by junit-bom in dependencyManagement -->
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/com/jagrosh/jmusicbot/BotConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -456,6 +457,10 @@ public boolean useYouTubeOauth() {
return useYouTubeOauth;
}

public boolean generatePOT() {
return generatePOT;
}

public String getMaxTime() {
return TimeUtil.formatTime(maxSeconds * 1000);
}
Expand Down
35 changes: 34 additions & 1 deletion src/main/java/com/jagrosh/jmusicbot/audio/AudioSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -75,6 +79,7 @@ public enum AudioSource
(manager, config) -> {
YoutubeAudioSourceManager yt = setupYoutubeAudioSourceManager(
config.useYouTubeOauth(),
config.generatePOT(),
config.getMaxYTPlaylistPages()
);
manager.registerSourceManager(yt);
Expand Down Expand Up @@ -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);

Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)"),
Expand Down
91 changes: 91 additions & 0 deletions src/main/java/com/jagrosh/jmusicbot/utils/BgUtil.java
Original file line number Diff line number Diff line change
@@ -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 <a href="https://github.com/jim60105/bgutil-ytdlp-pot-provider-rs/blob/master/docs/ffi-guide.md">BgUtils POT Provider</a>.
*/
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();
}
}
5 changes: 5 additions & 0 deletions src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -191,15 +191,15 @@ 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");
}

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;
}
Expand Down