@@ -2,7 +2,7 @@ import { fetchApi } from '@libs/fetch';
|
|||||||
import { Filters, FilterTypes } from '@libs/filterInputs';
|
import { Filters, FilterTypes } from '@libs/filterInputs';
|
||||||
import { Plugin } from '@/types/plugin';
|
import { Plugin } from '@/types/plugin';
|
||||||
import { NovelStatus } from '@libs/novelStatus';
|
import { NovelStatus } from '@libs/novelStatus';
|
||||||
import { load as parseHTML } from 'cheerio';
|
import { Parser } from 'htmlparser2';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
export type IfreedomMetadata = {
|
export type IfreedomMetadata = {
|
||||||
@@ -25,10 +25,55 @@ class IfreedomPlugin implements Plugin.PluginBase {
|
|||||||
this.name = metadata.sourceName;
|
this.name = metadata.sourceName;
|
||||||
this.icon = `multisrc/ifreedom/${metadata.id.toLowerCase()}/icon.png`;
|
this.icon = `multisrc/ifreedom/${metadata.id.toLowerCase()}/icon.png`;
|
||||||
this.site = metadata.sourceSite;
|
this.site = metadata.sourceSite;
|
||||||
this.version = '1.0.2';
|
this.version = '1.1.0';
|
||||||
this.filters = metadata.filters;
|
this.filters = metadata.filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parseNovels(url: string) {
|
||||||
|
return fetchApi(url)
|
||||||
|
.then(res => res.text())
|
||||||
|
.then(html => {
|
||||||
|
const novels: Plugin.NovelItem[] = [];
|
||||||
|
let tempNovel = {} as Plugin.NovelItem;
|
||||||
|
let isInsideNovelCard = false;
|
||||||
|
const site = this.site;
|
||||||
|
|
||||||
|
const parser = new Parser({
|
||||||
|
onopentag(name, attribs) {
|
||||||
|
const className = attribs['class'] || '';
|
||||||
|
if (
|
||||||
|
name === 'div' &&
|
||||||
|
(className.includes('one-book-home') ||
|
||||||
|
className.includes('item-book-slide'))
|
||||||
|
) {
|
||||||
|
isInsideNovelCard = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInsideNovelCard) {
|
||||||
|
if (name === 'img') {
|
||||||
|
tempNovel.cover = attribs['src'];
|
||||||
|
if (attribs['alt']) tempNovel.name = attribs['alt'];
|
||||||
|
}
|
||||||
|
if (name === 'a' && attribs['href']) {
|
||||||
|
tempNovel.path = attribs['href'].replace(site, '');
|
||||||
|
if (attribs['title']) tempNovel.name = attribs['title'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onclosetag(name) {
|
||||||
|
if (name === 'div' && isInsideNovelCard) {
|
||||||
|
isInsideNovelCard = false;
|
||||||
|
if (tempNovel.path) novels.push(tempNovel);
|
||||||
|
tempNovel = {} as Plugin.NovelItem;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
parser.write(html);
|
||||||
|
parser.end();
|
||||||
|
return novels;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async popularNovels(
|
async popularNovels(
|
||||||
page: number,
|
page: number,
|
||||||
{
|
{
|
||||||
@@ -36,101 +81,208 @@ class IfreedomPlugin implements Plugin.PluginBase {
|
|||||||
showLatestNovels,
|
showLatestNovels,
|
||||||
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
let url =
|
let url = `${this.site}/vse-knigi/?sort=${showLatestNovels ? 'По дате обновления' : filters?.sort?.value || 'По рейтингу'}`;
|
||||||
this.site +
|
|
||||||
'/vse-knigi/?sort=' +
|
|
||||||
(showLatestNovels
|
|
||||||
? 'По дате обновления'
|
|
||||||
: filters?.sort?.value || 'По рейтингу');
|
|
||||||
|
|
||||||
Object.entries(filters || {}).forEach(([type, { value }]) => {
|
Object.entries(filters || {}).forEach(([type, { value }]) => {
|
||||||
if (value instanceof Array && value.length) {
|
if (Array.isArray(value) && value.length) {
|
||||||
url += '&' + type + '[]=' + value.join('&' + type + '[]=');
|
url += `&${type}[]=${value.join(`&${type}[]=`)}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
url += '&bpage=' + page;
|
url += `&bpage=${page}`;
|
||||||
|
return this.parseNovels(url);
|
||||||
const body = await fetchApi(url).then(res => res.text());
|
|
||||||
const loadedCheerio = parseHTML(body);
|
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = loadedCheerio(
|
|
||||||
'div.one-book-home > div.img-home a',
|
|
||||||
)
|
|
||||||
.map((index, element) => ({
|
|
||||||
name: loadedCheerio(element).attr('title') || '',
|
|
||||||
cover: loadedCheerio(element).find('img').attr('src'),
|
|
||||||
path:
|
|
||||||
loadedCheerio(element).attr('href')?.replace?.(this.site, '') || '',
|
|
||||||
}))
|
|
||||||
.get()
|
|
||||||
.filter(novel => novel.name && novel.path);
|
|
||||||
|
|
||||||
return novels;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
||||||
const body = await fetchApi(this.site + novelPath).then(res => res.text());
|
const html = await fetchApi(this.site + novelPath).then(res => res.text());
|
||||||
const loadedCheerio = parseHTML(body);
|
|
||||||
|
|
||||||
const novel: Plugin.SourceNovel = {
|
const novel: Plugin.SourceNovel = {
|
||||||
path: novelPath,
|
path: novelPath,
|
||||||
name: loadedCheerio('.entry-title').text(),
|
name: '',
|
||||||
cover: loadedCheerio('.img-ranobe > img').attr('src'),
|
author: '',
|
||||||
summary: loadedCheerio('meta[name="description"]').attr('content'),
|
summary: '',
|
||||||
|
status: NovelStatus.Unknown,
|
||||||
};
|
};
|
||||||
|
|
||||||
loadedCheerio('div.data-ranobe').each(function () {
|
|
||||||
switch (loadedCheerio(this).find('b').text()) {
|
|
||||||
case 'Автор':
|
|
||||||
novel.author = loadedCheerio(this)
|
|
||||||
.find('div.data-value')
|
|
||||||
.text()
|
|
||||||
.trim();
|
|
||||||
break;
|
|
||||||
case 'Жанры':
|
|
||||||
novel.genres = loadedCheerio('div.data-value > a')
|
|
||||||
.map((index, element) => loadedCheerio(element).text()?.trim())
|
|
||||||
.get()
|
|
||||||
.join(',');
|
|
||||||
break;
|
|
||||||
case 'Статус книги':
|
|
||||||
novel.status = loadedCheerio('div.data-value')
|
|
||||||
.text()
|
|
||||||
.includes('активен')
|
|
||||||
? NovelStatus.Ongoing
|
|
||||||
: NovelStatus.Completed;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (novel.author == 'Не указан') delete novel.author;
|
|
||||||
|
|
||||||
const chapters: Plugin.ChapterItem[] = [];
|
const chapters: Plugin.ChapterItem[] = [];
|
||||||
const totalChapters = loadedCheerio('div.li-ranobe').length;
|
const genres: string[] = [];
|
||||||
|
const site = this.site;
|
||||||
|
|
||||||
loadedCheerio('div.li-ranobe').each((chapterIndex, element) => {
|
let isReadingName = false;
|
||||||
const name = loadedCheerio(element).find('a').text();
|
let isReadingSummary = false;
|
||||||
const url = loadedCheerio(element).find('a').attr('href');
|
let isCoverContainer = false;
|
||||||
|
|
||||||
|
let metaContext: 'author' | 'status' | 'genre' | null = null;
|
||||||
|
let isMetaRow = false;
|
||||||
|
let isMetaValue = false;
|
||||||
|
|
||||||
|
let isInsideChapterRow = false;
|
||||||
|
let isReadingChapterName = false;
|
||||||
|
let isReadingChapterDate = false;
|
||||||
|
let tempChapter = {} as Plugin.ChapterItem;
|
||||||
|
|
||||||
|
const parser = new Parser({
|
||||||
|
onopentag(name, attribs) {
|
||||||
|
const className = attribs['class'] || '';
|
||||||
|
|
||||||
|
if (name === 'h1') isReadingName = true;
|
||||||
|
|
||||||
|
if (name === 'div') {
|
||||||
if (
|
if (
|
||||||
!loadedCheerio(element).find('label.buy-ranobe').length &&
|
className.includes('block-book-slide-img') ||
|
||||||
name &&
|
className.includes('img-ranobe')
|
||||||
url
|
|
||||||
) {
|
) {
|
||||||
const releaseDate = loadedCheerio(element)
|
isCoverContainer = true;
|
||||||
.find('div.li-col2-ranobe')
|
|
||||||
.text()
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
chapters.push({
|
|
||||||
name,
|
|
||||||
path: url.replace(this.site, ''),
|
|
||||||
releaseTime: this.parseDate(releaseDate),
|
|
||||||
chapterNumber: totalChapters - chapterIndex,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
className === 'descr-ranobe' ||
|
||||||
|
(className === 'active' && attribs['data-name'] === 'Описание')
|
||||||
|
) {
|
||||||
|
isReadingSummary = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isReadingSummary &&
|
||||||
|
name === 'span' &&
|
||||||
|
className.includes('open-desc')
|
||||||
|
) {
|
||||||
|
const onclick = attribs['onclick'];
|
||||||
|
if (onclick) {
|
||||||
|
const match = onclick.match(/innerHTML\s*=\s*'([\s\S]+?)'/);
|
||||||
|
if (match && match[1]) {
|
||||||
|
let fullText = match[1];
|
||||||
|
fullText = fullText
|
||||||
|
.replace(/<br>/gi, '\n')
|
||||||
|
.replace(/<br\s*\/?>/gi, '\n')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/&/g, '&');
|
||||||
|
|
||||||
|
novel.summary = fullText;
|
||||||
|
isReadingSummary = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'img' && isCoverContainer && !novel.cover) {
|
||||||
|
novel.cover = attribs['src'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'div') {
|
||||||
|
if (className.includes('data-ranobe')) {
|
||||||
|
isMetaRow = true;
|
||||||
|
metaContext = null;
|
||||||
|
}
|
||||||
|
if (className.includes('data-value')) {
|
||||||
|
isMetaValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (className.includes('book-info-list')) {
|
||||||
|
isMetaRow = true;
|
||||||
|
metaContext = null;
|
||||||
|
}
|
||||||
|
if (className.includes('genreslist')) {
|
||||||
|
metaContext = 'genre';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMetaRow) {
|
||||||
|
if (name === 'span') {
|
||||||
|
if (
|
||||||
|
className.includes('dashicons-book') &&
|
||||||
|
!className.includes('book-alt')
|
||||||
|
)
|
||||||
|
metaContext = 'genre';
|
||||||
|
else if (className.includes('admin-users')) metaContext = 'author';
|
||||||
|
else if (className.includes('megaphone')) metaContext = 'status';
|
||||||
|
}
|
||||||
|
if (name === 'svg') {
|
||||||
|
if (className.includes('icon-tabler-tag')) metaContext = 'genre';
|
||||||
|
else if (
|
||||||
|
className.includes('mood-edit') ||
|
||||||
|
className.includes('icon-tabler-user')
|
||||||
|
)
|
||||||
|
metaContext = 'author';
|
||||||
|
else if (
|
||||||
|
className.includes('chart-infographic') ||
|
||||||
|
className.includes('megaphone')
|
||||||
|
)
|
||||||
|
metaContext = 'status';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
name === 'div' &&
|
||||||
|
(className === 'li-ranobe' || className === 'chapterinfo')
|
||||||
|
) {
|
||||||
|
isInsideChapterRow = true;
|
||||||
|
}
|
||||||
|
if (name === 'a' && isInsideChapterRow) {
|
||||||
|
tempChapter.path = attribs['href'].replace(site, '');
|
||||||
|
isReadingChapterName = true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(name === 'div' || name === 'span') &&
|
||||||
|
(className === 'li-col2-ranobe' || className === 'timechapter')
|
||||||
|
) {
|
||||||
|
isReadingChapterDate = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ontext(data) {
|
||||||
|
const text = data.trim();
|
||||||
|
if (!text) return;
|
||||||
|
|
||||||
|
if (isReadingName) novel.name = text.replace(/®/g, '').trim();
|
||||||
|
if (isReadingSummary && text !== 'Прочесть полностью') {
|
||||||
|
novel.summary += text + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metaContext) {
|
||||||
|
const shouldRead = isMetaValue || (isMetaRow && !isMetaValue);
|
||||||
|
if (shouldRead) {
|
||||||
|
if (metaContext === 'author') {
|
||||||
|
if (
|
||||||
|
text !== 'Автор' &&
|
||||||
|
text !== 'Переводчик' &&
|
||||||
|
text !== 'Не указан' &&
|
||||||
|
!text.includes('Просмотров')
|
||||||
|
) {
|
||||||
|
novel.author = text;
|
||||||
|
}
|
||||||
|
} else if (metaContext === 'status') {
|
||||||
|
if (!text.includes('Статус')) novel.status = parseStatus(text);
|
||||||
|
} else if (metaContext === 'genre') {
|
||||||
|
if (text !== ',' && text !== 'Жанры') genres.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReadingChapterName) tempChapter.name = text;
|
||||||
|
if (isReadingChapterDate) tempChapter.releaseTime = parseDate(text);
|
||||||
|
},
|
||||||
|
onclosetag(name) {
|
||||||
|
if (name === 'h1') isReadingName = false;
|
||||||
|
if (name === 'div') {
|
||||||
|
if (isReadingSummary) isReadingSummary = false;
|
||||||
|
if (isCoverContainer) isCoverContainer = false;
|
||||||
|
if (isMetaValue) isMetaValue = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'a') isReadingChapterName = false;
|
||||||
|
if ((name === 'div' || name === 'span') && isReadingChapterDate) {
|
||||||
|
isReadingChapterDate = false;
|
||||||
|
if (tempChapter.path) {
|
||||||
|
chapters.push(tempChapter);
|
||||||
|
}
|
||||||
|
tempChapter = {} as Plugin.ChapterItem;
|
||||||
|
isInsideChapterRow = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
parser.write(html);
|
||||||
|
parser.end();
|
||||||
|
|
||||||
|
novel.genres = genres.join(',');
|
||||||
novel.chapters = chapters.reverse();
|
novel.chapters = chapters.reverse();
|
||||||
return novel;
|
return novel;
|
||||||
}
|
}
|
||||||
@@ -139,22 +291,35 @@ class IfreedomPlugin implements Plugin.PluginBase {
|
|||||||
const body = await fetchApi(this.site + chapterPath).then(res =>
|
const body = await fetchApi(this.site + chapterPath).then(res =>
|
||||||
res.text(),
|
res.text(),
|
||||||
);
|
);
|
||||||
let chapterText =
|
|
||||||
body.match(/<article id="([\s\S]*?)<\/article>/)?.[0] || '';
|
const startTag =
|
||||||
|
this.id === 'bookhamster'
|
||||||
|
? '<div class="entry-content">'
|
||||||
|
: '<div class="chapter-content">';
|
||||||
|
const endTag =
|
||||||
|
this.id === 'bookhamster'
|
||||||
|
? '<!-- .entry-content -->'
|
||||||
|
: '<div class="chapter-setting">';
|
||||||
|
|
||||||
|
const chapterStart = body.indexOf(startTag);
|
||||||
|
if (chapterStart === -1) return '';
|
||||||
|
|
||||||
|
const chapterEnd = body.indexOf(endTag, chapterStart);
|
||||||
|
let chapterText = body.slice(
|
||||||
|
chapterStart,
|
||||||
|
chapterEnd !== -1 ? chapterEnd : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
chapterText = chapterText.replace(/<script[^>]*>[\s\S]*?<\/script>/gim, '');
|
chapterText = chapterText.replace(/<script[^>]*>[\s\S]*?<\/script>/gim, '');
|
||||||
|
|
||||||
if (chapterText.includes('<img')) {
|
if (chapterText.includes('<img')) {
|
||||||
return chapterText.replace(/srcset="([^"]+)"/g, (match, src) => {
|
chapterText = chapterText.replace(/srcset="([^"]+)"/g, (match, src) => {
|
||||||
if (!src) return match;
|
if (!src) return match;
|
||||||
const bestlink = src
|
const bestLink = src
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.filter((url: string) => url.startsWith('http'))
|
.filter((s: string) => s.startsWith('http'))
|
||||||
.pop();
|
.pop();
|
||||||
|
return bestLink ? `src="${bestLink}"` : match;
|
||||||
if (bestlink) {
|
|
||||||
return `src="${bestlink}"`;
|
|
||||||
}
|
|
||||||
return match;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,33 +328,36 @@ class IfreedomPlugin implements Plugin.PluginBase {
|
|||||||
|
|
||||||
async searchNovels(
|
async searchNovels(
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
page: number | undefined = 1,
|
page: number = 1,
|
||||||
): Promise<Plugin.NovelItem[]> {
|
): Promise<Plugin.NovelItem[]> {
|
||||||
const url =
|
const url = `${this.site}/vse-knigi/?searchname=${encodeURIComponent(searchTerm)}&bpage=${page}`;
|
||||||
this.site +
|
return this.parseNovels(url);
|
||||||
'/vse-knigi/?searchname=' +
|
}
|
||||||
encodeURIComponent(searchTerm) +
|
|
||||||
'&bpage=' +
|
|
||||||
page;
|
|
||||||
const result = await fetchApi(url).then(res => res.text());
|
|
||||||
const loadedCheerio = parseHTML(result);
|
|
||||||
|
|
||||||
const novels: Plugin.NovelItem[] = loadedCheerio(
|
|
||||||
'div.one-book-home > div.img-home a',
|
|
||||||
)
|
|
||||||
.map((index, element) => ({
|
|
||||||
name: loadedCheerio(element).attr('title') || '',
|
|
||||||
cover: loadedCheerio(element).find('img').attr('src'),
|
|
||||||
path:
|
|
||||||
loadedCheerio(element).attr('href')?.replace?.(this.site, '') || '',
|
|
||||||
}))
|
|
||||||
.get()
|
|
||||||
.filter(novel => novel.name && novel.path);
|
|
||||||
|
|
||||||
return novels;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parseDate = (dateString: string | undefined = '') => {
|
function parseStatus(statusString: string): string {
|
||||||
|
const s = statusString.toLowerCase().trim();
|
||||||
|
|
||||||
|
if (
|
||||||
|
s.includes('активен') ||
|
||||||
|
s.includes('продолжается') ||
|
||||||
|
s.includes('онгоинг')
|
||||||
|
) {
|
||||||
|
return NovelStatus.Ongoing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.includes('завершен') || s.includes('конец') || s.includes('закончен')) {
|
||||||
|
return NovelStatus.Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.includes('приостановлен') || s.includes('заморожен')) {
|
||||||
|
return NovelStatus.OnHiatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NovelStatus.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDate(dateString: string = ''): string | null {
|
||||||
const months: Record<string, number> = {
|
const months: Record<string, number> = {
|
||||||
января: 1,
|
января: 1,
|
||||||
февраля: 2,
|
февраля: 2,
|
||||||
@@ -205,11 +373,18 @@ class IfreedomPlugin implements Plugin.PluginBase {
|
|||||||
декабря: 12,
|
декабря: 12,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Checking the format "X ч. назад"
|
||||||
|
const relativeTimeRegex = /(d+)s*ч.?s*назад/;
|
||||||
|
const match = dateString.match(relativeTimeRegex);
|
||||||
|
if (match) {
|
||||||
|
const hoursAgo = parseInt(match[1], 10);
|
||||||
|
return dayjs().subtract(hoursAgo, 'hour').format('LL');
|
||||||
|
}
|
||||||
|
|
||||||
if (dateString.includes('.')) {
|
if (dateString.includes('.')) {
|
||||||
const [day, month, year] = dateString.split('.');
|
const [day, month, year] = dateString.split('.');
|
||||||
if (day && month && year) {
|
const fullYear = year?.length === 2 ? '20' + year : year;
|
||||||
return dayjs(year + '-' + month + '-' + day).format('LL');
|
return dayjs(fullYear + '-' + month + '-' + day).format('LL');
|
||||||
}
|
|
||||||
} else if (dateString.includes(' ')) {
|
} else if (dateString.includes(' ')) {
|
||||||
const [day, month] = dateString.split(' ');
|
const [day, month] = dateString.split(' ');
|
||||||
if (day && months[month]) {
|
if (day && months[month]) {
|
||||||
@@ -217,6 +392,6 @@ class IfreedomPlugin implements Plugin.PluginBase {
|
|||||||
return dayjs(year + '-' + months[month] + '-' + day).format('LL');
|
return dayjs(year + '-' + months[month] + '-' + day).format('LL');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dateString || null;
|
return dateString || null;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user