improvement: filenameAttributes and quality picker in nicovideo

This commit is contained in:
mikhail 2024-05-22 22:29:33 +05:00
parent 60f9f99568
commit 6524d4d44c
3 changed files with 63 additions and 32 deletions

View File

@ -187,7 +187,10 @@ export default async function(host, patternMatch, lang, obj) {
r = await dailymotion(patternMatch); r = await dailymotion(patternMatch);
break; break;
case "nicovideo": case "nicovideo":
r = await nicovideo({ id: patternMatch.id }); r = await nicovideo({
id: patternMatch.id,
quality: obj.vQuality
});
break; break;
default: default:
return createResponse("error", { return createResponse("error", {

View File

@ -1,4 +1,5 @@
import { genericUserAgent } from "../../config.js"; import { genericUserAgent } from "../../config.js";
import { cleanString } from "../../sub/utils.js";
import HLS from "hls-parser"; import HLS from "hls-parser";
import util from "node:util"; import util from "node:util";
@ -10,34 +11,34 @@ const NICOVIDEO_EMBED_FRONTEND_HEADERS = {
}; };
const NICOVIDEO_EMBED_URL = "https://embed.nicovideo.jp/watch/%s"; const NICOVIDEO_EMBED_URL = "https://embed.nicovideo.jp/watch/%s";
const NICOVIDEO_AUTHOR_DATA_URL = "https://embed.nicovideo.jp/users/%d";
const NICOVIDEO_GUEST_API_URL = const NICOVIDEO_GUEST_API_URL =
"https://www.nicovideo.jp/api/watch/v3_guest/%s?_frontendId=70&_frontendVersion=0&actionTrackId=%s"; "https://www.nicovideo.jp/api/watch/v3_guest/%s?_frontendId=70&_frontendVersion=0&actionTrackId=%s";
const NICOVIDEO_HLS_API_URL = const NICOVIDEO_HLS_API_URL =
"https://nvapi.nicovideo.jp/v1/watch/%s/access-rights/hls?actionTrackId=%s"; "https://nvapi.nicovideo.jp/v1/watch/%s/access-rights/hls?actionTrackId=%s";
const ACTION_TRACK_ID_REGEXP = async function getBasicVideoInformation(id) {
/"actionTrackId":"[A-Za-z0-9]+_[0-9]+"/;
async function getActionTrackId(id) {
const page = await fetch(util.format(NICOVIDEO_EMBED_URL, id), { const page = await fetch(util.format(NICOVIDEO_EMBED_URL, id), {
headers: { "user-agent": genericUserAgent }, headers: { "user-agent": genericUserAgent },
}).then((response) => response.text()); }).then((response) => response.text());
if (!ACTION_TRACK_ID_REGEXP.test(page)) { const data = JSON.parse(
throw new Error(); // we can't fetch the embed page page
} .split('data-props="')
const actionTrackId = page
// getting the regexp results
.match(ACTION_TRACK_ID_REGEXP)
.shift()
// getting the actionTrackId field's value
.split(":"")
.pop() .pop()
// cleaning from double quotation mark .split('" data-style-map="')
.replaceAll(""", ""); .shift()
.replaceAll(""", '"')
);
return actionTrackId; const author = await fetch(
util.format(NICOVIDEO_AUTHOR_DATA_URL, data.videoUploaderId),
{
headers: { "user-agent": genericUserAgent },
}
).then((response) => response.json());
return { ...data, author };
} }
async function fetchGuestData(id, actionTrackId) { async function fetchGuestData(id, actionTrackId) {
@ -90,34 +91,60 @@ async function fetchContentURL(id, actionTrackId, accessRightKey, outputs) {
return data.data.contentUrl; return data.data.contentUrl;
} }
async function getHighestQualityHLS(contentURL) { async function getHLSContent(contentURL, quality) {
const hls = await fetch(contentURL) const hls = await fetch(contentURL)
.then((response) => response.text()) .then((response) => response.text())
.then((response) => HLS.parse(response)); .then((response) => HLS.parse(response));
const highestQualityHLS = hls.variants const height = quality === "max" ? 9000 : parseInt(quality, 10);
let hlsContent = hls.variants.find(
(variant) => variant.resolution.height === height
);
if (hlsContent === undefined) {
hlsContent = hls.variants
.sort( .sort(
(firstVariant, secondVariant) => (firstVariant, secondVariant) =>
firstVariant.bandwidth - secondVariant.bandwidth firstVariant.bandwidth - secondVariant.bandwidth
) )
.pop(); .shift();
return [highestQualityHLS.uri, highestQualityHLS.audio.pop().uri];
} }
export default async function nicovideo({ id }) { return {
resolution: hlsContent.resolution,
urls: [hlsContent.uri, hlsContent.audio.pop().uri],
};
}
// TODO @synzr only audio support
// TODO @synzr better error handling
export default async function nicovideo({ id, quality }) {
try { try {
const actionTrackId = await getActionTrackId(id); const { actionTrackId, title, author } = await getBasicVideoInformation(id);
const [video, audio] = await fetchGuestData(id, actionTrackId) const {
resolution,
urls: [video, audio],
} = await fetchGuestData(id, actionTrackId)
.then(({ accessRightKey, outputs }) => .then(({ accessRightKey, outputs }) =>
fetchContentURL(id, actionTrackId, accessRightKey, outputs) fetchContentURL(id, actionTrackId, accessRightKey, outputs)
) )
.then((contentURL) => getHighestQualityHLS(contentURL)); .then((contentURL) => getHLSContent(contentURL, quality));
return { return {
urls: [video, audio], urls: [video, audio],
// TODO @synzr get video information from embed page props filenameAttributes: {
filenameAttributes: { service: "nicovideo", id, extension: "mp4" }, service: "nicovideo",
id,
title,
author: author.nickname,
resolution: `${resolution.width}x${resolution.height}`,
qualityLabel: `${resolution.height}p`,
extension: "mp4",
},
fileMetadata: {
title: cleanString(title.trim()),
artist: cleanString(author.nickname.trim()),
},
}; };
} catch (error) { } catch (error) {
return { error: "ErrorEmptyDownload" }; return { error: "ErrorEmptyDownload" };

View File

@ -115,6 +115,7 @@
"alias": "niconico videos", "alias": "niconico videos",
"tld": "jp", "tld": "jp",
"patterns": ["watch/:id"], "patterns": ["watch/:id"],
"subdomains": ["www", "sp", "embed"],
"enabled": true "enabled": true
} }
} }