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); } }