mirror of
https://github.com/imputnet/cobalt.git
synced 2025-07-16 10:18:28 +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 instagram from "./services/instagram.js";
|
||||||
import vine from "./services/vine.js";
|
import vine from "./services/vine.js";
|
||||||
import pinterest from "./services/pinterest.js";
|
import pinterest from "./services/pinterest.js";
|
||||||
|
import nicovideo from "./services/nicovideo.js";
|
||||||
|
|
||||||
export default async function (host, patternMatch, url, lang, obj) {
|
export default async function (host, patternMatch, url, lang, obj) {
|
||||||
try {
|
try {
|
||||||
@ -114,6 +115,9 @@ export default async function (host, patternMatch, url, lang, obj) {
|
|||||||
case "pinterest":
|
case "pinterest":
|
||||||
r = await pinterest({ id: patternMatch["id"] });
|
r = await pinterest({ id: patternMatch["id"] });
|
||||||
break;
|
break;
|
||||||
|
case "nicovideo":
|
||||||
|
r = await nicovideo({ id: patternMatch["id"] });
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return apiJSON(0, { t: errorUnsupported(lang) });
|
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",
|
"alias": "pinterest videos & stories",
|
||||||
"patterns": ["pin/:id"],
|
"patterns": ["pin/:id"],
|
||||||
"enabled": true
|
"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),
|
"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.mute) args.push('-an');
|
||||||
if (streamInfo.service === "vimeo") args.push('-bsf:a', 'aac_adtstoasc');
|
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');
|
args.push('-f', format, 'pipe:3');
|
||||||
const ffmpegProcess = spawn(ffmpeg, args, {
|
const ffmpegProcess = spawn(ffmpeg, args, {
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
|
Loading…
Reference in New Issue
Block a user