Skip to content
Open
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
119 changes: 88 additions & 31 deletions plugins/english/relibrary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ class ReLibraryPlugin implements Plugin.PluginBase {
name = 'Re:Library';
icon = 'src/en/relibrary/icon.png';
site = 'https://re-library.com';
version = '1.0.3';
version = '1.0.4';
imageRequestInit: Plugin.ImageRequestInit = {
headers: {
Referer: 'https://re-library.com/',
Expand All @@ -178,49 +178,54 @@ class ReLibraryPlugin implements Plugin.PluginBase {
private async popularNovelsInner(url: string): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
const result = await fetchApi(url);
if (!result.ok) {
throw Object.assign(
new Error(`Could not load ${url} (HTTP ${result.status})`),
{ status: result.status, response: result },
);
}
const body = await result.text();

const loadedCheerio = loadCheerio(body);
loadedCheerio('.entry-content > ol > li').each((_i, el) => {
const novel: Partial<Plugin.NovelItem> = {};
novel.name = loadedCheerio(el).find('h3 > a').text();
novel.path = loadedCheerio(el)
.find('table > tbody > tr > td > a')
.attr('href');
if (novel.name === undefined || novel.path === undefined) return;
novel.cover =
loadedCheerio(el)
.find('table > tbody > tr > td > a > img')
.attr('data-cfsrc') ||
loadedCheerio(el)
.find('table > tbody > tr > td > a > img')
.attr('src') ||
defaultCover;
novel.path = new URL(novel.path, this.site).pathname;
novels.push(novel as Plugin.NovelItem);
});
loadedCheerio('.relibrary-top-novels .relibrary-novel-card').each(
(_i, el) => {
const novel: Partial<Plugin.NovelItem> = {};
const title = loadedCheerio(el).find('.relibrary-title a');
novel.name = title.text().trim();
novel.path = title.attr('href');
if (!novel.name || novel.path === undefined) return;
const cover = loadedCheerio(el).find('img.relibrary-cover');
novel.cover =
cover.attr('data-cfsrc') || cover.attr('src') || defaultCover;
novel.path = new URL(novel.path, this.site).pathname;
novels.push(novel as Plugin.NovelItem);
},
);
return novels;
}

private async lastestNovelsInner(url: string): Promise<Plugin.NovelItem[]> {
const novels: Plugin.NovelItem[] = [];
const result = await fetchApi(url);
if (!result.ok) {
throw Object.assign(
new Error(`Could not load ${url} (HTTP ${result.status})`),
{ status: result.status, response: result },
);
}
const body = await result.text();

const loadedCheerio = loadCheerio(body);
loadedCheerio('article.type-page.page').each((_i, el) => {
const novel: Partial<Plugin.NovelItem> = {};
novel.name = loadedCheerio(el).find('.entry-title').text();
novel.path = loadedCheerio(el).find('.entry-title a').attr('href');
if (novel.path === undefined || novel.name === undefined) return;
// The title heading also wraps the novel card, so only read its link.
const title = loadedCheerio(el).find('.entry-title > a').first();
novel.name = title.text().trim();
novel.path = title.attr('href');
if (!novel.name || novel.path === undefined) return;
const cover = loadedCheerio(el).find('img.relibrary-cover');
novel.cover =
loadedCheerio(el)
.find('.entry-content > table > tbody > tr > td > a >img')
.attr('data-cfsrc') ||
loadedCheerio(el)
.find('.entry-content > table > tbody > tr > td > a >img')
.attr('src') ||
defaultCover;
cover.attr('data-cfsrc') || cover.attr('src') || defaultCover;
novel.path = new URL(novel.path, this.site).pathname;
novels.push(novel as Plugin.NovelItem);
});
Expand Down Expand Up @@ -344,11 +349,32 @@ class ReLibraryPlugin implements Plugin.PluginBase {
async parseChapter(chapterPath: string): Promise<string> {
// parse chapter text here
const result = await fetchApi(`${this.site}${chapterPath}`);
if (!result.ok) {
throw Object.assign(
new Error(
`Could not load chapter ${chapterPath} (HTTP ${result.status})`,
),
{ status: result.status, response: result },
);
}
const body = await result.text();

const loadedCheerio = loadCheerio(body);

const entryContent = loadedCheerio('.entry-content');

// Early access chapters only show a password form until they unlock.
const passwordForm = entryContent.find('form.post-password-form');
if (passwordForm.length) {
const unlock = passwordForm
.find('.ppw-pcp-pf-desc-below-form')
.text()
.trim();
throw new Error(
`This chapter is password protected (early access) on Re:Library${unlock ? ` ${unlock}` : ''}`,
);
}

const pageLinkHr = entryContent.find('.PageLink + hr').first();
if (pageLinkHr.length) {
// Remove all previous siblings before the .PageLink + hr
Expand All @@ -361,7 +387,12 @@ class ReLibraryPlugin implements Plugin.PluginBase {
if (pageLink.length) {
pageLink.remove();
}
pageLinkHr.next().remove();
// The credits table follows the top navigation, either on its own or
// as the first child of a wrapper (div.ppw-restricted-content) that
// holds the whole chapter, so only remove the table itself.
const afterNav = pageLinkHr.next();
if (afterNav.is('table')) afterNav.remove();
else afterNav.children().first().filter('table').remove();
Comment on lines +393 to +395

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Chapter fix lacks regression coverage

The wrapper-preserving fix in parseChapter has no repeatable check. The existing live check opens only the first chapter, but the reported failure affects chapters after Chapter 0. A later cleanup change could therefore bring back blank chapters unnoticed.

Add saved HTML cases for a credits table inside div.ppw-restricted-content, a standalone credits table, and a chapter starting with a paragraph. Check that chapter text survives and credits are removed.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

pageLinkHr.remove();
}

Expand All @@ -378,7 +409,33 @@ class ReLibraryPlugin implements Plugin.PluginBase {
hrAfterPageLink.remove();
}

return entryContent.html() || '';
// The app renders this HTML as is, so drop event handlers and URLs that
// would run script.
entryContent.find('script, style, noscript').remove();
entryContent.find('*').each((_i, el) => {
const node = loadedCheerio(el);
for (const name of Object.keys(el.attribs)) {
if (/^on/i.test(name)) {
node.removeAttr(name);
} else if (name === 'href' || name === 'src') {
const value = el.attribs[name]
.split('')
.filter(c => c.charCodeAt(0) > 32)
.join('')
.toLowerCase();
if (
/^(javascript|vbscript):/.test(value) ||
(value.startsWith('data:') && !value.startsWith('data:image/'))
) {
node.removeAttr(name);
}
}
}
});

const html = entryContent.html()?.trim();
if (!html) throw new Error(`No chapter text found at ${chapterPath}`);
return html;
}

async searchNovels(
Expand Down
Loading