diff --git a/plugins/arabic/rewayahfans.ts b/plugins/arabic/rewayahfans.ts new file mode 100644 index 000000000..a20fe1f43 --- /dev/null +++ b/plugins/arabic/rewayahfans.ts @@ -0,0 +1,234 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; + +type WPPage = { + title: { rendered: string }; + slug: string; + date: string; + content?: { rendered: string }; + _embedded?: { + 'wp:featuredmedia'?: { source_url: string }[]; + }; +}; + +class RewayahFans implements Plugin.PluginBase { + id = 'rewayahfans'; + name = 'روايه فانز'; + version = '1.0.0'; + icon = 'src/ar/rewayahfans/icon.png'; + site = 'https://rewayahfans.net/'; + + private allNovels: Plugin.NovelItem[] = []; + + private async fetchJson(url: string): Promise { + const res = await fetchApi(url); + if (!res.ok) throw new Error(`Request failed: ${res.status}`); + return res.json() as Promise; + } + + private async fetchHtml(url: string): Promise { + const res = await fetchApi(url); + if (!res.ok) throw new Error(`Request failed: ${res.status}`); + return res.text(); + } + + private async loadAllNovels(): Promise { + if (this.allNovels.length > 0) return this.allNovels; + + const html = await this.fetchHtml( + `${this.site}%d9%82%d8%a7%d8%a6%d9%85%d8%a9-%d8%a7%d9%84%d8%b1%d9%88%d8%a7%d9%8a%d8%a7%d8%aa/`, + ); + const $ = parseHTML(html); + const novels: Plugin.NovelItem[] = []; + const seen = new Set(); + + $('figure.wp-block-image').each((_, el) => { + const fig = $(el); + const linkEl = fig.find('figcaption a').first(); + const href = + linkEl.attr('href') || fig.find('a').first().attr('href') || ''; + const name = linkEl.text().trim(); + const cover = fig.find('img').attr('src') || ''; + const decodedCover = cover.replace(/&/g, '&').replace(/&/g, '&'); + + if (name && href) { + const path = href.replace(this.site, '').replace(/\/$/, ''); + if (!seen.has(path)) { + seen.add(path); + novels.push({ name, path, cover: decodedCover }); + } + } + }); + + this.allNovels = novels; + return novels; + } + + async popularNovels( + page: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _options: Plugin.PopularNovelsOptions, + ): Promise { + const allNovels = await this.loadAllNovels(); + if (page > 1) return []; + return allNovels; + } + + async parseNovel(novelPath: string): Promise { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: '', + cover: '', + summary: '', + author: '', + genres: '', + status: '', + chapters: [], + }; + + const html = await this.fetchHtml(`${this.site}${novelPath}`); + const $ = parseHTML(html); + + const titleTag = $('title').text().trim(); + novel.name = + titleTag.split(' - ')[0].trim() || + titleTag.split('\u2013')[0].trim() || + 'بدون عنوان'; + + novel.name = this.extractNovelName(novel.name); + + const ogImage = $('meta[property="og:image"]').attr('content') || ''; + novel.cover = ogImage || ''; + + const summaryParts: string[] = []; + let inStory = false; + let inChapters = false; + $('.entry-content > *').each((_, el) => { + const $el = $(el); + const tag = $el.prop('tagName')?.toLowerCase() || ''; + const text = $el.text().trim(); + + if (tag === 'p' && text.startsWith('القصة')) { + inStory = true; + return; + } + if (inStory) { + if (tag === 'p' && text.startsWith('الفصول')) { + inChapters = true; + return false; + } + if (tag === 'p' && text && !inChapters) { + summaryParts.push(text); + } + } + }); + novel.summary = summaryParts.join('\n') || ''; + + const metadataMap: Record = {}; + $('ul.wp-block-list li').each((_, el) => { + const text = $(el).text().trim(); + const colonIdx = text.indexOf(':'); + if (colonIdx > 0) { + const key = text.substring(0, colonIdx).trim(); + const value = text.substring(colonIdx + 1).trim(); + metadataMap[key] = value; + } + }); + + novel.author = metadataMap['المؤلف'] || ''; + novel.genres = metadataMap['التصنيفات'] || ''; + novel.status = metadataMap['الحالة'] || ''; + + const chapterSet = new Set(); + + const chapterSection = $( + 'p:contains("الفصول"), p:contains("Chapters"), p:contains("Fichier"), p:contains("Capitulos"), p:contains("Capítulos"), p:contains("Chapitres"), p:contains("Kapitel"), p:contains("Файл"), p:contains("Глава"), p:contains("章")', + ); + + if (chapterSection.length > 0) { + chapterSection.nextAll().each((_, el) => { + const $el = $(el); + if ($el.hasClass('wp-block-paragraph') || $el.is('p')) { + $el.find('a').each((_, aEl) => { + const href = $(aEl).attr('href') || ''; + const text = $(aEl).text().trim(); + if (!href || !text) return; + if (!href.startsWith(this.site)) return; + const chapterPath = href.replace(this.site, '').replace(/\/$/, ''); + if (chapterPath === novelPath) return; + if (chapterSet.has(chapterPath)) return; + const numMatch = text.match(/(\d+)/); + if (!numMatch) return; + const chapterNum = parseInt(numMatch[1], 10); + if (chapterNum === 0) return; + const cleanName = text.replace(/\bnew\b/i, '').trim(); + if (cleanName.includes('النهاية')) return; + if (cleanName.includes('اضغط هنا')) return; + if (cleanName.includes('Summary')) return; + chapterSet.add(chapterPath); + novel.chapters!.push({ + name: cleanName, + path: chapterPath, + chapterNumber: chapterNum, + }); + }); + } else { + return false; + } + }); + } + + novel.chapters!.sort( + (a, b) => (a.chapterNumber || 0) - (b.chapterNumber || 0), + ); + + if (!novel.name && novel.chapters!.length > 0) { + novel.name = this.extractNovelName(novel.chapters![0].name); + } + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const pages = await this.fetchJson( + `${this.site}wp-json/wp/v2/pages?slug=${chapterPath}&_fields=content`, + ); + + const arr = Array.isArray(pages) ? pages : [pages]; + if (arr.length > 0 && arr[0].content?.rendered) { + const $ = parseHTML(arr[0].content.rendered); + $( + 'script, style, .sharedaddy, .jp-relatedposts, .wp-block-spacer, .simplefavorite-button, .wp-block-jetpack-rating-star', + ).remove(); + return $.html(); + } + + const html = await this.fetchHtml(`${this.site}${chapterPath}/`); + const $ = parseHTML(html); + const content = + $('article .entry-content, .post-content, .entry-content').html() || ''; + return content || '

المحتوى غير متاح.

'; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const allNovels = await this.loadAllNovels(); + const lower = searchTerm.toLowerCase(); + const filtered = allNovels.filter(n => + n.name.toLowerCase().includes(lower), + ); + const perPage = 20; + const start = (page - 1) * perPage; + return filtered.slice(start, start + perPage); + } + + private extractNovelName(title: string): string { + const match = title.match(/^(.+?)\s+\d+$/); + return match ? match[1].trim() : title.trim(); + } +} + +export default new RewayahFans(); diff --git a/public/static/src/ar/rewayahfans/icon.png b/public/static/src/ar/rewayahfans/icon.png new file mode 100644 index 000000000..5fbe65285 Binary files /dev/null and b/public/static/src/ar/rewayahfans/icon.png differ