2
2
3
3
import com .sedmelluq .discord .lavaplayer .player .AudioPlayerManager ;
4
4
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 .*;
9
6
import com .sedmelluq .discord .lavaplayer .tools .io .HttpClientTools ;
10
7
import com .sedmelluq .discord .lavaplayer .tools .io .HttpConfigurable ;
11
8
import com .sedmelluq .discord .lavaplayer .tools .io .HttpInterface ;
19
16
import org .apache .http .client .config .RequestConfig ;
20
17
import org .apache .http .client .methods .CloseableHttpResponse ;
21
18
import org .apache .http .client .methods .HttpGet ;
19
+ import org .apache .http .client .methods .HttpUriRequest ;
20
+ import org .apache .http .client .utils .URIBuilder ;
22
21
import org .apache .http .impl .client .HttpClientBuilder ;
23
22
24
23
import java .io .DataInput ;
25
24
import java .io .DataOutput ;
26
25
import java .io .IOException ;
26
+ import java .net .URISyntaxException ;
27
27
import java .nio .charset .StandardCharsets ;
28
28
import java .util .function .Consumer ;
29
29
import java .util .function .Function ;
30
+ import java .util .regex .Matcher ;
30
31
import java .util .regex .Pattern ;
31
32
32
33
import static com .sedmelluq .discord .lavaplayer .tools .FriendlyException .Severity .SUSPICIOUS ;
35
36
* Audio source manager which detects Vimeo tracks by URL.
36
37
*/
37
38
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]+) (?:\\ ?.*|)$" ;
39
40
private static final Pattern trackUrlPattern = Pattern .compile (TRACK_URL_REGEX );
40
41
41
42
private final HttpInterfaceManager httpInterfaceManager ;
@@ -54,13 +55,15 @@ public String getSourceName() {
54
55
55
56
@ Override
56
57
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 ()) {
58
61
return null ;
59
62
}
60
63
61
64
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 ) {
64
67
throw new FriendlyException ("Loading Vimeo track information failed." , SUSPICIOUS , e );
65
68
}
66
69
}
@@ -149,4 +152,87 @@ private AudioTrack loadTrackFromPageContent(String trackUrl, String content) thr
149
152
null
150
153
), this );
151
154
}
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
+ }
152
238
}
0 commit comments