Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix NicoNico Track #50

Merged
merged 18 commits into from
Dec 21, 2023
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.sedmelluq.discord.lavaplayer.container.mpeg.MpegAudioTrack;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
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;
Expand All @@ -14,30 +15,31 @@
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
* Audio track that handles processing NicoNico tracks.
*/
public class NicoAudioTrack extends DelegatedAudioTrack {
private static final Logger log = LoggerFactory.getLogger(NicoAudioTrack.class);

private static final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

private final NicoAudioSourceManager sourceManager;

private String heartbeatURL;

private JSONObject info;
private JsonBrowser info;
topi314 marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param trackInfo Track info
Expand All @@ -58,24 +60,22 @@ public void process(LocalAudioTrackExecutor localExecutor) throws Exception {
log.debug("Starting NicoNico track from URL: {}", playbackUrl);

try (PersistentHttpStream stream = new PersistentHttpStream(httpInterface, new URI(playbackUrl), null)) {
int heartbeat = (int) info.query("/session/keep_method/heartbeat/lifetime") - 1000;
Timer t = new Timer();
t.schedule(new TimerTask() {
public void run() {
try {
sendHeartbeat(httpInterface);
} catch (IOException ex) {
log.error("Heartbeat error!",ex);
}
long heartbeat = info.get("session").get("keep_method").get("heartbeat").get("lifetime").asLong(120000) - 60000;
executorService.scheduleAtFixedRate(() -> {
try {
sendHeartbeat(httpInterface);
} catch (Exception ex) {
log.error("Heartbeat error!",ex);
localExecutor.stop();
}
},heartbeat,heartbeat);
},heartbeat,heartbeat, TimeUnit.MILLISECONDS);
processDelegate(new MpegAudioTrack(trackInfo, stream), localExecutor);
t.cancel();
executorService.shutdown();
}
}
}

private JSONObject loadVideoMainPage(HttpInterface httpInterface) throws IOException {
private JsonBrowser loadVideoMainPage(HttpInterface httpInterface) throws IOException {
HttpGet request = new HttpGet(trackInfo.uri);

try (CloseableHttpResponse response = httpInterface.execute(request)) {
Expand All @@ -84,31 +84,32 @@ private JSONObject loadVideoMainPage(HttpInterface httpInterface) throws IOExcep
throw new IOException("Unexpected status code from video main page: " + statusCode);
}

return (JSONObject) new JSONObject(
Jsoup.parse(response.getEntity().getContent(), StandardCharsets.UTF_8.name(), "")
.getElementById("js-initial-watch-data").attributes().get("data-api-data"))
.query("/media/delivery/movie/session");
Document mainPage = Jsoup.parse(response.getEntity().getContent(), StandardCharsets.UTF_8.name(), "");
String watchdata = mainPage.getElementById("js-initial-watch-data").attributes().get("data-api-data");

return JsonBrowser.parse(watchdata).get("media").get("delivery").get("movie").get("session");
}
}

private String loadPlaybackUrl(HttpInterface httpInterface) throws IOException {
JsonBrowser watchdata = processJSON(loadVideoMainPage(httpInterface));
HttpPost request = new HttpPost("https://api.dmc.nico/api/sessions?_format=json");
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(processJSON(loadVideoMainPage(httpInterface)).toString()));
request.setEntity(new StringEntity(watchdata.text()));
topi314 marked this conversation as resolved.
Show resolved Hide resolved

try (CloseableHttpResponse response = httpInterface.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_CREATED) {
throw new IOException("Unexpected status code from playback parameters page: " + statusCode);
}

info = new JSONObject(new JSONTokener(response.getEntity().getContent())).getJSONObject("data");
heartbeatURL = "https://api.dmc.nico/api/sessions/" + info.query("/session/id") + "?_format=json&_method=PUT";
info = JsonBrowser.parse(response.getEntity().getContent()).get("data");
topi314 marked this conversation as resolved.
Show resolved Hide resolved
heartbeatURL = "https://api.dmc.nico/api/sessions/" + info.get("session").get("id").text() + "?_format=json&_method=PUT";
log.debug("NicoNico heartbeat URL: {}", heartbeatURL);
return (String) info.query("/session/content_uri");
return info.get("session").get("content_uri").text();
}
}

Expand All @@ -118,76 +119,115 @@ private void sendHeartbeat(HttpInterface httpInterface) throws IOException {
request.addHeader("Connection","keep-alive");
request.addHeader("Content-Type","application/json");
request.addHeader("Origin","https://www.nicovideo.jp");
request.setEntity(new StringEntity(info.toString()));
request.setEntity(new StringEntity(info.text()));
topi314 marked this conversation as resolved.
Show resolved Hide resolved

try (CloseableHttpResponse response = httpInterface.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
if (!HttpClientTools.isSuccessWithContent(statusCode)) {
throw new IOException("Unexpected status code from heartbeat: " + statusCode);
throw new IOException("Unexpected status code from heartbeat page: " + statusCode);
}

info = new JSONObject(new JSONTokener(response.getEntity().getContent())).getJSONObject("data");
info = JsonBrowser.parse(response.getEntity().getContent()).get("data");
topi314 marked this conversation as resolved.
Show resolved Hide resolved
}
}

private static JSONObject processJSON(JSONObject input){
return new JSONObject().put("session",new JSONObject()
.put("content_type","movie")
.put("keep_method",new JSONObject()
.put("heartbeat",new JSONObject()
.put("lifetime",input.getInt("heartbeatLifetime"))
)
)
.put("timing_constraint","unlimited")
.put("content_src_id_sets", new JSONArray()
.put(new JSONObject()
.put("content_src_ids",new JSONArray()
.put(new JSONObject()
.put("src_id_to_mux",new JSONObject()
.put("video_src_ids",input.getJSONArray("videos"))
.put("audio_src_ids",input.getJSONArray("audios"))
)
)
)
)
)
.put("recipe_id",input.getString("recipeId"))
.put("priority",input.getInt("priority"))
.put("protocol",new JSONObject()
.put("name","http")
.put("parameters",new JSONObject()
.put("http_parameters", new JSONObject()
.put("parameters",new JSONObject()
.put("http_output_download_parameters",new JSONObject()
.put("use_well_known_port",
(boolean) input.query("/urls/0/isWellKnownPort")
? "yes" : "no")
.put("use_ssl",
(boolean) input.query("/urls/0/isSsl")
? "yes" : "no")
.put("transfer_preset",""))
)
)
)
)
.put("content_uri","")
.put("session_operation_auth",new JSONObject()
.put("session_operation_auth_by_signature", new JSONObject()
.put("token",input.getString("token"))
.put("signature",input.getString("signature"))
)
)
.put("content_id",input.getString("contentId"))
.put("content_auth",new JSONObject()
.put("auth_type",input.query("/authTypes/http"))
.put("content_key_timeout",input.getInt("contentKeyTimeout"))
.put("service_id","nicovideo")
.put("service_user_id",input.getString("serviceUserId"))
)
.put("client_info",new JSONObject()
.put("player_id",input.getString("playerId"))
)
);
private static JsonBrowser processJSON(JsonBrowser input) throws IOException {
topi314 marked this conversation as resolved.
Show resolved Hide resolved
topi314 marked this conversation as resolved.
Show resolved Hide resolved
JsonBrowser session = JsonBrowser.newMap();
session.put("content_type","movie");
session.put("timing_constraint","unlimited");
session.put("recipe_id",input.get("recipeId"));
session.put("priority",input.get("priority"));
session.put("content_uri","");
session.put("content_id",input.get("contentId"));


JsonBrowser lifetime = JsonBrowser.newMap();
lifetime.put("lifetime",input.get("heartbeatLifetime"));

JsonBrowser heartbeat = JsonBrowser.newMap();
heartbeat.put("heartbeat",lifetime);

session.put("keep_method",heartbeat);


JsonBrowser srcids = JsonBrowser.newMap();
srcids.put("video_src_ids",input.get("videos"));
srcids.put("audio_src_ids",input.get("audios"));

JsonBrowser srcidtomux = JsonBrowser.newMap();
srcidtomux.put("src_id_to_mux",srcids);

JsonBrowser array = JsonBrowser.newList();
array.add(srcidtomux);

JsonBrowser contentsrcids = JsonBrowser.newMap();
contentsrcids.put("content_src_ids",array);

JsonBrowser contentsrcidsets = JsonBrowser.newList();
contentsrcidsets.add(contentsrcids);

session.put("content_src_id_sets", contentsrcidsets);


JsonBrowser http_download_parameters = JsonBrowser.newMap();

if(input.get("urls").index(0).get("isWellKnownPort").asBoolean(false))
http_download_parameters.put("use_well_known_port","yes");
else
http_download_parameters.put("use_well_known_port","no");

if(input.get("urls").index(0).get("isSsl").asBoolean(false))
http_download_parameters.put("use_ssl","yes");
else
http_download_parameters.put("use_ssl","no");

http_download_parameters.put("transfer_preset","");

JsonBrowser innerparameters = JsonBrowser.newMap();
innerparameters.put("http_output_download_parameters", http_download_parameters);

JsonBrowser httpparameters = JsonBrowser.newMap();
httpparameters.put("parameters", innerparameters);

JsonBrowser outerparameters = JsonBrowser.newMap();
outerparameters.put("http_parameters", httpparameters);

JsonBrowser protocol = JsonBrowser.newMap();
protocol.put("name","http");
protocol.put("parameters", outerparameters);

session.put("protocol",protocol);


JsonBrowser session_operation_auth_by_signature = JsonBrowser.newMap();
session_operation_auth_by_signature.put("token",input.get("token"));
session_operation_auth_by_signature.put("signature",input.get("signature"));

JsonBrowser session_operation_auth = JsonBrowser.newMap();
session_operation_auth.put("session_operation_auth_by_signature",session_operation_auth_by_signature);

session.put("session_operation_auth",session_operation_auth);


JsonBrowser contentauth = JsonBrowser.newMap();
contentauth.put("auth_type",input.get("authTypes").get("http"));
contentauth.put("content_key_timeout",input.get("contentKeyTimeout"));
contentauth.put("service_id","nicovideo");
contentauth.put("service_user_id",input.get("serviceUserId"));

session.put("content_auth", contentauth);


JsonBrowser clientinfo = JsonBrowser.newMap();
clientinfo.put("player_id",input.get("playerId"));

session.put("client_info",clientinfo);


JsonBrowser out = JsonBrowser.newMap();
out.put("session",session);

return out;
}

@Override
Expand Down