Remove outdated Udio plugin references and related code
This commit is contained in:
1 parent
38bbf01e9f
commit
ffc15e8044
5 files changed
+3
-399
No files matched your search
@@ -1,43 +0,0 @@
|
|||||||
# Udio source — follow-up reference
|
|
||||||
|
|
||||||
These files are **not part of the build**. They are the original client-side Lavaplayer
|
|
||||||
implementation of the udio.com source, kept verbatim as the reference for a future port.
|
|
||||||
|
|
||||||
| File | Was |
|
|
||||||
|---|---|
|
|
||||||
| `UdioAudioSourceManager.java` | `me.bobert.netneutrality.music.source.UdioAudioSourceManager` |
|
|
||||||
| `UdioAudioTrack.java` | `me.bobert.netneutrality.music.source.UdioAudioTrack` |
|
|
||||||
| `UdioAudioSourceManagerTest.java` | its unit test |
|
|
||||||
|
|
||||||
## Why they were moved
|
|
||||||
|
|
||||||
The bot no longer decodes audio itself: after the [migration to Lavalink](../../music.md), source
|
|
||||||
resolution happens on the node. A Lavaplayer `AudioSourceManager` living in the bot process can
|
|
||||||
never be used by Lavalink, and these classes cannot even compile now that the `dev.arbjerg:lavaplayer`
|
|
||||||
dependency has been dropped from `pom.xml`.
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
**Udio playback is disabled.** `SourceFinder` still classifies Udio URLs, but
|
|
||||||
`SourceFinder.getIdentifier` returns no identifier for them and `MusicManager.play` replies that
|
|
||||||
Udio is temporarily unsupported.
|
|
||||||
|
|
||||||
## Follow-up
|
|
||||||
|
|
||||||
Port `UdioAudioSourceManager` / `UdioAudioTrack` into a standalone **Lavalink server plugin**
|
|
||||||
(its own build and published artifact), then enable it by adding the dependency to
|
|
||||||
`lavalink/application.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
lavalink:
|
|
||||||
plugins:
|
|
||||||
- dependency: "<group>:udio-plugin:<version>"
|
|
||||||
```
|
|
||||||
|
|
||||||
Once the node resolves `https://www.udio.com/songs/...` identifiers, the only bot-side change is to
|
|
||||||
restore `Udio` in the `SourceFinder.getIdentifier` switch and drop the "temporarily unsupported"
|
|
||||||
reply in `MusicManager.play`.
|
|
||||||
|
|
||||||
The scraping logic itself still applies: the manager extracts metadata and a direct playback URL
|
|
||||||
from the Next.js payload (`self.__next_f.push(...)`) of the track page, and the URL must be
|
|
||||||
re-fetched right before playback because it expires.
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
package me.bobert.netneutrality.music.source;
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
|
|
||||||
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.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.methods.CloseableHttpResponse;
|
|
||||||
import org.apache.http.client.methods.HttpGet;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import java.io.DataInput;
|
|
||||||
import java.io.DataOutput;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS;
|
|
||||||
|
|
||||||
public class UdioAudioSourceManager implements AudioSourceManager {
|
|
||||||
private static final Logger log = LogManager.getLogger(UdioAudioSourceManager.class);
|
|
||||||
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 static final Pattern NEXT_PUSH_PATTERN = Pattern.compile(
|
|
||||||
"self\\.__next_f\\.push\\(\\[(\\d+),\"(.*?)\"]\\)",
|
|
||||||
Pattern.DOTALL
|
|
||||||
);
|
|
||||||
private final HttpInterfaceManager httpInterfaceManager;
|
|
||||||
|
|
||||||
public UdioAudioSourceManager() {
|
|
||||||
httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getSourceName() {
|
|
||||||
return "udio";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isTrackEncodable(AudioTrack track) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void encodeTrack(AudioTrack track, DataOutput output) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) {
|
|
||||||
return new UdioAudioTrack(trackInfo, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void shutdown() {
|
|
||||||
ExceptionTools.closeWithWarnings(httpInterfaceManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpInterface getHttpInterface() {
|
|
||||||
return httpInterfaceManager.getInterface();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 trackInfo = loadTrackInfoFromPageContent(content);
|
|
||||||
|
|
||||||
if (trackInfo == null) {
|
|
||||||
log.debug("Udio HTML: {}", content);
|
|
||||||
throw new FriendlyException("Udio: Failed to extract track information from web page.", SUSPICIOUS, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
String title = trackInfo.get("title").text();
|
|
||||||
String artist = trackInfo.get("artist").text();
|
|
||||||
long duration = (long) (trackInfo.get("duration").as(Double.class) * 1000.0);
|
|
||||||
String playbackUrl = trackInfo.get("song_path").text();
|
|
||||||
String artworkUrl = getArtworkUrlFromImagePath(trackInfo);
|
|
||||||
|
|
||||||
AudioTrackInfo audioTrackInfo = new AudioTrackInfo(title, artist, duration, trackUrl, false, playbackUrl, artworkUrl, null);
|
|
||||||
return new UdioAudioTrack(audioTrackInfo, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected JsonBrowser loadTrackInfoFromPageContent(String content) throws IOException {
|
|
||||||
Matcher matcher = NEXT_PUSH_PATTERN.matcher(content);
|
|
||||||
while (matcher.find()) {
|
|
||||||
String payload = matcher.group(2);
|
|
||||||
JsonBrowser trackInfo = tryExtractTrackFromNextChunk(payload);
|
|
||||||
|
|
||||||
if (trackInfo != null) {
|
|
||||||
return trackInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonBrowser tryExtractTrackFromNextChunk(String payload) {
|
|
||||||
try {
|
|
||||||
int trackKeyIndex = payload.indexOf("\\\"track\\\":{");
|
|
||||||
if (trackKeyIndex < 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
int objectStart = payload.indexOf('{', trackKeyIndex);
|
|
||||||
if (objectStart < 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String escapedTrackJson = extractJsonObjectFromString(payload, objectStart);
|
|
||||||
if (escapedTrackJson == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String trackJson = unescapeJsonStringFragment(escapedTrackJson);
|
|
||||||
JsonBrowser track = JsonBrowser.parse(trackJson);
|
|
||||||
|
|
||||||
if (isValidTrack(track)) {
|
|
||||||
return track;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.debug("Skipping unparsable track payload fragment", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts a JSON object from a given string starting at the specified index. This method parses the string
|
|
||||||
* to locate and retrieve the JSON object, ensuring proper handling of nested structures and escaped characters.
|
|
||||||
*
|
|
||||||
* @param text The input string containing the JSON object to be extracted.
|
|
||||||
* @param startIndex The index at which to start searching for the JSON object within the input string.
|
|
||||||
* @return The extracted JSON object as a string, or {@code null} if no valid JSON object is found.
|
|
||||||
*/
|
|
||||||
private String extractJsonObjectFromString(String text, int startIndex) {
|
|
||||||
boolean escaped = false;
|
|
||||||
boolean inString = false;
|
|
||||||
int depth = 0;
|
|
||||||
|
|
||||||
for (int i = startIndex; i < text.length(); i++) {
|
|
||||||
char c = text.charAt(i);
|
|
||||||
|
|
||||||
if (escaped) {
|
|
||||||
escaped = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c == '\\') {
|
|
||||||
escaped = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c == '"') {
|
|
||||||
inString = !inString;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inString) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c == '{') {
|
|
||||||
depth++;
|
|
||||||
} else if (c == '}') {
|
|
||||||
depth--;
|
|
||||||
if (depth == 0) {
|
|
||||||
return text.substring(startIndex, i + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String unescapeJsonStringFragment(String value) {
|
|
||||||
return value.replace("\\\"", "\"").replace("\\\\", "\\");
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isValidTrack(JsonBrowser track) {
|
|
||||||
return track != null
|
|
||||||
&& !track.isNull()
|
|
||||||
&& !track.get("title").isNull()
|
|
||||||
&& !track.get("artist").isNull()
|
|
||||||
&& !track.get("duration").isNull()
|
|
||||||
&& !track.get("song_path").isNull()
|
|
||||||
&& !track.get("image_path").isNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getArtworkUrlFromImagePath(JsonBrowser trackInfo) {
|
|
||||||
String imagePath = trackInfo.get("image_path").text();
|
|
||||||
String artworkOptions = "width=3840,quality=75";
|
|
||||||
|
|
||||||
return imagePath.replace("public", artworkOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package me.bobert.netneutrality.music.source;
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioItem;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioReference;
|
|
||||||
import com.sedmelluq.discord.lavaplayer.track.AudioTrackInfo;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
|
|
||||||
public class UdioAudioSourceManagerTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void loadItem() {
|
|
||||||
AudioReference reference = new AudioReference("https://www.udio.com/songs/eNmZxqpMt9MRrjFUgYDgyi", null);
|
|
||||||
|
|
||||||
UdioAudioSourceManager udioAudioSourceManager = new UdioAudioSourceManager();
|
|
||||||
AudioItem loaded = udioAudioSourceManager.loadItem(null, reference);
|
|
||||||
|
|
||||||
assertNotNull(loaded);
|
|
||||||
|
|
||||||
UdioAudioTrack audioTrack = (UdioAudioTrack) loaded;
|
|
||||||
AudioTrackInfo info = audioTrack.getInfo();
|
|
||||||
|
|
||||||
assertNotNull(info);
|
|
||||||
|
|
||||||
assertNotNull(info.artworkUrl);
|
|
||||||
|
|
||||||
String artworkOptions = "width=3840,quality=75";
|
|
||||||
assertTrue(info.artworkUrl.contains(artworkOptions));
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
package me.bobert.netneutrality.music.source;
|
|
||||||
/*
|
|
||||||
* Copyright (c) anthorob
|
|
||||||
*
|
|
||||||
* https://discord.gg/qUa6zNFZsd
|
|
||||||
*/
|
|
||||||
|
|
||||||
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.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
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 = LogManager.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 trackInfo = loadTrackInfo(httpInterface);
|
|
||||||
|
|
||||||
if (trackInfo == null) {
|
|
||||||
throw new FriendlyException("Udio: Failed to extract track information from web page.", SUSPICIOUS, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return trackInfo.get("song_path").text();
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonBrowser loadTrackInfo(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.loadTrackInfoFromPageContent(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -61,8 +61,11 @@
|
|||||||
<artifactId>flyway-maven-plugin</artifactId>
|
<artifactId>flyway-maven-plugin</artifactId>
|
||||||
<version>11.20.3</version>
|
<version>11.20.3</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
|
<!--suppress UnresolvedMavenProperty -->
|
||||||
<url>${env.FLYWAY_URL}</url>
|
<url>${env.FLYWAY_URL}</url>
|
||||||
|
<!--suppress UnresolvedMavenProperty -->
|
||||||
<user>${env.FLYWAY_USER}</user>
|
<user>${env.FLYWAY_USER}</user>
|
||||||
|
<!--suppress UnresolvedMavenProperty -->
|
||||||
<password>${env.FLYWAY_PASSWORD}</password>
|
<password>${env.FLYWAY_PASSWORD}</password>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|||||||
Reference in new issue
Block a user