diff --git a/public/static/src/ar/rewayatclub/icon.png b/public/static/src/ar/rewayatclub/icon.png new file mode 100644 index 0000000..cb76ac4 Binary files /dev/null and b/public/static/src/ar/rewayatclub/icon.png differ diff --git a/src/plugins/arabic/rewayatclub.ts b/src/plugins/arabic/rewayatclub.ts new file mode 100644 index 0000000..348a6d0 --- /dev/null +++ b/src/plugins/arabic/rewayatclub.ts @@ -0,0 +1,250 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; + +class RewayatClub implements Plugin.PagePlugin { + id = 'rewayatclub'; + name = 'Rewayat Club'; + version = '1.0.0'; + icon = 'src/ar/rewayatclub/icon.png'; + site = 'https://rewayat.club/'; + + parseNovels(data: NovelData): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + data.results.map((item: NovelEntry) => { + novels.push({ + name: item.arabic, + path: `novel/${item.slug}`, + cover: `https://api.rewayat.club/${item.poster_url.slice(1)}`, + }); + }); + return novels; + } + + async popularNovels( + page: number, + { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `https://api.rewayat.club/api/novels/`; + + if (filters) { + if (filters.categories.value !== '') { + link += `?type=${filters.categories.value}`; + } + if (filters.sortOptions.value !== '') { + link += `&ordering=${filters.sortOptions.value}`; + } + if (filters.genre.value.length > 0) { + filters.genre.value.forEach((genre: string) => { + link += `&genre=${genre}`; + }); + } + } + link += `&page=${page}`; + const body = await fetchApi(link).then(r => r.json()); + const loadedCheerio = parseHTML(body); + return this.parseNovels(body); + } + + async parseNovel( + novelUrl: string, + ): Promise { + const result = await fetchApi(new URL(novelUrl, this.site).toString()); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelUrl, + name: loadedCheerio('h1.primary--text span').text().trim() || 'Untitled', + author: loadedCheerio('.novel-author').text().trim(), + summary: loadedCheerio('div.text-pre-line span').text().trim(), + totalPages: 1, + chapters: [], + }; + const statusWords = new Set(['مكتملة', 'متوقفة', 'مستمرة']); + const mainGenres = Array.from(loadedCheerio('.v-slide-group__content a')) + .map(el => loadedCheerio(el).text().trim()) + .join(','); + const statusGenre = Array.from( + loadedCheerio('div.v-slide-group__content span.v-chip__content'), + ) + .map(el => loadedCheerio(el).text().trim()) + .filter(text => statusWords.has(text)); + novel.genres = `${statusGenre},${mainGenres}`; + const statusText = Array.from( + loadedCheerio('div.v-slide-group__content span.v-chip__content'), + ) + .map(el => loadedCheerio(el).text().trim()) + .filter(text => statusWords.has(text)) + .join(); + novel.status = + { + 'متوقفة': 'On Hiatus', + 'مكتملة': 'Completed', + 'مستمرة': 'Ongoing', + }[statusText] || 'Unknown'; + const imageRaw = loadedCheerio('body script:contains("__NUXT__")') + .first() + .text(); + const imageUrlRegex = /poster_url:"(\\u002F[^"]+)"/; + const imageUrlMatch = imageRaw?.match(imageUrlRegex); + const ImageUrlShort = imageUrlMatch + ? imageUrlMatch[1].replace(/\\u002F/g, '/').replace(/^\/*/, '') + : defaultCover; + const imageUrl = `https://api.rewayat.club/${ImageUrlShort}`; + novel.cover = imageUrl; + const chapterNumberStr = loadedCheerio('div.v-tab--active span.mr-1') + .text() + .replace(/[^\d]/g, ''); + const chapterNumber = parseInt(chapterNumberStr, 10); + const pageNumber = Math.ceil(chapterNumber / 24); + novel.totalPages = pageNumber; + + return novel; + } + parseChapters(data: ChapterData, novelPath: string) { + const chapter: Plugin.ChapterItem[] = []; + data.results.map((item: ChapterEntry) => { + chapter.push({ + name: item.title, + releaseTime: new Date(item.date).toISOString(), + path: `${novelPath}/${item.number}`, + chapterNumber: item.number, + }); + }); + return chapter; + } + async parsePage(novelPath: string, page: string): Promise { + const pagePath = novelPath.slice(6); + const pageUrl = `https://api.rewayat.club/api/chapters/${pagePath}/?ordering=number&page=${page}`; + const dataJson = await fetchApi(pageUrl).then(r => r.json()); + const chapters = this.parseChapters(dataJson, novelPath); + return { + chapters, + }; + } + async parseChapter(chapterUrl: string): Promise { + const result = await fetchApi(new URL(chapterUrl, this.site).toString()); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + let chapterText = ''; + loadedCheerio('div.v-card--flat').each((idx, ele) => { + loadedCheerio(ele) + .find('div.v-card__text') + .each((idx, textEle) => { + chapterText += + loadedCheerio(textEle) + .find('p') + .map((_, pEle) => loadedCheerio(pEle).text().trim()) + .get() + .join(' ') + ' '; + }); + }); + chapterText = chapterText.trim(); + return chapterText; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const searchUrl = `https://api.rewayat.club/api/novels/?type=0&ordering=-num_chapters&page=${page}&search=${searchTerm}`; + + const result = await fetchApi(searchUrl).then(r => r.json()); + // const body = await result.text(); + // const loadedCheerio = parseHTML(body); + return this.parseNovels(result); + } + + filters = { + genre: { + value: [], + label: 'Genres', + options: [ + { label: 'كوميديا', value: '1' }, // Comedy + { label: 'أكشن', value: '2' }, // Action + { label: 'دراما', value: '3' }, // Drama + { label: 'فانتازيا', value: '4' }, // Fantasy + { label: 'مهارات القتال', value: '5' }, // Combat Skills + { label: 'مغامرة', value: '6' }, // Adventure + { label: 'رومانسي', value: '7' }, // Romance + { label: 'خيال علمي', value: '8' }, // Science Fiction + { label: 'الحياة المدرسية', value: '9' }, // School Life + { label: 'قوى خارقة', value: '10' }, // Super Powers + { label: 'سحر', value: '11' }, // Magic + { label: 'رياضة', value: '12' }, // Sports + { label: 'رعب', value: '13' }, // Horror + { label: 'حريم', value: '14' }, // Harem + ], + type: FilterTypes.CheckboxGroup, + }, + categories: { + value: '0', + label: 'الفئات', + options: [ + { label: 'جميع الروايات', value: '0' }, + { label: 'مترجمة', value: '1' }, + { label: 'مؤلفة', value: '2' }, + { label: 'مكتملة', value: '3' }, + ], + type: FilterTypes.Picker, + }, + sortOptions: { + value: '-num_chapters', + label: 'الترتيب', + options: [ + { label: 'عدد الفصول - من أقل ﻷعلى', value: 'num_chapters' }, + { label: 'عدد الفصول - من أعلى ﻷقل', value: '-num_chapters' }, + { label: 'الاسم - من أقل ﻷعلى', value: 'english' }, + { label: 'الاسم - من أعلى ﻷقل', value: '-english' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new RewayatClub(); + +interface NovelEntry { + arabic: string; + english: string; + about: string; + poster_url: string; + slug: string; + original: boolean; + complete: boolean; + num_chapters: number; + genre: { + id: number; + arabic: string; + english: string; + }; +} +interface NovelData { + count: number; + next: string; + previous: string; + results: NovelEntry[]; +} +interface ChapterEntry { + number: number; + title: string; + date: string; + uploader: { + username: string; + id: number; + }; + hitcounts: { + hits: number; + id: number; + }; + read: any[]; +} + +interface ChapterData { + count: number; + next: string; + previous: string; + results: ChapterEntry[]; +} diff --git a/src/plugins/english/ao3.ts b/src/plugins/english/ao3.ts index a59f503..7353e10 100644 --- a/src/plugins/english/ao3.ts +++ b/src/plugins/english/ao3.ts @@ -7,7 +7,7 @@ import { defaultCover } from '@libs/defaultCover'; class ArchiveOfOurOwn implements Plugin.PluginBase { id = 'archiveofourown'; name = 'Archive Of Our Own'; - version = '1.0.1'; + version = '1.0.2'; icon = 'src/en/ao3/icon.png'; site = 'https://archiveofourown.org/'; @@ -100,7 +100,11 @@ class ArchiveOfOurOwn implements Plugin.PluginBase { async parseNovel(novelUrl: string): Promise { const result = await fetchApi(new URL(novelUrl, this.site).toString()); + const urlchapter = novelUrl + '/navigate'; + const chapters = await fetchApi(new URL(urlchapter, this.site).toString()); const body = await result.text(); + const chapterlisttext = await chapters.text(); + const chapterlistload = parseHTML(chapterlisttext); const loadedCheerio = parseHTML(body); const novel: Plugin.SourceNovel = { @@ -144,12 +148,31 @@ class ArchiveOfOurOwn implements Plugin.PluginBase { .join(','); novel.summary = `Fandom:\n${fandom}\n\nRating:\n${rating}\n\nWarning:\n${warning}\n\nSummary:\n${summary}\n\nSeries:\n${series}\n\nRelationships:\n${relation}\n\nCharacters:\n${character}\n\nStats:\n${stats}`; const chapterItems: Plugin.ChapterItem[] = []; + const longReleaseDate: string[] = []; + let match: RegExpExecArray | null; + chapterlistload('ol.index').each((i, ele) => { + chapterlistload(ele) + .find('li') + .each((i, el) => { + const chapterNameMatch = chapterlistload(el).find('a').text().trim(); + const releaseTimeText = chapterlistload(el) + .find('span.datetime') + .text() + .replace(/\(([^)]+)\)/g, '$1') + .trim(); + const releaseTime = releaseTimeText + ? new Date(releaseTimeText).toISOString() + : ''; + longReleaseDate.push(releaseTime); + }); + }); const releaseTimeText = loadedCheerio('.wrapper dd.published') .text() .trim(); const releaseTime = releaseTimeText ? new Date(releaseTimeText).toISOString() : ''; + let dateCounter: number = 0; if (loadedCheerio('#chapter_index select').length > 0) { loadedCheerio('#chapter_index select').each((i, selectEl) => { loadedCheerio(selectEl) @@ -158,11 +181,13 @@ class ArchiveOfOurOwn implements Plugin.PluginBase { const chapterName = loadedCheerio(el).text().trim(); const chapterUrlCode = loadedCheerio(el).attr('value')?.trim(); const chapterUrl = `${novelUrl}/chapters/${chapterUrlCode}`; - + const releaseDate: string = longReleaseDate[dateCounter]; + dateCounter++; if (chapterUrl) { chapterItems.push({ name: chapterName, path: chapterUrl, + releaseTime: releaseDate, }); } });