diff --git a/plugins/arabic/rewayahfans.ts b/plugins/arabic/rewayahfans.ts new file mode 100644 index 000000000..8a041754c --- /dev/null +++ b/plugins/arabic/rewayahfans.ts @@ -0,0 +1,168 @@ +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; + featured_media: number; + _embedded?: { + 'wp:featuredmedia'?: Array<{ source_url: string }>; + }; +}; + +class RewayahFans implements Plugin.PluginBase { + id = 'rewayahfans'; + name = 'روايه فانز'; + version = '4.1.0'; + icon = 'src/ar/rewayahfans/icon.png'; + site = 'https://rewayahfans.net/'; + + 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 getCover(page: WPPage): string { + return page._embedded?.['wp:featuredmedia']?.[0]?.source_url || ''; + } + + async popularNovels( + page: number, + { showLatestNovels }: Plugin.PopularNovelsOptions, + ): Promise { + const pages = await this.fetchJson( + `${this.site}wp-json/wp/v2/pages?per_page=20&page=${page}&orderby=date&order=desc&_embed`, + ); + + const seen = new Set(); + const novels: Plugin.NovelItem[] = []; + + for (const page of pages) { + const novelName = this.extractNovelName(page.title.rendered); + if (novelName && !seen.has(novelName)) { + seen.add(novelName); + novels.push({ + name: novelName, + path: page.slug, + cover: this.getCover(page), + }); + } + } + + return novels; + } + + async parseNovel(novelPath: string): Promise { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: '', + chapters: [], + }; + + const slugBase = novelPath.replace(/\/$/, '').split('/').pop() || novelPath; + const novelPrefix = slugBase.replace(/-\d+$/, ''); + + let pg = 1; + let hasMore = true; + + while (hasMore) { + const pages = await this.fetchJson( + `${this.site}wp-json/wp/v2/pages?search=${encodeURIComponent(novelPrefix.replace(/-/g, ' '))}&per_page=100&page=${pg}&_fields=slug,title,date`, + ); + + if (pages.length === 0) { + hasMore = false; + break; + } + + for (const page of pages) { + if (page.slug.startsWith(novelPrefix)) { + if (!novel.name) { + novel.name = this.extractNovelName(page.title.rendered); + } + const numMatch = page.slug.match(/(\d+)$/); + const chapterNum = numMatch ? parseInt(numMatch[1], 10) : 0; + + novel.chapters!.push({ + name: page.title.rendered, + path: page.slug, + chapterNumber: chapterNum, + releaseTime: page.date, + }); + } + } + + if (pages.length < 100) hasMore = false; + pg++; + } + + 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').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 pages = await this.fetchJson( + `${this.site}wp-json/wp/v2/pages?search=${encodeURIComponent(searchTerm)}&per_page=20&page=${page}&_embed`, + ); + + const seen = new Set(); + const novels: Plugin.NovelItem[] = []; + + for (const page of pages) { + const novelName = this.extractNovelName(page.title.rendered); + if (novelName && !seen.has(novelName)) { + seen.add(novelName); + novels.push({ + name: novelName, + path: page.slug, + cover: this.getCover(page), + }); + } + } + + return novels; + } + + 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..81b122268 Binary files /dev/null and b/public/static/src/ar/rewayahfans/icon.png differ