mirror of
https://github.com/imputnet/cobalt.git
synced 2025-07-14 01:08:27 +00:00
nicovideo: basic niconico video support
This commit is contained in:
parent
0848923cc7
commit
4015799fdb
@ -18,6 +18,7 @@ import soundcloud from "./services/soundcloud.js";
|
||||
import instagram from "./services/instagram.js";
|
||||
import vine from "./services/vine.js";
|
||||
import pinterest from "./services/pinterest.js";
|
||||
import nicovideo from "./services/nicovideo.js";
|
||||
|
||||
export default async function (host, patternMatch, url, lang, obj) {
|
||||
try {
|
||||
@ -114,6 +115,9 @@ export default async function (host, patternMatch, url, lang, obj) {
|
||||
case "pinterest":
|
||||
r = await pinterest({ id: patternMatch["id"] });
|
||||
break;
|
||||
case "nicovideo":
|
||||
r = await nicovideo({ id: patternMatch["id"] });
|
||||
break;
|
||||
default:
|
||||
return apiJSON(0, { t: errorUnsupported(lang) });
|
||||
}
|
||||
|
130
src/modules/processing/services/nicovideo.js
Normal file
130
src/modules/processing/services/nicovideo.js
Normal file
@ -0,0 +1,130 @@
|
||||
import { genericUserAgent, maxVideoDuration } from "../../config.js";
|
||||
|
||||
/**
|
||||
* Regex for parsing the HTML element containing the JSON object used to
|
||||
* authenticate the request for the video stream.
|
||||
*/
|
||||
const JS_INITIAL_WATCH_DATA_REGEX = /<div[^>]+id="js-initial-watch-data"[^>]+data-api-data="([^"]+)"/;
|
||||
|
||||
/**
|
||||
* Undo html escaping. e.g. `"` -> `"`
|
||||
*
|
||||
* @param {string} input Possibly escaped string
|
||||
*
|
||||
* @returns `input` with escaped characters un-escaped
|
||||
*/
|
||||
const unescapeHtml = input => {
|
||||
return input
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll(""", "\"")
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
export default async function(obj) {
|
||||
// Step 1: We need to retrieve a JSON object from an element with ID
|
||||
// `js-initial-watch-data`.
|
||||
const videoPageBody = await fetch(`https://www.nicovideo.jp/watch/${obj.id}`, {
|
||||
headers: { "user-agent": genericUserAgent }
|
||||
}).then(res => res.text());
|
||||
|
||||
const matches = JS_INITIAL_WATCH_DATA_REGEX.exec(videoPageBody);
|
||||
if (matches?.length < 2) return { error: 'ErrorCouldntFetch' };
|
||||
|
||||
const jsonStr = unescapeHtml(matches[1]);
|
||||
|
||||
let jsInitialWatchData = null;
|
||||
try {
|
||||
jsInitialWatchData = JSON.parse(jsonStr);
|
||||
} catch (_err) {
|
||||
return { error: 'ErrorCouldntFetch' };
|
||||
}
|
||||
|
||||
const { video } = jsInitialWatchData;
|
||||
if (!video || !video.duration) {
|
||||
return { error: 'ErrorCouldntFetch' };
|
||||
}
|
||||
if (video.duration > maxVideoDuration * 1000) {
|
||||
return { error: ['ErrorLengthLimit', maxVideoDuration / 60000] };
|
||||
}
|
||||
|
||||
// Step 2: We use the JSON object to send an HTTP request to retrieve
|
||||
// the m3u8 for the video stream.
|
||||
const { session } = jsInitialWatchData?.media?.delivery?.movie;
|
||||
if (!session) {
|
||||
return { error: 'ErrorCouldntFetch' };
|
||||
}
|
||||
const body = {
|
||||
session: {
|
||||
client_info: {
|
||||
player_id: session.playerId,
|
||||
},
|
||||
content_auth: {
|
||||
auth_type: session.authTypes.http,
|
||||
content_key_timeout: 600000,
|
||||
service_id: "nicovideo",
|
||||
service_user_id: session.serviceUserId
|
||||
},
|
||||
content_id: session.contentId,
|
||||
content_src_id_sets: [{
|
||||
content_src_ids: [{
|
||||
src_id_to_mux: {
|
||||
video_src_ids: session.videos,
|
||||
audio_src_ids: session.audios
|
||||
}
|
||||
}]
|
||||
}],
|
||||
content_type: "movie",
|
||||
content_uri: "",
|
||||
keep_method: {
|
||||
heartbeat: {
|
||||
lifetime: 120000
|
||||
}
|
||||
},
|
||||
priority: 0,
|
||||
protocol: {
|
||||
name: "http",
|
||||
parameters: {
|
||||
http_parameters: {
|
||||
parameters: {
|
||||
hls_parameters: {
|
||||
use_well_known_port: "yes",
|
||||
use_ssl: "yes",
|
||||
transfer_preset: "",
|
||||
segment_duration: 6000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
recipe_id: session.recipeId,
|
||||
session_operation_auth: {
|
||||
session_operation_auth_by_signature: {
|
||||
signature: session.signature,
|
||||
token: session.token
|
||||
}
|
||||
},
|
||||
timing_constraint: "unlimited"
|
||||
}
|
||||
};
|
||||
const res = await fetch("https://api.dmc.nico/api/sessions?_format=json", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body)
|
||||
}).then(res => res.json());
|
||||
|
||||
if (res?.meta?.status !== 201) {
|
||||
return { error: 'ErrorCouldntFetch' };
|
||||
}
|
||||
|
||||
const playlistUri = res?.data?.session?.content_uri;
|
||||
if (!playlistUri) {
|
||||
return { error: 'ErrorCouldntFetch' };
|
||||
}
|
||||
|
||||
return {
|
||||
urls: playlistUri,
|
||||
isM3U8: true,
|
||||
filename: `nicovideo_${obj.id}.mp4`
|
||||
}
|
||||
}
|
@ -67,6 +67,12 @@
|
||||
"alias": "pinterest videos & stories",
|
||||
"patterns": ["pin/:id"],
|
||||
"enabled": true
|
||||
},
|
||||
"nicovideo": {
|
||||
"alias": "niconico videos",
|
||||
"patterns": ["watch/:id"],
|
||||
"enabled": true,
|
||||
"tld": "jp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,5 +30,6 @@ export const testers = {
|
||||
|
||||
"vine": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 12),
|
||||
|
||||
"pinterest": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 128)
|
||||
"pinterest": (patternMatch) => (patternMatch["id"] && patternMatch["id"].length <= 128),
|
||||
"nicovideo": (patternMatch) => (patternMatch["id"] && patternMatch["id"].startsWith("sm") && patternMatch["id"].length === 10)
|
||||
}
|
||||
|
@ -148,7 +148,15 @@ export function streamVideoOnly(streamInfo, res) {
|
||||
]
|
||||
if (streamInfo.mute) args.push('-an');
|
||||
if (streamInfo.service === "vimeo") args.push('-bsf:a', 'aac_adtstoasc');
|
||||
if (format === "mp4") args.push('-movflags', 'faststart+frag_keyframe+empty_moov');
|
||||
if (format === "mp4") {
|
||||
// for some reason, downloading nicovideo with the `empty_moov` flag
|
||||
// will only download one second of video
|
||||
// TODO: figure out why
|
||||
const movflags = streamInfo.service === 'nicovideo'
|
||||
? 'faststart+frag_keyframe'
|
||||
: 'faststart+frag_keyframe+empty_moov';
|
||||
args.push('-movflags', movflags);
|
||||
}
|
||||
args.push('-f', format, 'pipe:3');
|
||||
const ffmpegProcess = spawn(ffmpeg, args, {
|
||||
windowsHide: true,
|
||||
|
Loading…
Reference in New Issue
Block a user