Skip to content

Commit 3034cd1

Browse files
authored
Merge pull request #149 from lavalink-devs/fix/vimeo-playback
Fix Vimeo playback
2 parents 0e9bce3 + 85076c4 commit 3034cd1

File tree

3 files changed

+125
-79
lines changed

3 files changed

+125
-79
lines changed

main/src/main/java/com/sedmelluq/discord/lavaplayer/source/vimeo/VimeoAudioSourceManager.java

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22

33
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
44
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
5-
import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools;
6-
import com.sedmelluq.discord.lavaplayer.tools.ExceptionTools;
7-
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
8-
import com.sedmelluq.discord.lavaplayer.tools.JsonBrowser;
5+
import com.sedmelluq.discord.lavaplayer.tools.*;
96
import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools;
107
import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable;
118
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface;
@@ -19,14 +16,18 @@
1916
import org.apache.http.client.config.RequestConfig;
2017
import org.apache.http.client.methods.CloseableHttpResponse;
2118
import org.apache.http.client.methods.HttpGet;
19+
import org.apache.http.client.methods.HttpUriRequest;
20+
import org.apache.http.client.utils.URIBuilder;
2221
import org.apache.http.impl.client.HttpClientBuilder;
2322

2423
import java.io.DataInput;
2524
import java.io.DataOutput;
2625
import java.io.IOException;
26+
import java.net.URISyntaxException;
2727
import java.nio.charset.StandardCharsets;
2828
import java.util.function.Consumer;
2929
import java.util.function.Function;
30+
import java.util.regex.Matcher;
3031
import java.util.regex.Pattern;
3132

3233
import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS;
@@ -35,7 +36,7 @@
3536
* Audio source manager which detects Vimeo tracks by URL.
3637
*/
3738
public class VimeoAudioSourceManager implements AudioSourceManager, HttpConfigurable {
38-
private static final String TRACK_URL_REGEX = "^https://vimeo.com/[0-9]+(?:\\?.*|)$";
39+
private static final String TRACK_URL_REGEX = "^https?://vimeo.com/([0-9]+)(?:\\?.*|)$";
3940
private static final Pattern trackUrlPattern = Pattern.compile(TRACK_URL_REGEX);
4041

4142
private final HttpInterfaceManager httpInterfaceManager;
@@ -54,13 +55,15 @@ public String getSourceName() {
5455

5556
@Override
5657
public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) {
57-
if (!trackUrlPattern.matcher(reference.identifier).matches()) {
58+
Matcher trackUrl = trackUrlPattern.matcher(reference.identifier);
59+
60+
if (!trackUrl.matches()) {
5861
return null;
5962
}
6063

6164
try (HttpInterface httpInterface = httpInterfaceManager.getInterface()) {
62-
return loadFromTrackPage(httpInterface, reference.identifier);
63-
} catch (IOException e) {
65+
return loadVideoFromApi(httpInterface, trackUrl.group(1));
66+
} catch (IOException | URISyntaxException e) {
6467
throw new FriendlyException("Loading Vimeo track information failed.", SUSPICIOUS, e);
6568
}
6669
}
@@ -149,4 +152,87 @@ private AudioTrack loadTrackFromPageContent(String trackUrl, String content) thr
149152
null
150153
), this);
151154
}
155+
156+
private AudioTrack loadVideoFromApi(HttpInterface httpInterface, String videoId) throws IOException, URISyntaxException {
157+
JsonBrowser videoData = getVideoFromApi(httpInterface, videoId);
158+
159+
AudioTrackInfo info = new AudioTrackInfo(
160+
videoData.get("name").text(),
161+
videoData.get("uploader").get("name").textOrDefault("Unknown artist"),
162+
Units.secondsToMillis(videoData.get("duration").asLong(Units.DURATION_SEC_UNKNOWN)),
163+
videoId,
164+
false,
165+
"https://vimeo.com/" + videoId,
166+
videoData.get("pictures").get("base_link").text(),
167+
null
168+
);
169+
170+
return new VimeoAudioTrack(info, this);
171+
}
172+
173+
public JsonBrowser getVideoFromApi(HttpInterface httpInterface, String videoId) throws IOException, URISyntaxException {
174+
String jwt = getApiJwt(httpInterface);
175+
176+
URIBuilder builder = new URIBuilder("https://api.vimeo.com/videos/" + videoId);
177+
// adding `play` to the fields achieves the same thing as requesting the config_url, but with one less request.
178+
// maybe we should consider using that instead? Need to figure out what the difference is, if any.
179+
builder.setParameter("fields", "config_url,name,uploader.name,duration,pictures");
180+
181+
HttpUriRequest request = new HttpGet(builder.build());
182+
request.setHeader("Authorization", "jwt " + jwt);
183+
request.setHeader("Accept", "application/json");
184+
185+
try (CloseableHttpResponse response = httpInterface.execute(request)) {
186+
HttpClientTools.assertSuccessWithContent(response, "fetch video api");
187+
return JsonBrowser.parse(response.getEntity().getContent());
188+
}
189+
}
190+
191+
public PlaybackFormat getPlaybackFormat(HttpInterface httpInterface, String configUrl) throws IOException {
192+
try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(configUrl))) {
193+
HttpClientTools.assertSuccessWithContent(response, "fetch playback formats");
194+
195+
JsonBrowser json = JsonBrowser.parse(response.getEntity().getContent());
196+
197+
// {"dash", "hls", "progressive"}
198+
// N.B. opus is referenced in some of the URLs, but I don't see any formats offering opus audio codec.
199+
// Might be a gradual rollout so this may need revisiting.
200+
JsonBrowser files = json.get("request").get("files");
201+
202+
if (!files.get("progressive").isNull()) {
203+
JsonBrowser progressive = files.get("progressive").index(0);
204+
205+
if (!progressive.isNull()) {
206+
return new PlaybackFormat(progressive.get("url").text(), false);
207+
}
208+
}
209+
210+
if (!files.get("hls").isNull()) {
211+
JsonBrowser hls = files.get("hls");
212+
// ["akfire_interconnect_quic", "fastly_skyfire"]
213+
JsonBrowser cdns = hls.get("cdns");
214+
return new PlaybackFormat(cdns.get(hls.get("default_cdn").text()).get("url").text(), true);
215+
}
216+
217+
throw new RuntimeException("No supported formats");
218+
}
219+
}
220+
221+
private String getApiJwt(HttpInterface httpInterface) throws IOException {
222+
try (CloseableHttpResponse response = httpInterface.execute(new HttpGet("https://vimeo.com/_next/viewer"))) {
223+
HttpClientTools.assertSuccessWithContent(response, "fetch jwt");
224+
JsonBrowser json = JsonBrowser.parse(response.getEntity().getContent());
225+
return json.get("jwt").text();
226+
}
227+
}
228+
229+
public static class PlaybackFormat {
230+
public final String url;
231+
public final boolean isHls;
232+
233+
public PlaybackFormat(String url, boolean isHls) {
234+
this.url = url;
235+
this.isHls = isHls;
236+
}
237+
}
152238
}

main/src/main/java/com/sedmelluq/discord/lavaplayer/source/vimeo/VimeoAudioTrack.java

Lines changed: 16 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public class VimeoAudioTrack extends DelegatedAudioTrack {
3535
private final VimeoAudioSourceManager sourceManager;
3636

3737
/**
38-
* @param trackInfo Track info
38+
* @param trackInfo Track info
3939
* @param sourceManager Source manager which was used to find this track
4040
*/
4141
public VimeoAudioTrack(AudioTrackInfo trackInfo, VimeoAudioSourceManager sourceManager) {
@@ -47,81 +47,24 @@ public VimeoAudioTrack(AudioTrackInfo trackInfo, VimeoAudioSourceManager sourceM
4747
@Override
4848
public void process(LocalAudioTrackExecutor localExecutor) throws Exception {
4949
try (HttpInterface httpInterface = sourceManager.getHttpInterface()) {
50-
PlaybackSource playbackSource = getPlaybackSource(httpInterface);
50+
JsonBrowser videoData = sourceManager.getVideoFromApi(httpInterface, trackInfo.identifier);
51+
VimeoAudioSourceManager.PlaybackFormat playbackFormat = sourceManager.getPlaybackFormat(httpInterface, videoData.get("config_url").text());
5152

52-
log.debug("Starting Vimeo track. HLS: {}, URL: {}", playbackSource.isHls, playbackSource.url);
53+
log.debug("Starting Vimeo track. HLS: {}, URL: {}", playbackFormat.isHls, playbackFormat.url);
5354

54-
if (playbackSource.isHls) {
55-
processDelegate(new HlsStreamTrack(
56-
trackInfo,
57-
extractHlsAudioPlaylistUrl(httpInterface, playbackSource.url),
58-
sourceManager.getHttpInterfaceManager(),
59-
true
60-
), localExecutor);
55+
if (playbackFormat.isHls) {
56+
processDelegate(
57+
new HlsStreamTrack(trackInfo, extractHlsAudioPlaylistUrl(httpInterface, playbackFormat.url), sourceManager.getHttpInterfaceManager(), true),
58+
localExecutor
59+
);
6160
} else {
62-
try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(playbackSource.url), null)) {
61+
try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(playbackFormat.url), null)) {
6362
processDelegate(new MpegAudioTrack(trackInfo, stream), localExecutor);
6463
}
6564
}
6665
}
6766
}
6867

69-
private PlaybackSource getPlaybackSource(HttpInterface httpInterface) throws IOException {
70-
JsonBrowser config = loadPlayerConfig(httpInterface);
71-
if (config == null) {
72-
throw new FriendlyException("Track information not present on the page.", SUSPICIOUS, null);
73-
}
74-
75-
String trackConfigUrl = config.get("player").get("config_url").text();
76-
JsonBrowser trackConfig = loadTrackConfig(httpInterface, trackConfigUrl);
77-
JsonBrowser files = trackConfig.get("request").get("files");
78-
79-
if (!files.get("progressive").values().isEmpty()) {
80-
String url = files.get("progressive").index(0).get("url").text();
81-
return new PlaybackSource(url, false);
82-
} else {
83-
JsonBrowser hls = files.get("hls");
84-
String defaultCdn = hls.get("default_cdn").text();
85-
return new PlaybackSource(hls.get("cdns").get(defaultCdn).get("url").text(), true);
86-
}
87-
}
88-
89-
private static class PlaybackSource {
90-
public String url;
91-
public boolean isHls;
92-
93-
public PlaybackSource(String url, boolean isHls) {
94-
this.url = url;
95-
this.isHls = isHls;
96-
}
97-
}
98-
99-
private JsonBrowser loadPlayerConfig(HttpInterface httpInterface) throws IOException {
100-
try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackInfo.identifier))) {
101-
int statusCode = response.getStatusLine().getStatusCode();
102-
103-
if (!HttpClientTools.isSuccessWithContent(statusCode)) {
104-
throw new FriendlyException("Server responded with an error.", SUSPICIOUS,
105-
new IllegalStateException("Response code for player config is " + statusCode));
106-
}
107-
108-
return sourceManager.loadConfigJsonFromPageContent(IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
109-
}
110-
}
111-
112-
private JsonBrowser loadTrackConfig(HttpInterface httpInterface, String trackAccessInfoUrl) throws IOException {
113-
try (CloseableHttpResponse response = httpInterface.execute(new HttpGet(trackAccessInfoUrl))) {
114-
int statusCode = response.getStatusLine().getStatusCode();
115-
116-
if (!HttpClientTools.isSuccessWithContent(statusCode)) {
117-
throw new FriendlyException("Server responded with an error.", SUSPICIOUS,
118-
new IllegalStateException("Response code for track access info is " + statusCode));
119-
}
120-
121-
return JsonBrowser.parse(response.getEntity().getContent());
122-
}
123-
}
124-
12568
protected String resolveRelativeUrl(String baseUrl, String url) {
12669
while (url.startsWith("../")) {
12770
url = url.substring(3);
@@ -145,16 +88,18 @@ private String extractHlsAudioPlaylistUrl(HttpInterface httpInterface, String vi
14588
String bodyString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
14689
for (String rawLine : bodyString.split("\n")) {
14790
ExtendedM3uParser.Line line = ExtendedM3uParser.parseLine(rawLine);
148-
if (Objects.equals(line.directiveName, "EXT-X-MEDIA")
149-
&& Objects.equals(line.directiveArguments.get("TYPE"), "AUDIO")) {
91+
92+
if (Objects.equals(line.directiveName, "EXT-X-MEDIA") && Objects.equals(line.directiveArguments.get("TYPE"), "AUDIO")) {
15093
url = line.directiveArguments.get("URI");
15194
break;
15295
}
15396
}
15497
}
15598

156-
if (url == null) throw new FriendlyException("Failed to find audio playlist URL.", SUSPICIOUS,
157-
new IllegalStateException("Valid audio directive was not found"));
99+
if (url == null) {
100+
throw new FriendlyException("Failed to find audio playlist URL.", SUSPICIOUS,
101+
new IllegalStateException("Valid audio directive was not found"));
102+
}
158103

159104
return resolveRelativeUrl(videoPlaylistUrl.substring(0, videoPlaylistUrl.lastIndexOf('/')), url);
160105
}

main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/JsonBrowser.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.io.IOException;
1212
import java.io.InputStream;
1313
import java.util.ArrayList;
14+
import java.util.Collections;
1415
import java.util.List;
1516

1617
/**
@@ -163,6 +164,20 @@ public List<JsonBrowser> values() {
163164
return values;
164165
}
165166

167+
/**
168+
* Returns a list of all key names in this element if it's a map.
169+
* @return The list of keys.
170+
*/
171+
public List<String> keys() {
172+
if (!isMap()) {
173+
return Collections.emptyList();
174+
}
175+
176+
List<String> keys = new ArrayList<>();
177+
node.fieldNames().forEachRemaining(keys::add);
178+
return keys;
179+
}
180+
166181
/**
167182
* Attempt to retrieve the value in the specified format
168183
*

0 commit comments

Comments
 (0)