11package dev .lavalink .youtube .plugin ;
22
33import com .sedmelluq .discord .lavaplayer .player .AudioPlayerManager ;
4- import com .sedmelluq .discord .lavaplayer .tools .DataFormatTools ;
4+ import com .sedmelluq .discord .lavaplayer .tools .io .HttpInterface ;
5+ import dev .lavalink .youtube .CannotBeLoaded ;
56import dev .lavalink .youtube .YoutubeAudioSourceManager ;
67import dev .lavalink .youtube .clients .Web ;
78import dev .lavalink .youtube .clients .WebEmbedded ;
9+ import dev .lavalink .youtube .clients .skeleton .Client ;
810import dev .lavalink .youtube .plugin .rest .MinimalConfigRequest ;
911import dev .lavalink .youtube .plugin .rest .MinimalConfigResponse ;
12+ import dev .lavalink .youtube .track .YoutubePersistentHttpStream ;
13+ import dev .lavalink .youtube .track .format .StreamFormat ;
14+ import dev .lavalink .youtube .track .format .TrackFormats ;
1015import org .slf4j .Logger ;
1116import org .slf4j .LoggerFactory ;
1217import org .springframework .http .HttpStatus ;
18+ import org .springframework .http .MediaType ;
19+ import org .springframework .http .ResponseEntity ;
1320import org .springframework .stereotype .Service ;
1421import org .springframework .web .bind .annotation .*;
1522import org .springframework .web .server .ResponseStatusException ;
23+ import org .springframework .web .servlet .mvc .method .annotation .StreamingResponseBody ;
24+
25+ import java .io .IOException ;
26+ import java .net .URI ;
27+ import java .util .Arrays ;
1628
1729@ Service
1830@ RestController
@@ -25,26 +37,136 @@ public YoutubeRestHandler(AudioPlayerManager playerManager) {
2537 this .playerManager = playerManager ;
2638 }
2739
28- @ GetMapping ("/youtube" )
29- public MinimalConfigResponse getYoutubeConfig () {
40+ private YoutubeAudioSourceManager getYoutubeSource () {
3041 YoutubeAudioSourceManager source = playerManager .source (YoutubeAudioSourceManager .class );
3142
3243 if (source == null ) {
3344 throw new ResponseStatusException (HttpStatus .BAD_REQUEST , "The YouTube source manager is not registered." );
3445 }
3546
36- return MinimalConfigResponse . from ( source ) ;
47+ return source ;
3748 }
3849
39- @ PostMapping ("/youtube" )
40- @ ResponseStatus (HttpStatus .NO_CONTENT )
41- public void updateYoutubeConfig (@ RequestBody MinimalConfigRequest config ) {
42- YoutubeAudioSourceManager source = playerManager .source (YoutubeAudioSourceManager .class );
50+ @ GetMapping ("/youtube/stream/{videoId}" )
51+ public ResponseEntity <StreamingResponseBody > getYoutubeVideoStream (@ PathVariable ("videoId" ) String videoId ,
52+ @ RequestParam (name = "itag" , required = false ) Integer itag ,
53+ @ RequestParam (name = "withClient" , required = false ) String clientIdentifier ) throws IOException {
54+ YoutubeAudioSourceManager source = getYoutubeSource ();
4355
44- if (source == null ) {
45- throw new ResponseStatusException (HttpStatus .BAD_REQUEST , "The YouTube source manager is not registered ." );
56+ if (Arrays . stream ( source . getClients ()). noneMatch ( Client :: supportsFormatLoading ) ) {
57+ throw new ResponseStatusException (HttpStatus .BAD_REQUEST , "None of the registered clients supports format loading ." );
4658 }
4759
60+ boolean foundFormats = false ;
61+
62+ HttpInterface httpInterface = source .getInterface ();
63+
64+ for (Client client : source .getClients ()) {
65+ log .debug ("REST streaming {} attempting to use client {}" , videoId , client .getIdentifier ());
66+
67+ if (clientIdentifier != null && !client .getIdentifier ().equalsIgnoreCase (clientIdentifier )) {
68+ log .debug ("Client identifier specified but does not match, trying next." );
69+ continue ;
70+ }
71+
72+ if (!client .supportsFormatLoading ()) {
73+ continue ;
74+ }
75+
76+ log .debug ("Loading formats for {} with client {}" , videoId , client .getIdentifier ());
77+
78+ TrackFormats formats ;
79+
80+ try {
81+ formats = client .loadFormats (source , httpInterface , videoId );
82+ } catch (CannotBeLoaded cbl ) {
83+ throw new ResponseStatusException (HttpStatus .BAD_REQUEST , "This video cannot be loaded. Reason: " + cbl .getCause ().getMessage ());
84+ }
85+
86+ if (formats == null || formats .getFormats ().isEmpty ()) {
87+ log .debug ("No formats found for {}" , videoId );
88+ continue ;
89+ }
90+
91+ foundFormats = true ;
92+ StreamFormat selectedFormat ;
93+
94+ if (itag == null ) {
95+ selectedFormat = formats .getBestFormat ();
96+ } else {
97+ selectedFormat = formats .getFormats ().stream ().filter (fmt -> fmt .getItag () == itag ).findFirst ()
98+ .orElse (null );
99+ }
100+
101+ if (selectedFormat == null ) {
102+ log .debug ("No suitable formats found. (Matching: {})" , itag );
103+ continue ;
104+ }
105+
106+ log .debug ("Selected format {} for {}" , selectedFormat .getItag (), videoId );
107+
108+ URI resolved = source .getCipherManager ().resolveFormatUrl (httpInterface , formats .getPlayerScriptUrl (), selectedFormat );
109+ URI transformed = client .transformPlaybackUri (selectedFormat .getUrl (), resolved );
110+ YoutubePersistentHttpStream httpStream = new YoutubePersistentHttpStream (httpInterface , transformed , selectedFormat .getContentLength ());
111+
112+ boolean streamValidated = false ;
113+
114+ try {
115+ int statusCode = httpStream .checkStatusCode ();
116+ streamValidated = statusCode == 200 ;
117+
118+ if (statusCode != 200 ) {
119+ log .debug ("REST streaming with {} for {} returned status code {} when opening video stream" , client .getIdentifier (), videoId , statusCode );
120+ }
121+ } catch (Throwable t ) {
122+ if ("Not success status code: 403" .equals (t .getMessage ())) {
123+ log .debug ("REST streaming with {} for {} returned status code 403 when opening video stream" , client .getIdentifier (), videoId );
124+ } else {
125+ IOUtils .closeQuietly (httpStream , httpInterface );
126+ throw t ;
127+ }
128+ }
129+
130+ if (!streamValidated ) {
131+ IOUtils .closeQuietly (httpStream );
132+ continue ;
133+ }
134+
135+ StreamingResponseBody buffer = (os ) -> {
136+ int bytesRead ;
137+ byte [] copy = new byte [1024 ];
138+
139+ try (httpStream ; httpInterface ) {
140+ while ((bytesRead = httpStream .read (copy , 0 , copy .length )) != -1 ) {
141+ os .write (copy , 0 , bytesRead );
142+ }
143+ }
144+ };
145+
146+ return ResponseEntity .ok ()
147+ .contentLength (selectedFormat .getContentLength ())
148+ .contentType (MediaType .parseMediaType (selectedFormat .getType ().getMimeType ()))
149+ .body (buffer );
150+ }
151+
152+ IOUtils .closeQuietly (httpInterface );
153+
154+ if (foundFormats ) {
155+ throw new ResponseStatusException (HttpStatus .BAD_REQUEST , "No formats found with the requested itag." );
156+ }
157+
158+ throw new ResponseStatusException (HttpStatus .BAD_REQUEST , "Could not find formats for the requested videoId." );
159+ }
160+
161+ @ GetMapping ("/youtube" )
162+ public MinimalConfigResponse getYoutubeConfig () {
163+ return MinimalConfigResponse .from (getYoutubeSource ());
164+ }
165+
166+ @ PostMapping ("/youtube" )
167+ @ ResponseStatus (HttpStatus .NO_CONTENT )
168+ public void updateYoutubeConfig (@ RequestBody MinimalConfigRequest config ) {
169+ YoutubeAudioSourceManager source = getYoutubeSource ();
48170 String refreshToken = config .getRefreshToken ();
49171
50172 if (!"x" .equals (refreshToken )) {
0 commit comments