Skip to content

Commit a7f6ea5

Browse files
committed
api: add initial draft support for nicovideo
1 parent 40da8a4 commit a7f6ea5

File tree

4 files changed

+136
-0
lines changed

4 files changed

+136
-0
lines changed

api/src/processing/match.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import loom from "./services/loom.js";
2929
import facebook from "./services/facebook.js";
3030
import bluesky from "./services/bluesky.js";
3131
import xiaohongshu from "./services/xiaohongshu.js";
32+
import nicovideo from "./services/nicovideo.js";
3233

3334
let freebind;
3435

@@ -268,6 +269,14 @@ export default async function({ host, patternMatch, params, authType }) {
268269
});
269270
break;
270271

272+
case "nicovideo":
273+
r = await nicovideo({
274+
...patternMatch,
275+
dispatcher,
276+
quality: params.videoQuality,
277+
});
278+
break;
279+
271280
default:
272281
return createResponse("error", {
273282
code: "error.api.service.unsupported"

api/src/processing/service-config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ export const services = {
6060
loom: {
6161
patterns: ["share/:id", "embed/:id"],
6262
},
63+
nicovideo: {
64+
patterns: ["watch/:id"],
65+
tld: "jp",
66+
subdomains: ["sp"],
67+
},
6368
ok: {
6469
patterns: [
6570
"video/:id",

api/src/processing/service-patterns.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,6 @@ export const testers = {
7979
"xiaohongshu": pattern =>
8080
pattern.id?.length <= 24 && pattern.token?.length <= 64
8181
|| pattern.shareId?.length <= 24,
82+
83+
"nicovideo": pattern => pattern.id?.length <= 12,
8284
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { genericUserAgent } from "../../config.js";
2+
3+
const genericHeaders = {
4+
"user-agent": genericUserAgent,
5+
"accept-language": "en-US,en;q=0.9",
6+
}
7+
8+
const getVideoInfo = async (id, dispatcher, quality) => {
9+
const html = await fetch(`https://www.nicovideo.jp/watch/${id}`, {
10+
dispatcher,
11+
headers: genericHeaders
12+
}).then(r => r.text()).catch(() => {});
13+
14+
if (!(html.includes("accessRightKey")
15+
|| !(html.includes('<meta name="server-response" content="')))) {
16+
return { error: "fetch.fail" };
17+
}
18+
19+
const rawContent =
20+
html.split('<meta name="server-response" content="')[1]
21+
.split('" />')[0]
22+
.replaceAll("&quot;", '"');
23+
24+
const data = JSON.parse(rawContent)?.data?.response;
25+
26+
if (!data) {
27+
return { error: "fetch.fail" };
28+
}
29+
30+
const audio = data.media?.domand?.audios.find(audio => audio.isAvailable);
31+
const bestVideo = data.media?.domand?.videos.find(video => video.isAvailable);
32+
33+
const preferredVideo = data.media?.domand?.videos.find(video =>
34+
video.isAvailable && video.label.split('p')[0] === quality
35+
);
36+
37+
const video = preferredVideo || bestVideo;
38+
39+
return {
40+
watchTrackId: data.client?.watchTrackId,
41+
accessRightKey: data.media?.domand?.accessRightKey,
42+
43+
video,
44+
45+
outputs: [[video.id, audio.id]],
46+
47+
author: data.owner?.nickname,
48+
title: data.video?.title,
49+
}
50+
}
51+
52+
const getHls = async (dispatcher, id, trackId, accessRightKey, outputs) => {
53+
const response = await fetch(
54+
`https://nvapi.nicovideo.jp/v1/watch/${id}/access-rights/hls?actionTrackId=${trackId}`,
55+
{
56+
method: "POST",
57+
dispatcher,
58+
headers: {
59+
...genericHeaders,
60+
"content-type": "application/json",
61+
"x-access-right-key": accessRightKey,
62+
"x-frontend-id": "6",
63+
"x-frontend-version": "0",
64+
"x-request-with": "nicovideo",
65+
},
66+
body: JSON.stringify({
67+
outputs,
68+
})
69+
}
70+
).then(r => r.json()).catch(() => {});
71+
72+
if (!response?.data?.contentUrl) return;
73+
return response.data.contentUrl;
74+
}
75+
76+
export default async function ({ id, dispatcher, quality }) {
77+
const {
78+
watchTrackId,
79+
accessRightKey,
80+
video,
81+
outputs,
82+
author,
83+
title,
84+
error
85+
} = await getVideoInfo(id, dispatcher, quality);
86+
87+
if (error) {
88+
return { error };
89+
}
90+
91+
if (!watchTrackId || !accessRightKey || !outputs) {
92+
return { error: "fetch.empty" };
93+
}
94+
95+
const hlsUrl = await getHls(
96+
dispatcher,
97+
id,
98+
watchTrackId,
99+
accessRightKey,
100+
outputs
101+
);
102+
103+
if (!hlsUrl) {
104+
return { error: "fetch.empty" };
105+
}
106+
107+
return {
108+
urls: hlsUrl,
109+
isHLS: true,
110+
filenameAttributes: {
111+
service: "nicovideo",
112+
id,
113+
title,
114+
author,
115+
resolution: `${video.width}x${video.height}`,
116+
qualityLabel: `${video.label}`,
117+
extension: "mp4"
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)