From f79ab8984a13de9fd178e97275d10cf6cc329fe4 Mon Sep 17 00:00:00 2001 From: anthorob Date: Thu, 9 May 2024 15:32:43 -0400 Subject: [PATCH] Add Udio music source --- .../netneutrality/music/MusicManager.java | 3 + .../netneutrality/music/SourceFinder.java | 10 +- .../music/source/UdioAudioSourceManager.java | 159 ++++++++++++++++++ .../music/source/UdioAudioTrack.java | 90 ++++++++++ 4 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 src/main/java/netneutrality/music/source/UdioAudioSourceManager.java create mode 100644 src/main/java/netneutrality/music/source/UdioAudioTrack.java diff --git a/src/main/java/netneutrality/music/MusicManager.java b/src/main/java/netneutrality/music/MusicManager.java index c5df1a3..cde9d79 100644 --- a/src/main/java/netneutrality/music/MusicManager.java +++ b/src/main/java/netneutrality/music/MusicManager.java @@ -11,6 +11,7 @@ import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager; +import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoAudioSourceManager; import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager; import com.sedmelluq.discord.lavaplayer.track.AudioTrack; import net.dv8tion.jda.api.entities.Guild; @@ -24,6 +25,7 @@ import netneutrality.core.context.CommandContext; import netneutrality.db.GSettings; import netneutrality.exceptions.CommandExecutionException; import netneutrality.music.loadable.LoadableSong; +import netneutrality.music.source.UdioAudioSourceManager; import netneutrality.thread.MusicPlayerAutoDisconnectThread; import netneutrality.thread.ThreadManager; import netneutrality.utils.Settings; @@ -79,6 +81,7 @@ public class MusicManager { playerManager.registerSourceManager(youtubeSource); playerManager.registerSourceManager(SoundCloudAudioSourceManager.createDefault()); + playerManager.registerSourceManager(new UdioAudioSourceManager()); } public boolean isNotConnectedTo(VoiceChannel vChannel) { diff --git a/src/main/java/netneutrality/music/SourceFinder.java b/src/main/java/netneutrality/music/SourceFinder.java index b78073d..eb64383 100644 --- a/src/main/java/netneutrality/music/SourceFinder.java +++ b/src/main/java/netneutrality/music/SourceFinder.java @@ -19,6 +19,7 @@ public class SourceFinder { Bandcamp, Vimeo, YTSearch, + Udio, Nothing } @@ -26,12 +27,14 @@ public class SourceFinder { 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); - private static Matcher twitchMatcher; + private static final Pattern udioPattern = Pattern.compile("^https://www.udio.com/songs/[0-9A-Za-z]+$", Pattern.CASE_INSENSITIVE); + public static AudioSourceType getSource(String url) { Matcher youtubeMatcher = youtubePattern.matcher(url); Matcher youtubeMusicMatcher = youtubeMusicPattern.matcher(url); Matcher soundcloudMatcher = soundcloudPattern.matcher(url); + Matcher udioMatcher = udioPattern.matcher(url); if (youtubeMatcher.find()) return AudioSourceType.Youtube; @@ -42,6 +45,9 @@ public class SourceFinder { if (soundcloudMatcher.find()) return AudioSourceType.Soundcloud; + if (udioMatcher.find()) + return AudioSourceType.Udio; + if (!(url.startsWith("http://") || url.startsWith("https://"))) return AudioSourceType.YTSearch; @@ -52,7 +58,7 @@ public class SourceFinder { String identifier; identifier = switch (getSource(url)) { - case Youtube, YTMusic, Soundcloud -> url; + case Youtube, YTMusic, Soundcloud, Udio -> url; case YTSearch -> "ytsearch:" + url; default -> null; }; diff --git a/src/main/java/netneutrality/music/source/UdioAudioSourceManager.java b/src/main/java/netneutrality/music/source/UdioAudioSourceManager.java new file mode 100644 index 0000000..2c191cb --- /dev/null +++ b/src/main/java/netneutrality/music/source/UdioAudioSourceManager.java @@ -0,0 +1,159 @@ +package netneutrality.music.source;/* + * Copyright (c) anthorob + * + * https://xtremcommunity.com/ + * https://discord.gg/CdFcyQ + */ + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager; +import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools; +import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterfaceManager; +import com.sedmelluq.discord.lavaplayer.track.AudioItem; +import com.sedmelluq.discord.lavaplayer.track.AudioReference; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.HttpClientBuilder; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.regex.Pattern; + +import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS; + +public class UdioAudioSourceManager implements AudioSourceManager, HttpConfigurable { + + private static final String TRACK_URL_REGEX = "^https://www.udio.com/songs/[0-9A-Za-z]+$"; + private static final Pattern trackUrlPattern = Pattern.compile(TRACK_URL_REGEX); + + private final HttpInterfaceManager httpInterfaceManager; + + public UdioAudioSourceManager() { + httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager(); + } + + public HttpInterface getHttpInterface() { + return httpInterfaceManager.getInterface(); + } + + @Override + public String getSourceName() { + return "udio"; + } + + @Override + public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) { + // start = "og:audio:url\",\"content\":\" + // end = \"}], + + if (!trackUrlPattern.matcher(reference.identifier).matches()) { + return null; + } + + try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) { + return loadFromTrackPage(httpInterface, reference.identifier); + } catch (IOException e) { + throw new FriendlyException("Loading Udio track information failed.", SUSPICIOUS, e); + } + + } + + private AudioItem loadFromTrackPage(HttpInterface httpInterface, String trackUrl) throws IOException { + + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackUrl))) { + int statusCode = response.getStatusLine().getStatusCode(); + + if (statusCode == HttpStatus.SC_NOT_FOUND) { + return AudioReference.NO_TRACK; + } else if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new FriendlyException("Server responded with an error.", SUSPICIOUS, + new IllegalStateException("Response code is " + statusCode)); + } + + return loadTrackFromPageContent(trackUrl, IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); + } + } + + private AudioTrack loadTrackFromPageContent(String trackUrl, String content) throws IOException { + JsonBrowser config = loadConfigJsonFromPageContent(content); + + if (config == null) { + throw new FriendlyException("Track information not found on the page.", SUSPICIOUS, null); + } + + JsonBrowser trackInfo; + try { + trackInfo = config.index(3).get("children").index(3).get("children").index(3).get("track"); + } catch (NullPointerException e) { + throw new FriendlyException("Track information not found on the page.", SUSPICIOUS, null); + } + + return new UdioAudioTrack(new AudioTrackInfo( + trackInfo.get("title").text(), + trackInfo.get("artist").text(), + (long) (trackInfo.get("duration").as(Double.class) * 1000.0), + trackUrl, + false, + trackUrl + ), this); + } + + JsonBrowser loadConfigJsonFromPageContent(String content) throws IOException { + String configText = DataFormatTools.extractBetween(content, "self.__next_f.push([1,\"8:", "\\n"); + System.out.println(configText); + + if (configText != null) { + configText = configText.replace("\\", ""); + System.out.println(configText); + + return JsonBrowser.parse(configText); + } + + return null; + } + + @Override + public boolean isTrackEncodable(AudioTrack track) { + return true; + } + + @Override + public void encodeTrack(AudioTrack track, DataOutput output) throws IOException { + + } + + @Override + public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException { + return new UdioAudioTrack(trackInfo, this); + } + + @Override + public void shutdown() { + ExceptionTools.closeWithWarnings(httpInterfaceManager); + } + + @Override + public void configureRequests(Function configurator) { + httpInterfaceManager.configureRequests(configurator); + } + + @Override + public void configureBuilder(Consumer configurator) { + httpInterfaceManager.configureBuilder(configurator); + } +} diff --git a/src/main/java/netneutrality/music/source/UdioAudioTrack.java b/src/main/java/netneutrality/music/source/UdioAudioTrack.java new file mode 100644 index 0000000..e44f97f --- /dev/null +++ b/src/main/java/netneutrality/music/source/UdioAudioTrack.java @@ -0,0 +1,90 @@ +package netneutrality.music.source;/* + * Copyright (c) anthorob + * + * https://xtremcommunity.com/ + * https://discord.gg/CdFcyQ + */ + + +import com.sedmelluq.discord.lavaplayer.container.mp3.Mp3AudioTrack; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools; +import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface; +import com.sedmelluq.discord.lavaplayer.tools.io.PersistentHttpStream; +import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo; +import com.sedmelluq.discord.lavaplayer.track.DelegatedAudioTrack; +import com.sedmelluq.discord.lavaplayer.track.playback.LocalAudioTrackExecutor; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; + +import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS; + +public class UdioAudioTrack extends DelegatedAudioTrack { + + private static final Logger log = LoggerFactory.getLogger(UdioAudioTrack.class); + + private final UdioAudioSourceManager sourceManager; + + /** + * @param trackInfo Track info + * @param sourceManager Source manager which was used to find this track + */ + public UdioAudioTrack(AudioTrackInfo trackInfo, UdioAudioSourceManager sourceManager) { + super(trackInfo); + + this.sourceManager = sourceManager; + } + + @Override + public void process(LocalAudioTrackExecutor localExecutor) throws Exception { + + + try (HttpInterface httpInterface = sourceManager.getHttpInterface()) { + String playbackUrl = loadPlaybackUrl(httpInterface); + + log.debug("Starting Udio track from URL: {}", playbackUrl); + + try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(playbackUrl), null)) { + processDelegate(new Mp3AudioTrack(trackInfo, stream), localExecutor); + } + } + } + + private String loadPlaybackUrl(HttpInterface httpInterface) throws IOException { + JsonBrowser config = loadPlayerConfig(httpInterface); + + if (config == null) { + throw new FriendlyException("Track information not present on the page.", SUSPICIOUS, null); + } + + JsonBrowser trackInfo; + try { + trackInfo = config.index(3).get("children").index(3).get("children").index(3).get("track"); + } catch (NullPointerException e) { + throw new FriendlyException("Track information not found on the page.", SUSPICIOUS, null); + } + + return trackInfo.get("song_path").text(); + } + + private JsonBrowser loadPlayerConfig(HttpInterface httpInterface) throws IOException { + try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackInfo.identifier))) { + int statusCode = response.getStatusLine().getStatusCode(); + + if (!HttpClientTools.isSuccessWithContent(statusCode)) { + throw new FriendlyException("Server responded with an error.", SUSPICIOUS, + new IllegalStateException("Response code for player config is " + statusCode)); + } + + return sourceManager.loadConfigJsonFromPageContent(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); + } + } +}