forked from jagrosh/MusicBot
-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
新ニコニコ動画に対応するためにニコニコ動画の再生時の処理をyt-dlpに変更
- Loading branch information
Showing
5 changed files
with
650 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
94 changes: 94 additions & 0 deletions
94
src/main/java/com/sedmelluq/discord/lavaplayer/source/nico/HeartbeatingHttpStream.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
234 changes: 234 additions & 0 deletions
234
src/main/java/com/sedmelluq/discord/lavaplayer/source/nico/NicoAudioSourceManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.