Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release Pixiv & SIS001 #1

Merged
merged 8 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions lib/routes/pixiv/api/get-novel-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import got from '../pixiv-got';
import { maskHeader } from '../constants';
import queryString from 'query-string';

export default function getNovelContent(id, token) {
return got('https://app-api.pixiv.net/webview/v2/novel', {
headers: {
...maskHeader,
Authorization: 'Bearer ' + token,
},
searchParams: queryString.stringify({
id,
}),
});
}
16 changes: 16 additions & 0 deletions lib/routes/pixiv/api/get-novel-series.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import got from '../pixiv-got';
import { maskHeader } from '../constants';
import queryString from 'query-string';

export default function getNovelSeries(series_id, last_order, token) {
return got('https://app-api.pixiv.net/v2/novel/series', {
headers: {
...maskHeader,
Authorization: 'Bearer ' + token,
},
searchParams: queryString.stringify({
series_id,
last_order,
}),
});
}
15 changes: 15 additions & 0 deletions lib/routes/pixiv/api/get-user-novels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import got from '../pixiv-got';
import { maskHeader } from '../constants';
import queryString from 'query-string';

export default function getUserNovels(user_id, token) {
return got('https://app-api.pixiv.net/v1/user/novels', {
headers: {
...maskHeader,
Authorization: 'Bearer ' + token,
},
searchParams: queryString.stringify({
user_id,
}),
});
}
102 changes: 102 additions & 0 deletions lib/routes/pixiv/novel-series.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Route } from '@/types';
import { parseDate } from '@/utils/parse-date';
import { config } from '@/config';
import cache from '@/utils/cache';
import { getToken } from './token';
import getNovelSeries from './api/get-novel-series';
import getNovelContent from './api/get-novel-content';

const baseUrl = 'https://www.pixiv.net';
const novelTextRe = /"text":"(.+?[^\\])"/;

export const route: Route = {
path: '/novel/series/:id/:lang?',
categories: ['social-media'],
example: '/pixiv/user/novels/1394738',
parameters: {
id: "Novel series id, available in novel series' homepage URL",
lang: 'IETF BCP 47 language tag that helps RSS readers choose the right font',
},
features: {
requireConfig: false,
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
radar: {
source: ['www.pixiv.net/novel/series/:id'],
},
name: 'Novel Series',
maintainers: ['keocheung'],
handler,
};

async function handler(ctx) {
if (!config.pixiv || !config.pixiv.refreshToken) {
throw new Error('pixiv RSS is disabled due to the lack of <a href="https://docs.rsshub.app/install/#pei-zhi-bu-fen-rss-mo-kuai-pei-zhi">relevant config</a>');
}

const id = ctx.req.param('id');
let limit = Number.parseInt(ctx.req.query('limit')) || 10;
if (limit > 30) {
limit = 30;
}
const token = await getToken(cache.tryGet);
if (!token) {
throw new Error('pixiv not login');
}

let novelSeriesResponse = await getNovelSeries(id, 0, token);
const contentCount = Number.parseInt(novelSeriesResponse.data.novel_series_detail.content_count);
if (contentCount > limit) {
novelSeriesResponse = await getNovelSeries(id, contentCount - limit, token);
}

const novels = novelSeriesResponse.data.novels.reverse().map((novel) => {
const tags = novel.tags.map((tag) => `<a href="${baseUrl}/tags/${tag.name}/novels">#${tag.name}</a>`).join('<span>&nbsp;&nbsp;</span>');
const item = {
novelId: novel.id,
novelCaption: tags,
title: novel.title,
author: novel.user.name,
pubDate: parseDate(novel.create_date),
link: `https://www.pixiv.net/novel/show.php?id=${novel.id}`,
};
if (novel.caption) {
item.novelCaption = `${novel.caption}<br><br>${tags}`;
}
return item;
});

let langDivLeft = '';
let langDivRight = '';
const lang = ctx.req.param('lang');
if (lang) {
langDivLeft = `<div lang="${lang}">`;
langDivRight = '</div>';
}
const items = await Promise.all(
novels.map((novel) =>
cache.tryGet(novel.link, async () => {
const content = await getNovelContent(novel.novelId, token);
const rawText = novelTextRe.exec(content.data)[1];
novel.description = `${langDivLeft}<blockquote>${novel.novelCaption}</blockquote><p>${unescape(rawText.replaceAll('\\u', '%u'))}</p>${langDivRight}`

Check failure

Code scanning / ESLint

Prefer using the `String.raw` tag to avoid escaping `\`. Error

String.raw should be used to avoid escaping \.
.replaceAll('\\n', '</p><p>')

Check failure

Code scanning / ESLint

Prefer using the `String.raw` tag to avoid escaping `\`. Error

String.raw should be used to avoid escaping \.
.replaceAll('\\t', '\t')

Check failure

Code scanning / ESLint

Prefer using the `String.raw` tag to avoid escaping `\`. Error

String.raw should be used to avoid escaping \.
.replaceAll('\\', '')
.replaceAll(/\[\[rb:(.+?) > (.+?)]]/g, '<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>')
.replaceAll(/\[pixivimage:(\d+-\d+)]/g, `<p><img src="${config.pixiv.imgProxy}/$1.jpg"></p>`);
return novel;
})
)
);

return {
title: novelSeriesResponse.data.novel_series_detail.title,
link: `https://www.pixiv.net/novel/series/${id}`,
description: novelSeriesResponse.data.novel_series_detail.caption,
item: items,
};
}
79 changes: 77 additions & 2 deletions lib/routes/pixiv/novels.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { Route } from '@/types';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
import { config } from '@/config';
import cache from '@/utils/cache';
import { getToken } from './token';
import getUserNovels from './api/get-user-novels';
import getNovelContent from './api/get-novel-content';

const baseUrl = 'https://www.pixiv.net';
const novelTextRe = /"text":"(.+?[^\\])"/;

export const route: Route = {
path: '/user/novels/:id',
path: '/user/novels/:id/:lang?',
categories: ['social-media'],
example: '/pixiv/user/novels/27104704',
parameters: { id: "User id, available in user's homepage URL" },
parameters: {
id: "Novel series id, available in novel series' homepage URL",
lang: 'IETF BCP 47 language tag that helps RSS readers choose the right font',
},
features: {
requireConfig: false,
requirePuppeteer: false,
Expand All @@ -27,6 +37,71 @@
};

async function handler(ctx) {
if (!config.pixiv || !config.pixiv.refreshToken) {
return handleWeb(ctx);
}

const id = ctx.req.param('id');
const limit = Number.parseInt(ctx.req.query('limit')) || 10;
const token = await getToken(cache.tryGet);
if (!token) {
throw new Error('pixiv not login');
}

const userNovelsResponse = await getUserNovels(id, token);
const username = userNovelsResponse.data.user.name;
const novels = userNovelsResponse.data.novels.slice(0, limit).map((novel) => {
let title = novel.title;
if (novel.series.id) {
title = `${novel.series.title} - ${novel.title}`;
}
const tags = novel.tags.map((tag) => `<a href="${baseUrl}/tags/${tag.name}/novels">#${tag.name}</a>`).join('<span>&nbsp;&nbsp;</span>');
const item = {
novelId: novel.id,
novelCaption: tags,
title,
author: username,
pubDate: parseDate(novel.create_date),
link: `https://www.pixiv.net/novel/show.php?id=${novel.id}`,
};
if (novel.caption) {
item.novelCaption = `${novel.caption}<br><br>${tags}`;
}
return item;
});

let langDivLeft = '';
let langDivRight = '';
const lang = ctx.req.param('lang');
if (lang) {
langDivLeft = `<div lang="${lang}">`;
langDivRight = '</div>';
}
const items = await Promise.all(
novels.map((novel) =>
cache.tryGet(novel.link, async () => {
const content = await getNovelContent(novel.novelId, token);
const rawText = novelTextRe.exec(content.data)[1];
novel.description = `${langDivLeft}<blockquote>${novel.novelCaption}</blockquote><p>${unescape(rawText.replaceAll('\\u', '%u'))}</p>${langDivRight}`

Check failure

Code scanning / ESLint

Prefer using the `String.raw` tag to avoid escaping `\`. Error

String.raw should be used to avoid escaping \.
.replaceAll('\\n', '</p><p>')

Check failure

Code scanning / ESLint

Prefer using the `String.raw` tag to avoid escaping `\`. Error

String.raw should be used to avoid escaping \.
.replaceAll('\\t', '\t')

Check failure

Code scanning / ESLint

Prefer using the `String.raw` tag to avoid escaping `\`. Error

String.raw should be used to avoid escaping \.
.replaceAll('\\', '')
.replaceAll(/\[\[rb:(.+?) > (.+?)]]/g, '<ruby>$1<rp>(</rp><rt>$2</rt><rp>)</rp></ruby>')
.replaceAll(/\[pixivimage:(\d+-\d+)]/g, `<p><img src="${config.pixiv.imgProxy}/$1.jpg"></p>`);
return novel;
})
)
);

return {
title: `${username}'s Novels`,
link: `https://www.pixiv.net/users/${id}/novels`,
description: `${username}'s Novels`,
item: items,
};
}

async function handleWeb(ctx) {
const id = ctx.req.param('id');
const { limit = 100 } = ctx.req.query();
const url = `${baseUrl}/users/${id}/novels`;
Expand Down
Loading