Skip to content

Commit

Permalink
Implement fetching video streams over REST
Browse files Browse the repository at this point in the history
  • Loading branch information
devoxin committed Sep 16, 2024
1 parent 200ec45 commit 1d825c0
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 10 deletions.
15 changes: 15 additions & 0 deletions plugin/src/main/java/dev/lavalink/youtube/plugin/IOUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package dev.lavalink.youtube.plugin;

import java.io.Closeable;

public class IOUtils {
public static void closeQuietly(Closeable... closeables) {
for (Closeable closeable : closeables) {
try {
closeable.close();
} catch (Throwable ignored) {

}
}
}
}
142 changes: 132 additions & 10 deletions plugin/src/main/java/dev/lavalink/youtube/plugin/YoutubeRestHandler.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
package dev.lavalink.youtube.plugin;

import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpInterface;
import dev.lavalink.youtube.CannotBeLoaded;
import dev.lavalink.youtube.YoutubeAudioSourceManager;
import dev.lavalink.youtube.clients.Web;
import dev.lavalink.youtube.clients.WebEmbedded;
import dev.lavalink.youtube.clients.skeleton.Client;
import dev.lavalink.youtube.plugin.rest.MinimalConfigRequest;
import dev.lavalink.youtube.plugin.rest.MinimalConfigResponse;
import dev.lavalink.youtube.track.YoutubePersistentHttpStream;
import dev.lavalink.youtube.track.format.StreamFormat;
import dev.lavalink.youtube.track.format.TrackFormats;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;

import java.io.IOException;
import java.net.URI;
import java.util.Arrays;

@Service
@RestController
Expand All @@ -25,26 +37,136 @@ public YoutubeRestHandler(AudioPlayerManager playerManager) {
this.playerManager = playerManager;
}

@GetMapping("/youtube")
public MinimalConfigResponse getYoutubeConfig() {
private YoutubeAudioSourceManager getYoutubeSource() {
YoutubeAudioSourceManager source = playerManager.source(YoutubeAudioSourceManager.class);

if (source == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The YouTube source manager is not registered.");
}

return MinimalConfigResponse.from(source);
return source;
}

@PostMapping("/youtube")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void updateYoutubeConfig(@RequestBody MinimalConfigRequest config) {
YoutubeAudioSourceManager source = playerManager.source(YoutubeAudioSourceManager.class);
@GetMapping("/youtube/stream/{videoId}")
public ResponseEntity<StreamingResponseBody> getYoutubeVideoStream(@PathVariable("videoId") String videoId,
@RequestParam(name = "itag", required = false) Integer itag,
@RequestParam(name = "withClient", required = false) String clientIdentifier) throws IOException {
YoutubeAudioSourceManager source = getYoutubeSource();

if (source == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "The YouTube source manager is not registered.");
if (Arrays.stream(source.getClients()).noneMatch(Client::supportsFormatLoading)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "None of the registered clients supports format loading.");
}

boolean foundFormats = false;

HttpInterface httpInterface = source.getInterface();

for (Client client : source.getClients()) {
log.debug("REST streaming {} attempting to use client {}", videoId, client.getIdentifier());

if (clientIdentifier != null && !client.getIdentifier().equalsIgnoreCase(clientIdentifier)) {
log.debug("Client identifier specified but does not match, trying next.");
continue;
}

if (!client.supportsFormatLoading()) {
continue;
}

log.debug("Loading formats for {} with client {}", videoId, client.getIdentifier());

TrackFormats formats;

try {
formats = client.loadFormats(source, httpInterface, videoId);
} catch (CannotBeLoaded cbl) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "This video cannot be loaded. Reason: " + cbl.getCause().getMessage());
}

if (formats == null || formats.getFormats().isEmpty()) {
log.debug("No formats found for {}", videoId);
continue;
}

foundFormats = true;
StreamFormat selectedFormat;

if (itag == null) {
selectedFormat = formats.getBestFormat();
} else {
selectedFormat = formats.getFormats().stream().filter(fmt -> fmt.getItag() == itag).findFirst()
.orElse(null);
}

if (selectedFormat == null) {
log.debug("No suitable formats found. (Matching: {})", itag);
continue;
}

log.debug("Selected format {} for {}", selectedFormat.getItag(), videoId);

URI resolved = source.getCipherManager().resolveFormatUrl(httpInterface, formats.getPlayerScriptUrl(), selectedFormat);
URI transformed = client.transformPlaybackUri(selectedFormat.getUrl(), resolved);
YoutubePersistentHttpStream httpStream = new YoutubePersistentHttpStream(httpInterface, transformed, selectedFormat.getContentLength());

boolean streamValidated = false;

try {
int statusCode = httpStream.checkStatusCode();
streamValidated = statusCode == 200;

if (statusCode != 200) {
log.debug("REST streaming with {} for {} returned status code {} when opening video stream", client.getIdentifier(), videoId, statusCode);
}
} catch (Throwable t) {
if ("Not success status code: 403".equals(t.getMessage())) {
log.debug("REST streaming with {} for {} returned status code 403 when opening video stream", client.getIdentifier(), videoId);
} else {
IOUtils.closeQuietly(httpStream, httpInterface);
throw t;
}
}

if (!streamValidated) {
IOUtils.closeQuietly(httpStream);
continue;
}

StreamingResponseBody buffer = (os) -> {
int bytesRead;
byte[] copy = new byte[1024];

try (httpStream; httpInterface) {
while ((bytesRead = httpStream.read(copy, 0, copy.length)) != -1) {
os.write(copy, 0, bytesRead);
}
}
};

return ResponseEntity.ok()
.contentLength(selectedFormat.getContentLength())
.contentType(MediaType.parseMediaType(selectedFormat.getType().getMimeType()))
.body(buffer);
}

IOUtils.closeQuietly(httpInterface);

if (foundFormats) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No formats found with the requested itag.");
}

throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Could not find formats for the requested videoId.");
}

@GetMapping("/youtube")
public MinimalConfigResponse getYoutubeConfig() {
return MinimalConfigResponse.from(getYoutubeSource());
}

@PostMapping("/youtube")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void updateYoutubeConfig(@RequestBody MinimalConfigRequest config) {
YoutubeAudioSourceManager source = getYoutubeSource();
String refreshToken = config.getRefreshToken();

if (!"x".equals(refreshToken)) {
Expand Down

0 comments on commit 1d825c0

Please sign in to comment.