Files
RSSHub/lib/routes/pixiv/novel-api/content/nsfw.ts
T
pseudoyu 8aeca4cf73 fix(route/pixiv): read NSFW novel data defined with Object.defineProperty
The app webview page defines its data with
Object.defineProperty(window, 'pixiv', { value: { novel, ... } }).
JSDOM ran that call, but the script data parser does not execute calls,
so window.pixiv was never set and the novel series route failed with
"Unsupported script data expression: CallExpression". Read the value from
the defineProperty call arguments instead.

Closes #23592
2026-10-16 14:36:46 +08:00

78 lines
2.7 KiB
TypeScript

import { load } from 'cheerio';
import queryString from 'query-string';
import cache from '@/utils/cache';
import { evaluateScriptCallback } from '@/utils/evaluate-script';
import { parseDate } from '@/utils/parse-date';
import { maskHeader } from '../../constants';
import got from '../../pixiv-got';
import type { NovelContent, NSFWNovelDetail } from './types';
import { parseNovelContent } from './utils';
export async function getNSFWNovelContent(novelId: string, token: string): Promise<NovelContent> {
return await cache.tryGet(`https://app-api.pixiv.net/webview/v2/novel:${novelId}`, async () => {
const response = await got('https://app-api.pixiv.net/webview/v2/novel', {
headers: {
...maskHeader,
Authorization: 'Bearer ' + token,
},
searchParams: queryString.stringify({
id: novelId,
viewer_version: '20221031_ai',
}),
});
const $ = load(response.data);
const script = $('script')
.toArray()
.map((element) => $(element).text())
.filter((source) => source.includes('pixiv'))
.join('\n');
if (!script) {
throw new Error('No novel data found');
}
// The page defines its data with Object.defineProperty(window, 'pixiv', { value: { novel } })
const novelDetail = (await evaluateScriptCallback<{ value?: { novel?: NSFWNovelDetail } }>(script, 'Object.defineProperty')).value?.novel;
if (!novelDetail) {
throw new Error('No novel data found');
}
const images = Object.fromEntries(
Object.entries(novelDetail.images)
.filter(([, image]) => image?.urls?.original)
.map(([id, image]) => [id, image.urls.original])
);
const parsedContent = await parseNovelContent(novelDetail.text, images, token);
return {
id: novelDetail.id,
title: novelDetail.title,
description: novelDetail.caption,
content: parsedContent,
userId: novelDetail.userId,
userName: null, // Not provided in NSFW API
bookmarkCount: novelDetail.rating.bookmark,
viewCount: novelDetail.rating.view,
likeCount: novelDetail.rating.like,
createDate: parseDate(novelDetail.cdate),
updateDate: null, // Not provided in NSFW API
isOriginal: novelDetail.isOriginal,
aiType: novelDetail.aiType,
tags: novelDetail.tags,
coverUrl: novelDetail.coverUrl,
images,
seriesId: novelDetail.seriesId || null,
seriesTitle: novelDetail.seriesTitle || null,
};
});
}