Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f81b16a
feat(ar/markazriwayat): convert from madara to standalone plugin
Jul 18, 2026
e551923
feat(ar/markazriwayat): standalone plugin with full chapter loading
Jul 21, 2026
3673b89
fix(ar/markazriwayat): use REST API for search instead of broken HTML…
Jul 21, 2026
c486b8e
fix(ar/markazriwayat): simplify search with error handling and fallback
Jul 21, 2026
5580c17
fix(markazriwayat): version 1.0.0 and clean hidden HTML
Jul 22, 2026
cbb78a3
fix(markazriwayat): set version to 2.2.1 (1 increment from madara 2.2.0)
Jul 22, 2026
a2d3e47
Update plugins/arabic/Markazriwayat.ts
hamedhani1998 Jul 25, 2026
d191b8b
Apply suggestion from @K1ngfish3r
hamedhani1998 Jul 25, 2026
e902070
Update plugins/arabic/Markazriwayat.ts
hamedhani1998 Jul 25, 2026
4833a5d
Update plugins/arabic/Markazriwayat.ts
hamedhani1998 Jul 25, 2026
0b5fbdb
fix(ar/markazriwayat): reset version to 1.0.0
Jul 25, 2026
ef60862
fix(ar/markazriwayat): update version to 2.2.2
Jul 25, 2026
76ecd1f
fix(ar/markazriwayat): set version to 1.0.0
Jul 25, 2026
b51eac3
Apply suggestion from @K1ngfish3r
hamedhani1998 Jul 26, 2026
9dc89dc
Apply suggestion from @K1ngfish3r
hamedhani1998 Jul 26, 2026
35cbfeb
Apply suggestion from @K1ngfish3r
hamedhani1998 Jul 26, 2026
abd96d3
Apply suggestion from @K1ngfish3r
hamedhani1998 Jul 26, 2026
3d54994
Apply suggestion from @K1ngfish3r
hamedhani1998 Jul 26, 2026
e76ed06
Apply suggestion from @K1ngfish3r
hamedhani1998 Jul 26, 2026
2c40f4a
Apply suggestion from @K1ngfish3r
hamedhani1998 Jul 26, 2026
6138a70
Apply suggestion from @K1ngfish3r
hamedhani1998 Jul 26, 2026
9eaaa8e
fix(ar/markazriwayat): remove markazriwayat from madara sources and o…
Jul 26, 2026
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
225 changes: 225 additions & 0 deletions plugins/arabic/Markazriwayat.ts
Original file line number Diff line number Diff line change
@@ -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/';


Comment thread
hamedhani1998 marked this conversation as resolved.
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,
Comment thread
hamedhani1998 marked this conversation as resolved.
): Promise<Plugin.NovelItem[]> {
try {
if (page > 1) return [];
const apiUrl = `${this.site}wp-json/theam/v1/novel-search?term=${encodeURIComponent(searchTerm)}&per_page=20`;
Comment thread
hamedhani1998 marked this conversation as resolved.
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;

Comment thread
hamedhani1998 marked this conversation as resolved.
// 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 changes: 0 additions & 9 deletions plugins/multisrc/madara/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
Expand Down
Binary file removed public/static/multisrc/madara/markazriwayat/icon.png
Binary file not shown.
Binary file added public/static/src/ar/markazriwayat/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading