Add configurable music search engine support
Introduced a configurable `MUSIC_SEARCH_ENGINE` option for determining the default search engine used for non-URL queries (YouTube, YouTube Music, or SoundCloud). Updated `SourceFinder` and related components to support this feature, enhanced query resolution logic, and improved flexibility in music searches. Added `SourceFinderTest` for thorough unit test coverage, ensuring correct behavior across search engines.
This commit is contained in:
1 parent
22621e3912
commit
6d62a38b0b
6 files changed
+112
-8
No files matched your search
@@ -106,7 +106,10 @@ public class ConfigureCommand extends AdminCommand {
|
||||
new SubcommandData("locale", "Preferred locale for the bot's replies on this server")
|
||||
.addOption(OptionType.STRING, "locale", "Locale code, e.g. en, fr", true),
|
||||
new SubcommandData("timezone", "Timezone used to display timestamps on this server")
|
||||
.addOption(OptionType.STRING, "timezone", "Timezone id, e.g. UTC, Europe/Paris", true)
|
||||
.addOption(OptionType.STRING, "timezone", "Timezone id, e.g. UTC, Europe/Paris", true),
|
||||
new SubcommandData("music-search-engine", "Default search engine used when a play query is not a direct url")
|
||||
.addOptions(new OptionData(OptionType.STRING, "engine", "Search engine", true)
|
||||
.addChoice("YouTube", "YOUTUBE").addChoice("YouTube Music", "YTMUSIC").addChoice("SoundCloud", "SOUNDCLOUD"))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -143,6 +146,7 @@ public class ConfigureCommand extends AdminCommand {
|
||||
case "log-level" -> configureStringSetting(context, settingService, GSettings.LOG_LEVEL, "level", "Log level reset to default", "Log level is now %s");
|
||||
case "locale" -> configureStringSetting(context, settingService, GSettings.LOCALE, "locale", "Locale reset to default", "Locale is now %s");
|
||||
case "timezone" -> configureStringSetting(context, settingService, GSettings.TIMEZONE, "timezone", "Timezone reset to default", "Timezone is now %s");
|
||||
case "music-search-engine" -> configureStringSetting(context, settingService, GSettings.MUSIC_SEARCH_ENGINE, "engine", "Default search engine reset to YouTube", "Default search engine is now %s");
|
||||
case "show-all" -> showAll(context, settingService);
|
||||
default -> CommandExecutionResult.error("Unknown subcommand: " + sub);
|
||||
};
|
||||
|
||||
@@ -39,7 +39,9 @@ public enum GSettings {
|
||||
/** Preferred locale for the bot's replies on this guild. */
|
||||
LOCALE("LOCALE", "en", "Preferred locale for the bot's replies on this guild", SettingType.STRING),
|
||||
/** Timezone used to display timestamps for this guild. */
|
||||
TIMEZONE("TIMEZONE", "UTC", "Timezone used to display timestamps for this guild", SettingType.STRING);
|
||||
TIMEZONE("TIMEZONE", "UTC", "Timezone used to display timestamps for this guild", SettingType.STRING),
|
||||
/** Default search engine used when a play query is not a direct url (YOUTUBE, YTMUSIC or SOUNDCLOUD). */
|
||||
MUSIC_SEARCH_ENGINE("MUSIC_SEARCH_ENGINE", "YOUTUBE", "Default search engine used when a play query is not a direct url", SettingType.ENUM, "YOUTUBE", "YTMUSIC", "SOUNDCLOUD");
|
||||
|
||||
private final String name;
|
||||
private final String defaultValue;
|
||||
|
||||
@@ -196,13 +196,14 @@ public class MusicManager implements MusicManagerCallback {
|
||||
return;
|
||||
}
|
||||
|
||||
SourceFinder.AudioSourceType type = SourceFinder.getSource(query);
|
||||
SourceFinder.SearchEngine searchEngine = parseSearchEngine(guildService);
|
||||
SourceFinder.AudioSourceType type = SourceFinder.getSource(query, searchEngine);
|
||||
if (type == SourceFinder.AudioSourceType.Udio) {
|
||||
context.reply("Udio playback is temporarily unsupported since the migration to Lavalink");
|
||||
return;
|
||||
}
|
||||
|
||||
String identifier = SourceFinder.getIdentifier(query);
|
||||
String identifier = SourceFinder.getIdentifier(query, searchEngine);
|
||||
if (identifier == null) {
|
||||
context.reply("No source found to play the given url");
|
||||
return;
|
||||
@@ -295,4 +296,14 @@ public class MusicManager implements MusicManagerCallback {
|
||||
return DEFAULT_VOLUME;
|
||||
}
|
||||
}
|
||||
|
||||
private static SourceFinder.SearchEngine parseSearchEngine(GuildService guildService) {
|
||||
String value = guildService.getGuildSettingService().getOrDefault(GSettings.MUSIC_SEARCH_ENGINE);
|
||||
try {
|
||||
return SourceFinder.SearchEngine.valueOf(value);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Invalid MUSIC_SEARCH_ENGINE setting '{}', falling back to default", value, e);
|
||||
return SourceFinder.SearchEngine.valueOf(GSettings.MUSIC_SEARCH_ENGINE.getDefaultValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,24 @@ public class SourceFinder {
|
||||
Bandcamp,
|
||||
Vimeo,
|
||||
YTSearch,
|
||||
YTMSearch,
|
||||
SCSearch,
|
||||
Udio,
|
||||
DirectLinkMp3,
|
||||
Nothing
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Default search engine used for non-url queries, configurable per guild via
|
||||
* {@code GSettings.MUSIC_SEARCH_ENGINE}.
|
||||
*/
|
||||
public enum SearchEngine {
|
||||
YOUTUBE,
|
||||
YTMUSIC,
|
||||
SOUNDCLOUD
|
||||
}
|
||||
|
||||
private static final Pattern youtubePattern = Pattern.compile("^(?:https?://)?(?:www\\.)?(?:youtube\\.com|youtu\\.be)/.*?(?:embed|e|v|watch.*?v=)?/?([-_a-zA-Z0-9]{10,})?(?:&?index=\\d+)?(?>(?:playlist\\?|&)?list=([^#\\\\&?]{12,}))?", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern youtubeMusicPattern = Pattern.compile("^(?:https?://)?(?:www\\.)?music\\.(?:youtube\\.com|youtu\\.be)/.*?(?:embed|e|v|watch.*?v=)?/?([-_a-zA-Z0-9]{10,})?(?:&?index=\\d+)?(?>(?:playlist\\?|&)?list=([^#\\\\&?]{12,}))?", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern soundcloudPattern = Pattern.compile("^(?:https?://)?(?:www\\.)?(?:m\\.)?soundcloud\\.com/([a-zA-Z0-9-_]+)/([a-zA-Z0-9-_]+)(?:\\\\?.*|)$", Pattern.CASE_INSENSITIVE);
|
||||
@@ -31,6 +43,14 @@ public class SourceFinder {
|
||||
private static final Pattern mp3Pattern = Pattern.compile("^https?://(?:.*)(?:.mp3)$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
public static AudioSourceType getSource(String url) {
|
||||
return getSource(url, SearchEngine.YOUTUBE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the source type of the given query. For non-url queries, {@code searchEngine}
|
||||
* selects which search source is used (YouTube, YouTube Music or SoundCloud).
|
||||
*/
|
||||
public static AudioSourceType getSource(String url, SearchEngine searchEngine) {
|
||||
Matcher youtubeMatcher = youtubePattern.matcher(url);
|
||||
Matcher youtubeMusicMatcher = youtubeMusicPattern.matcher(url);
|
||||
Matcher soundcloudMatcher = soundcloudPattern.matcher(url);
|
||||
@@ -58,18 +78,33 @@ public class SourceFinder {
|
||||
}
|
||||
|
||||
if (!(url.startsWith("http://") || url.startsWith("https://"))) {
|
||||
return AudioSourceType.YTSearch;
|
||||
return switch (searchEngine) {
|
||||
case YTMUSIC -> AudioSourceType.YTMSearch;
|
||||
case SOUNDCLOUD -> AudioSourceType.SCSearch;
|
||||
default -> AudioSourceType.YTSearch;
|
||||
};
|
||||
}
|
||||
|
||||
return AudioSourceType.Nothing;
|
||||
}
|
||||
|
||||
public static String getIdentifier(String url) {
|
||||
return getIdentifier(url, SearchEngine.YOUTUBE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the identifier sent to Lavalink. Non-url queries are prefixed with the search
|
||||
* identifier matching {@code searchEngine} ({@code ytsearch:}, {@code ytmsearch:} or
|
||||
* {@code scsearch:}).
|
||||
*/
|
||||
public static String getIdentifier(String url, SearchEngine searchEngine) {
|
||||
String identifier;
|
||||
|
||||
identifier = switch (getSource(url)) {
|
||||
identifier = switch (getSource(url, searchEngine)) {
|
||||
case Youtube, YTMusic, Soundcloud, DirectLinkMp3 -> url;
|
||||
case YTSearch -> "ytsearch:" + url;
|
||||
case YTMSearch -> "ytmsearch:" + url;
|
||||
case SCSearch -> "scsearch:" + url;
|
||||
// Udio is not resolvable by the Lavalink node until the source is ported to a
|
||||
// server-side plugin, see docs/reference/udio-lavalink-plugin.
|
||||
default -> null;
|
||||
|
||||
@@ -60,7 +60,8 @@ public class MessageFunction {
|
||||
String url = info.getIdentifier();
|
||||
String thumbnail = info.getArtworkUrl();
|
||||
|
||||
if (type == SourceFinder.AudioSourceType.Youtube || type == SourceFinder.AudioSourceType.YTSearch || type == SourceFinder.AudioSourceType.YTMusic) {
|
||||
if (type == SourceFinder.AudioSourceType.Youtube || type == SourceFinder.AudioSourceType.YTSearch
|
||||
|| type == SourceFinder.AudioSourceType.YTMusic || type == SourceFinder.AudioSourceType.YTMSearch) {
|
||||
url = String.format("https://youtu.be/%s", info.getIdentifier());
|
||||
thumbnail = String.format("https://i3.ytimg.com/vi/%s/hqdefault.jpg", info.getIdentifier());
|
||||
}
|
||||
@@ -88,7 +89,7 @@ public class MessageFunction {
|
||||
thumbnail = String.format("https://i3.ytimg.com/vi/%s/hqdefault.jpg", tracks.get(0).getInfo().getIdentifier());
|
||||
}
|
||||
|
||||
if (type == SourceFinder.AudioSourceType.YTMusic) {
|
||||
if (type == SourceFinder.AudioSourceType.YTMusic || type == SourceFinder.AudioSourceType.YTMSearch) {
|
||||
thumbnail = String.format("https://i3.ytimg.com/vi/%s/hqdefault.jpg", tracks.get(0).getInfo().getIdentifier());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package me.bobert.netneutrality.music;
|
||||
/*
|
||||
* Copyright (c) anthorob
|
||||
*
|
||||
* https://discord.gg/qUa6zNFZsd
|
||||
*/
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class SourceFinderTest {
|
||||
|
||||
@Test
|
||||
public void defaultSearchUsesYoutube() {
|
||||
assertEquals(SourceFinder.AudioSourceType.YTSearch, SourceFinder.getSource("some song"));
|
||||
assertEquals("ytsearch:some song", SourceFinder.getIdentifier("some song"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void youtubeSearchEngineProducesYtSearchPrefix() {
|
||||
assertEquals(SourceFinder.AudioSourceType.YTSearch, SourceFinder.getSource("some song", SourceFinder.SearchEngine.YOUTUBE));
|
||||
assertEquals("ytsearch:some song", SourceFinder.getIdentifier("some song", SourceFinder.SearchEngine.YOUTUBE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void ytMusicSearchEngineProducesYtmSearchPrefix() {
|
||||
assertEquals(SourceFinder.AudioSourceType.YTMSearch, SourceFinder.getSource("some song", SourceFinder.SearchEngine.YTMUSIC));
|
||||
assertEquals("ytmsearch:some song", SourceFinder.getIdentifier("some song", SourceFinder.SearchEngine.YTMUSIC));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void soundcloudSearchEngineProducesScSearchPrefix() {
|
||||
assertEquals(SourceFinder.AudioSourceType.SCSearch, SourceFinder.getSource("some song", SourceFinder.SearchEngine.SOUNDCLOUD));
|
||||
assertEquals("scsearch:some song", SourceFinder.getIdentifier("some song", SourceFinder.SearchEngine.SOUNDCLOUD));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directUrlsIgnoreSearchEngine() {
|
||||
String url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
|
||||
assertEquals(SourceFinder.AudioSourceType.Youtube, SourceFinder.getSource(url, SourceFinder.SearchEngine.SOUNDCLOUD));
|
||||
assertEquals(url, SourceFinder.getIdentifier(url, SourceFinder.SearchEngine.SOUNDCLOUD));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void soundcloudUrlIsDetectedRegardlessOfSearchEngine() {
|
||||
String url = "https://soundcloud.com/artist/track";
|
||||
assertEquals(SourceFinder.AudioSourceType.Soundcloud, SourceFinder.getSource(url, SourceFinder.SearchEngine.YTMUSIC));
|
||||
assertEquals(url, SourceFinder.getIdentifier(url, SourceFinder.SearchEngine.YTMUSIC));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user