feat(ar/markazriwayat): convert from madara to standalone plugin (#2304)

Co-authored-by: yemen <hanihameda@gmail.com>
This commit is contained in:
hamedhani1998
2026-07-27 10:13:34 +03:00
committed by GitHub
parent d084532742
commit 2901a7232b
3 changed files with 225 additions and 9 deletions
+225
View File
@@ -0,0 +1,225 @@
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();
-9
View File
@@ -633,15 +633,6 @@
"useNewChapterEndpoint": true
}
},
{
"id": "markazriwayat",
"sourceSite": "https://markazriwayat.com/",
"sourceName": "Markazriwayat",
"options": {
"lang": "Arabic",
"useNewChapterEndpoint": true
}
},
{
"id": "lullobox",
"sourceSite": "https://lullobox.com/",

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB