2901a7232b
Co-authored-by: yemen <hanihameda@gmail.com>
226 lines
6.9 KiB
TypeScript
226 lines
6.9 KiB
TypeScript
import { load as parseHTML } from 'cheerio';
|
|
import { fetchApi } from '@libs/fetch';
|
|
import { Plugin } from '@/types/plugin';
|
|
import { defaultCover } from '@libs/defaultCover';
|
|
import { NovelStatus } from '@libs/novelStatus';
|
|
import { FilterTypes, Filters } from '@libs/filterInputs';
|
|
|
|
class Markazriwayat implements Plugin.PluginBase {
|
|
id = 'markazriwayat';
|
|
name = 'مركز الروايات';
|
|
version = '1.0.0';
|
|
icon = 'src/ar/markazriwayat/icon.png';
|
|
site = 'https://markazriwayat.com/';
|
|
|
|
|
|
filters = {
|
|
order: {
|
|
type: FilterTypes.Picker,
|
|
label: 'الترتيب',
|
|
value: 'popular',
|
|
options: [
|
|
{ label: 'الأكثر شعبية', value: 'popular' },
|
|
{ label: 'الأحدث', value: 'new' },
|
|
{ label: 'الأعلى تقييماً', value: 'rating' },
|
|
],
|
|
},
|
|
} satisfies Filters;
|
|
|
|
private async fetchHtml(url: string): Promise<string> {
|
|
const res = await fetchApi(url);
|
|
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
|
|
return res.text();
|
|
}
|
|
|
|
private parseNovelCards(html: string): Plugin.NovelItem[] {
|
|
const $ = parseHTML(html);
|
|
const novels: Plugin.NovelItem[] = [];
|
|
const seen = new Set<string>();
|
|
|
|
$('a.lib-card').each((_, el) => {
|
|
const $el = $(el);
|
|
const href = $el.attr('href') || '';
|
|
const path = href.replace(this.site, '').replace(/\/$/, '');
|
|
const name = $el.find('.lib-card__title').text().trim();
|
|
const cover =
|
|
$el.find('img').attr('data-src') ||
|
|
$el.find('img').attr('data-defer-src') ||
|
|
defaultCover;
|
|
|
|
if (name && path && !seen.has(path)) {
|
|
seen.add(path);
|
|
novels.push({ name, path, cover });
|
|
}
|
|
});
|
|
|
|
return novels;
|
|
}
|
|
|
|
async popularNovels(
|
|
page: number,
|
|
{
|
|
filters,
|
|
showLatestNovels,
|
|
}: Plugin.PopularNovelsOptions<typeof this.filters>,
|
|
): Promise<Plugin.NovelItem[]> {
|
|
let url = `${this.site}`;
|
|
if (showLatestNovels) {
|
|
url += 'new/';
|
|
} else {
|
|
url += `${filters.order.value}/`;
|
|
}
|
|
if (page > 1) url += `page/${page}/`;
|
|
|
|
const html = await this.fetchHtml(url);
|
|
return this.parseNovelCards(html);
|
|
}
|
|
|
|
async searchNovels(
|
|
searchTerm: string,
|
|
page: number,
|
|
): Promise<Plugin.NovelItem[]> {
|
|
try {
|
|
if (page > 1) return [];
|
|
const apiUrl = `${this.site}wp-json/theam/v1/novel-search?term=${encodeURIComponent(searchTerm)}&per_page=20`;
|
|
const res = await fetchApi(apiUrl)
|
|
if (!res.ok) return [];
|
|
const data = await res.json();
|
|
return (data.items || []).map(
|
|
(item: { title: string; link: string; cover?: string }) => ({
|
|
name: item.title,
|
|
path: item.link.replace(this.site, ''),
|
|
cover: item.cover || defaultCover,
|
|
}));
|
|
} catch {
|
|
// Fallback: use library search HTML
|
|
try {
|
|
const url = `${this.site}library/?search=${encodeURIComponent(searchTerm)}`;
|
|
const html = await this.fetchHtml(url);
|
|
return this.parseNovelCards(html);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
}
|
|
|
|
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
|
|
const html = await this.fetchHtml(`${this.site}${novelPath}`);
|
|
const $ = parseHTML(html);
|
|
|
|
const novel: Plugin.SourceNovel = {
|
|
path: novelPath,
|
|
name: $('h1').first().text().trim() || 'Untitled',
|
|
cover: defaultCover,
|
|
summary: '',
|
|
author: '',
|
|
status: NovelStatus.Unknown,
|
|
genres: '',
|
|
chapters: [],
|
|
};
|
|
|
|
// Cover
|
|
const coverImg = $('img')
|
|
.filter(function () {
|
|
const src = $(this).attr('data-src') || $(this).attr('src') || '';
|
|
return src.includes('wp-content/uploads') && !src.includes('cropped-');
|
|
})
|
|
.first();
|
|
novel.cover =
|
|
coverImg.attr('data-src') ||
|
|
coverImg.attr('data-defer-src') ||
|
|
defaultCover;
|
|
|
|
// Status
|
|
const statusEl = $('.status-pill').first();
|
|
const statusClass = statusEl.attr('class') || '';
|
|
if (statusClass.includes('is-ongoing')) novel.status = NovelStatus.Ongoing;
|
|
else if (statusClass.includes('is-complete'))
|
|
novel.status = NovelStatus.Completed;
|
|
else if (statusClass.includes('is-stopped'))
|
|
novel.status = NovelStatus.OnHiatus;
|
|
|
|
// Author
|
|
const authorLink = $('a[href*="/author/"]').first();
|
|
if (authorLink.length) novel.author = authorLink.text().trim();
|
|
|
|
// Summary
|
|
novel.summary = $('#manga-summary').text().trim();
|
|
|
|
// Genres
|
|
const genreParts: string[] = [];
|
|
$('a.pill, a[href*="/genre/"], a[href*="/tasnif/"]').each((_, el) => {
|
|
const t = $(el).text().trim();
|
|
if (t) genreParts.push(t);
|
|
});
|
|
novel.genres = genreParts.join(', ');
|
|
|
|
// Chapters: read total from HTML, generate paths directly (fast, no API)
|
|
const chapters: Plugin.ChapterItem[] = [];
|
|
|
|
const totalText = $('.manga-stat__value').last().text().trim();
|
|
const totalMatch = totalText.match(/(\d+)/);
|
|
const totalChapters = totalMatch ? parseInt(totalMatch[1], 10) : 0;
|
|
|
|
const firstRow = $('div.ch-row').first();
|
|
const firstLink = firstRow.find('a').first().attr('href') || '';
|
|
const firstNum = firstRow.attr('data-ch-num') || '';
|
|
|
|
if (totalChapters > 0 && firstLink && firstNum) {
|
|
const escapedNum = firstNum.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const basePath = firstLink.replace(new RegExp(`${escapedNum}/?$`), '');
|
|
const basePathRelative = basePath.replace(this.site, '');
|
|
|
|
for (let i = 1; i <= totalChapters; i++) {
|
|
chapters.push({
|
|
name: `الفصل ${i}`,
|
|
path: basePathRelative + i + '/',
|
|
chapterNumber: i,
|
|
});
|
|
}
|
|
} else {
|
|
$('div.ch-row').each((_, el) => {
|
|
const a = $(el).find('a').first();
|
|
const name =
|
|
$(el).find('.ch-title').text().trim() || a.attr('aria-label') || '';
|
|
const href = a.attr('href') || '';
|
|
const date = $(el).find('.ch-date').text().trim();
|
|
const chNum = $(el).attr('data-ch-num') || '';
|
|
if (name && href) {
|
|
chapters.push({
|
|
name,
|
|
path: href.replace(this.site, ''),
|
|
releaseTime: date || null,
|
|
chapterNumber: chNum ? parseInt(chNum, 10) : chapters.length + 1,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
novel.chapters = chapters;
|
|
|
|
return novel;
|
|
}
|
|
|
|
async parseChapter(chapterPath: string): Promise<string> {
|
|
const html = await this.fetchHtml(`${this.site}${chapterPath}`);
|
|
const $ = parseHTML(html);
|
|
|
|
$(
|
|
'script, style, .sharedaddy, .jp-relatedposts, .wp-block-spacer, .reading-nav, .ads, .advertisement, .nav-links, .comments-area',
|
|
).remove();
|
|
|
|
$(
|
|
'[style*="display:none"], [style*="display: none"], [hidden], .hidden',
|
|
).remove();
|
|
|
|
const content =
|
|
$('.reading-content, .entry-content, .chapter-content, .text-left')
|
|
.first()
|
|
.html() || '';
|
|
|
|
return content || '<p>المحتوى غير متاح.</p>';
|
|
}
|
|
}
|
|
|
|
export default new Markazriwayat();
|