Skip to content

Commit

Permalink
新ニコニコ動画に対応するためにニコニコ動画の再生時の処理をyt-dlpに変更
Browse files Browse the repository at this point in the history
  • Loading branch information
kosugikun committed Aug 8, 2024
1 parent 23b7354 commit 87e5b2f
Show file tree
Hide file tree
Showing 5 changed files with 650 additions and 5 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<groupId>com.jagrosh</groupId>
<artifactId>JMusicBot</artifactId>
<!-- バージョン装飾子参考の参考に: https://kengotoda.gitbooks.io/what-is-maven/deploy/snapshot-and-stable.html -->
<version>0.9.6</version>
<version>0.9.8</version>
<packaging>jar</packaging>

<repositories>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.sedmelluq.discord.lavaplayer.source.nico;

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.PersistentHttpStream;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
* An extension of PersistentHttpStream that allows for sending heartbeats to a secondary URL.
*/
public class HeartbeatingHttpStream extends PersistentHttpStream {
private static final Logger log = LoggerFactory.getLogger(HeartbeatingHttpStream.class);
private static final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

private String heartbeatUrl;
private int heartbeatInterval;
private String heartbeatPayload;

private ScheduledFuture<?> heartbeatFuture;

/**
* Creates a new heartbeating http stream.
* @param httpInterface The HTTP interface to use for requests.
* @param contentUrl The URL to play from.
* @param contentLength The length of the content. Null if unknown.
* @param heartbeatUrl The URL to send heartbeat requests to.
* @param heartbeatInterval The interval at which to heartbeat, in milliseconds.
* @param heartbeatPayload The initial heartbeat payload.
*/
public HeartbeatingHttpStream(
HttpInterface httpInterface,
URI contentUrl,
Long contentLength,
String heartbeatUrl,
int heartbeatInterval,
String heartbeatPayload
) {
super(httpInterface, contentUrl, contentLength);

this.heartbeatUrl = heartbeatUrl;
this.heartbeatInterval = heartbeatInterval;
this.heartbeatPayload = heartbeatPayload;

setupHeartbeat();
}

protected void setupHeartbeat() {
log.debug("Heartbeat every {} milliseconds to URL: {}", heartbeatInterval, heartbeatUrl);

heartbeatFuture = executor.scheduleAtFixedRate(() -> {
try {
sendHeartbeat();
} catch (Throwable t) {
log.error("Heartbeat error!", t);
IOUtils.closeQuietly(this);
}
}, heartbeatInterval, heartbeatInterval, TimeUnit.MILLISECONDS);
}

protected void sendHeartbeat() throws IOException {
HttpPost request = new HttpPost(heartbeatUrl);
request.addHeader("Host", "api.dmc.nico");
request.addHeader("Connection", "keep-alive");
request.addHeader("Content-Type", "application/json");
request.addHeader("Origin", "https://www.nicovideo.jp");
request.setEntity(new StringEntity(heartbeatPayload));

try (CloseableHttpResponse response = httpInterface.execute(request)) {
HttpClientTools.assertSuccessWithContent(response, "heartbeat page");

heartbeatPayload = JsonBrowser.parse(response.getEntity().getContent()).get("data").format();
}
}

@Override
public void close() throws IOException {
heartbeatFuture.cancel(false);
super.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package com.sedmelluq.discord.lavaplayer.source.nico;

import com.sedmelluq.discord.lavaplayer.container.MediaContainerDescriptor;
import com.sedmelluq.discord.lavaplayer.container.MediaContainerRegistry;
import com.sedmelluq.discord.lavaplayer.container.wav.WavContainerProbe;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
import com.sedmelluq.discord.lavaplayer.tools.DataFormatTools;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpClientTools;
import com.sedmelluq.discord.lavaplayer.tools.io.HttpConfigurable;
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.http.Header;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.parser.Parser;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.COMMON;
import static com.sedmelluq.discord.lavaplayer.tools.FriendlyException.Severity.SUSPICIOUS;

/**
* Audio source manager that implements finding NicoNico tracks based on URL.
*/
public class NicoAudioSourceManager implements AudioSourceManager, HttpConfigurable {
private static final String TRACK_URL_REGEX = "^(?:http://|https://|)(?:(?:www\\.|sp\\.|)nicovideo\\.jp/watch/|nico\\.ms/)(sm[0-9]+)(?:\\?.*|)$";

private static final Pattern trackUrlPattern = Pattern.compile(TRACK_URL_REGEX);

private final HttpInterfaceManager httpInterfaceManager;
private final AtomicBoolean loggedIn;

public NicoAudioSourceManager() {
this(null, null);
}

public HttpInterfaceManager getHttpInterfaceManager() {
return httpInterfaceManager;
}

/**
* @param email Site account email
* @param password Site account password
*/
public NicoAudioSourceManager(String email, String password) {
updateYtDlp();

File cacheDir = new File("cache");
if (!cacheDir.exists()) {
cacheDir.mkdirs();
}

httpInterfaceManager = HttpClientTools.createDefaultThreadLocalManager();
loggedIn = new AtomicBoolean();
// Log in at the start
if (!DataFormatTools.isNullOrEmpty(email) && !DataFormatTools.isNullOrEmpty(password)) {
logIn(email,password);
}
}

public void updateYtDlp() {
Runtime runtime = Runtime.getRuntime();
try {
Process process = runtime.exec("python3 -m pip install -U --pre \"yt-dlp\"");
process.waitFor();
process.destroy();
} catch (Exception e) {
throw new RuntimeException(e);
}

}

@Override
public String getSourceName() {
return "niconico";
}

@Override
public AudioItem loadItem(AudioPlayerManager manager, AudioReference reference) {
Matcher trackMatcher = trackUrlPattern.matcher(reference.identifier);

if (trackMatcher.matches()) {
return loadTrack(trackMatcher.group(1));
}

return null;
}

private AudioTrack loadTrack(String videoId) {
try (HttpInterface httpInterface = getHttpInterface()) {
try (CloseableHttpResponse response = httpInterface.execute(new HttpGet("https://ext.nicovideo.jp/api/getthumbinfo/" + videoId))) {
int statusCode = response.getStatusLine().getStatusCode();
if (!HttpClientTools.isSuccessWithContent(statusCode)) {
throw new IOException("Unexpected response code from video info: " + statusCode);
}

Document document = Jsoup.parse(response.getEntity().getContent(), StandardCharsets.UTF_8.name(), "", Parser.xmlParser());
return extractTrackFromXml(videoId, document);
}
} catch (IOException e) {
throw new FriendlyException("Error occurred when extracting video info.", SUSPICIOUS, e);
}
}

private AudioTrack extractTrackFromXml(String videoId, Document document) {
for (Element element : document.select(":root > thumb")) {

String uploader = "";
if(videoId.matches("so.*")){
uploader = element.select("ch_name").first().text();
}else{
uploader = element.select("user_nickname").first().text();
}
String title = element.selectFirst("title").text();
String thumbnailUrl = element.selectFirst("thumbnail_url").text();
long duration = DataFormatTools.durationTextToMillis(element.selectFirst("length").text());

return new NicoAudioTrack(new AudioTrackInfo(title,
uploader,
duration,
videoId,
false,
getWatchUrl(videoId),
thumbnailUrl,
null
), this, new MediaContainerDescriptor(new WavContainerProbe(), null));
}

return null;
}

@Override
public boolean isTrackEncodable(AudioTrack track) {
return true;
}

@Override
public void encodeTrack(AudioTrack track, DataOutput output) throws IOException {
// No extra information to save
}

@Override
public AudioTrack decodeTrack(AudioTrackInfo trackInfo, DataInput input) throws IOException {
return new NicoAudioTrack(trackInfo, this, new MediaContainerDescriptor(new WavContainerProbe(), null));
}

@Override
public void shutdown() {
// Nothing to shut down
}

/**
* @return Get an HTTP interface for a playing track.
*/
public HttpInterface getHttpInterface() {
return httpInterfaceManager.getInterface();
}

@Override
public void configureRequests(Function<RequestConfig, RequestConfig> configurator) {
httpInterfaceManager.configureRequests(configurator);
}

@Override
public void configureBuilder(Consumer<HttpClientBuilder> configurator) {
httpInterfaceManager.configureBuilder(configurator);
}

void logIn(String email, String password) {
synchronized (loggedIn) {
if (loggedIn.get()) {
return;
}

String url = "https://account.nicovideo.jp/login/redirector".trim();
URI uri = URI.create(url);
HttpPost loginRequest = new HttpPost(uri);

loginRequest.setEntity(new UrlEncodedFormEntity(Arrays.asList(
new BasicNameValuePair("mail_tel", email),
new BasicNameValuePair("password", password)
), StandardCharsets.UTF_8));

try (HttpInterface httpInterface = getHttpInterface()) {
try (CloseableHttpResponse response = httpInterface.execute(loginRequest)) {
int statusCode = response.getStatusLine().getStatusCode();

if (statusCode != 302) {
throw new IOException("Unexpected response code " + statusCode);
}

Header location = response.getFirstHeader("Location");

if (location == null || location.getValue().contains("message=cant_login")) {
throw new FriendlyException("Login details for NicoNico are invalid.", COMMON, null);
}

loggedIn.set(true);
}
} catch (IOException e) {
throw new FriendlyException("Exception when trying to log into NicoNico", SUSPICIOUS, e);
}
}
}

private static String getWatchUrl(String videoId) {
return "https://www.nicovideo.jp/watch/" + videoId;
}
}
Loading

0 comments on commit 87e5b2f

Please sign in to comment.