diff --git a/icons/multisrc/lightnovelwp/cpunovel/icon.png b/icons/multisrc/lightnovelwp/cpunovel/icon.png new file mode 100644 index 0000000..eb2259f Binary files /dev/null and b/icons/multisrc/lightnovelwp/cpunovel/icon.png differ diff --git a/icons/src/en/ellotl/icon.png b/icons/multisrc/lightnovelwp/ellotl/icon.png similarity index 100% rename from icons/src/en/ellotl/icon.png rename to icons/multisrc/lightnovelwp/ellotl/icon.png diff --git a/icons/multisrc/lightnovelwp/ippotranslations/icon.png b/icons/multisrc/lightnovelwp/ippotranslations/icon.png new file mode 100644 index 0000000..763b525 Binary files /dev/null and b/icons/multisrc/lightnovelwp/ippotranslations/icon.png differ diff --git a/icons/src/en/relibrary/icon.png b/icons/src/en/relibrary/icon.png index 0c27ad3..68f7296 100644 Binary files a/icons/src/en/relibrary/icon.png and b/icons/src/en/relibrary/icon.png differ diff --git a/icons/src/fr/phenixscans/icon.png b/icons/src/fr/phenixscans/icon.png index 878d68a..e4a836c 100644 Binary files a/icons/src/fr/phenixscans/icon.png and b/icons/src/fr/phenixscans/icon.png differ diff --git a/icons/src/uk/smakolykytl/icon.png b/icons/src/uk/smakolykytl/icon.png index a652b03..10b7e01 100644 Binary files a/icons/src/uk/smakolykytl/icon.png and b/icons/src/uk/smakolykytl/icon.png differ diff --git a/plugins/english/ellotl.ts b/plugins/english/ellotl.ts deleted file mode 100644 index 7c058e2..0000000 --- a/plugins/english/ellotl.ts +++ /dev/null @@ -1,426 +0,0 @@ -import { Parser } from 'htmlparser2'; -import { fetchFile } from '@libs/fetch'; -import { Plugin } from '@typings/plugin'; -import { NovelStatus } from '@libs/novelStatus'; -import { Filters, FilterTypes } from '@libs/filterInputs'; - -class ElloTL implements Plugin.PluginBase { - id = 'ellotl'; - name = 'ElloTL'; - icon = 'src/en/ellotl/icon.png'; - site = 'https://ellotl.com'; - version = '1.0.0'; - - parseNovels(url: string) { - return fetch(url) - .then(res => res.text()) - .then(html => { - let isParsingNovel = false; - let tempNovel = {} as Plugin.NovelItem; - const novels: Plugin.NovelItem[] = []; - const parser = new Parser({ - onopentag(name, attribs) { - if (attribs['class']?.includes('maindet')) { - isParsingNovel = true; - } - if (isParsingNovel) { - switch (name) { - case 'a': - tempNovel.path = attribs['href'] - .replace('https://ellotl.com', '') - .trim(); - break; - case 'img': - tempNovel.name = attribs['title']; - tempNovel.cover = attribs['src']; - break; - } - if (tempNovel.path && tempNovel.name) { - novels.push(tempNovel); - tempNovel = {} as Plugin.NovelItem; - isParsingNovel = false; - } - } - }, - }); - parser.write(html); - parser.end(); - return novels; - }); - } - - popularNovels( - pageNo: number, - { - showLatestNovels, - filters, - }: Plugin.PopularNovelsOptions, - ): Promise { - let link = this.site + '/series'; - if (showLatestNovels) { - link += '/?order=update&page=' + pageNo; - } else if (filters) { - const params = new URLSearchParams(); - for (const genre of filters.genres.value) { - params.append('genre[]', genre); - } - for (const type of filters.type.value) { - params.append('type[]', type); - } - params.append('status', filters.status.value); - params.append('order', filters.sort.value); - link += '/?' + params.toString() + '&page=' + pageNo; - } else { - link += '/?page=' + pageNo; - } - return this.parseNovels(link); - } - - parseNovel(novelPath: string): Promise { - return fetch(this.site + novelPath) - .then(res => res.text()) - .then(html => { - function extractChapterNumber( - data: string, - tempChapter: Plugin.ChapterItem, - ) { - let tempChapterNumber = data.match(/(\d+)$/); - if (tempChapterNumber !== null) { - tempChapter.chapterNumber = parseInt(tempChapterNumber[0]); - } - } - const novel: Plugin.SourceNovel = { - path: novelPath, - name: '', - genres: '', - summary: '', - author: '', - status: '', - chapters: [] as Plugin.ChapterItem[], - }; - let isParsingGenres = false; - let isReadingGenre = false; - let isReadingSummary = false; - let isParsingInfo = false; - let isReadingInfo = false; - let isReadingAuthor = false; - let isReadingStatus = false; - let isParsingChapterList = false; - let isReadingChapter = false; - let isReadingChapterInfo = 0; - const chapters: Plugin.ChapterItem[] = []; - let tempChapter = {} as Plugin.ChapterItem; - const parser = new Parser({ - onopentag(name, attribs) { - // name and cover - if ( - !novel.cover && - attribs['class']?.includes('ts-post-image' || 'wp-post-image') - ) { - novel.name = attribs['title']; - novel.cover = attribs['src']; - } // genres - else if (attribs['class'] === 'genxed') { - isParsingGenres = true; - } else if (isParsingGenres && name === 'a') { - isReadingGenre = true; - } // summary - else if (attribs['class'] === 'entry-content') { - isReadingSummary = true; - } // author and status - else if (attribs['class'] === 'spe') { - isParsingInfo = true; - } else if (isParsingInfo && name === 'span') { - isReadingInfo = true; - } // chapters - else if (attribs['class'] === 'eplister eplisterfull') { - isParsingChapterList = true; - } else if (isParsingChapterList && name === 'li') { - isReadingChapter = true; - } else if (isReadingChapter) { - if (name === 'a') { - tempChapter.path = attribs['href'] - .replace('https://ellotl.com', '') - .trim(); - } else if (attribs['class'] === 'epl-num') { - isReadingChapterInfo = 1; - } else if (attribs['class'] === 'epl-title') { - isReadingChapterInfo = 2; - } else if (attribs['class'] === 'epl-date') { - isReadingChapterInfo = 3; - } - } - }, - ontext(data) { - // genres - if (isParsingGenres) { - if (isReadingGenre) { - novel.genres += data + ', '; - } - } // summary - else if (isReadingSummary) { - novel.summary += data.trim(); - } // author and status - else if (isParsingInfo) { - if (isReadingInfo) { - if (data === 'Author:') { - isReadingAuthor = true; - } else if (data === 'Status:') { - isReadingStatus = true; - } else if (isReadingAuthor) { - novel.author += data; - } else if (isReadingStatus) { - novel.status = data; - } - } - } // chapters - else if (isParsingChapterList) { - if (isReadingChapter) { - if (isReadingChapterInfo === 1) { - extractChapterNumber(data, tempChapter); - } else if (isReadingChapterInfo === 2) { - tempChapter.name = - data - .match( - RegExp( - `^${novel.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*(.+)`, - ), - )?.[1] - ?.trim() || data.trim(); - if (!tempChapter.chapterNumber) { - extractChapterNumber(data, tempChapter); - } - } else if (isReadingChapterInfo === 3) { - tempChapter.releaseTime = data; //new Date(data).toISOString(); - } - } - } - }, - onclosetag(name) { - // genres - if (isParsingGenres) { - if (isReadingGenre) { - isReadingGenre = false; // stop reading genre - } else { - isParsingGenres = false; // stop parsing genres - novel.genres = novel.genres?.replace(/,*\s*$/, ''); // remove trailing comma - } - } // summary - else if (isReadingSummary) { - if (name === 'br') { - novel.summary += '\n'; - } else if (name === 'div') { - isReadingSummary = false; - } - } // author and status - else if (isParsingInfo) { - if (isReadingInfo) { - if (name === 'span') { - isReadingInfo = false; - if (isReadingAuthor) { - isReadingAuthor = false; - } else if (isReadingStatus) { - isReadingStatus = false; - } - } - } else if (name === 'div') { - isParsingInfo = false; - novel.author = novel.author?.trim(); - novel.status = novel.status?.trim(); - } - } // chapters - else if (isParsingChapterList) { - if (isReadingChapter) { - if (isReadingChapterInfo === 1) { - isReadingChapterInfo = 0; - } else if (isReadingChapterInfo === 2) { - isReadingChapterInfo = 0; - } else if (isReadingChapterInfo === 3) { - isReadingChapterInfo = 0; - } else if (name === 'li') { - isReadingChapter = false; - if (!tempChapter.chapterNumber) tempChapter.chapterNumber = 0; - chapters.push(tempChapter); - tempChapter = {} as Plugin.ChapterItem; - } - } else if (name === 'ul') { - isParsingChapterList = false; - novel.chapters = chapters.reverse(); - } - } - }, - }); - parser.write(html); - parser.end(); - switch (novel.status?.trim()) { - case 'Ongoing': - novel.status = NovelStatus.Ongoing; - break; - case 'Hiatus': - novel.status = NovelStatus.OnHiatus; - break; - case 'Completed': - novel.status = NovelStatus.Completed; - break; - default: - novel.status = NovelStatus.Unknown; - } - return novel; - }); - } - - parseChapter(chapterPath: string): Promise { - return fetch(this.site + chapterPath) - .then(res => res.text()) - .then(html => { - let isParsingTitleBlock = false; - let isParsingTitle = false; - let isParsingSubtitle = false; - let isParsingContent = false; - let chapterContent = ''; - let notContent = false; - - const parser = new Parser({ - onopentag(name, attribs) { - if (attribs['class']?.includes('epheader')) { - chapterContent += `

`; - isParsingTitleBlock = true; - isParsingTitle = true; - } - if (isParsingSubtitle) { - chapterContent += ' | '; - } - if (attribs['class']?.includes('entry-content')) { - isParsingContent = true; - } - if (isParsingContent && !notContent) { - chapterContent += `<${name}`; - for (let attrib in attribs) { - chapterContent += ` ${attrib}="${attribs[attrib]}"`; - } - chapterContent += `>`; - } - if (attribs['class']?.includes('mb-center')) { - notContent = true; - } - }, - ontext(data) { - if (isParsingTitle) { - chapterContent += data; - } - if (isParsingSubtitle) { - chapterContent += data; - } - if (isParsingContent && !notContent) { - chapterContent += data; - } - }, - onclosetag(name) { - if (isParsingTitle && name === 'h1') { - isParsingTitle = false; - isParsingSubtitle = true; - } - if (isParsingSubtitle && name === 'div') { - isParsingSubtitle = false; - } - if (isParsingTitleBlock && !isParsingTitle && !isParsingSubtitle) { - chapterContent += `


`; - isParsingTitleBlock = false; - } - if (isParsingContent && !notContent) { - chapterContent += ``; - } - if (notContent && name === 'span') { - notContent = false; - } - if (isParsingContent && name === 'div') { - isParsingContent = false; - } - }, - }); - parser.write(html); - parser.end(); - return chapterContent.trim(); - }); - } - - searchNovels( - searchTerm: string, - pageNo: number, - ): Promise { - const url = this.site + '/?s=' + searchTerm + '&page=' + pageNo; - return this.parseNovels(url); - } - - fetchImage(url: string): Promise { - // if your plugin has images, and they won't load - // this is the function to fiddle with - return fetchFile(url); - } - - filters = { - genres: { - type: FilterTypes.CheckboxGroup, - label: 'Genres', - value: [], - options: [ - { label: 'Action', value: 'action' }, - { label: 'Adventure', value: 'adventure' }, - { label: 'Comedy', value: 'comedy' }, - { label: 'Drama', value: 'drama' }, - { label: 'Ecchi', value: 'ecchi' }, - { label: 'Fantasy', value: 'fantasy' }, - { label: 'Gender Bender', value: 'gender-bender' }, - { label: 'Harem', value: 'harem' }, - { label: 'Martial Arts', value: 'martial-arts' }, - { label: 'Mature', value: 'mature' }, - { label: 'Mystery', value: 'mystery' }, - { label: 'Psychological', value: 'psychological' }, - { label: 'Romance', value: 'romance' }, - { label: 'School Life', value: 'school-life' }, - { label: 'Seinen', value: 'seinen' }, - { label: 'Shounen', value: 'shounen' }, - { label: 'Slice of Life', value: 'slice-of-life' }, - { label: 'Supernatural', value: 'supernatural' }, - { label: 'Tragedy', value: 'tragedy' }, - ], - }, - type: { - type: FilterTypes.CheckboxGroup, - label: 'Type', - value: [], - options: [ - { label: 'Japanese Web Novel', value: 'japanese-web-novel' }, - { label: 'Korean Web Novel', value: 'korean-web-novel' }, - { label: 'Light Novel (JP)', value: 'light-novel-jp' }, - { label: 'Web Novel', value: 'web-novel' }, - ], - }, - status: { - type: FilterTypes.Picker, - label: 'Status', - value: '', - options: [ - { label: 'All', value: '' }, - { label: 'Ongoing', value: 'ongoing' }, - { label: 'Hiatus', value: 'hiatus' }, - { label: 'Completed', value: 'completed' }, - ], - }, - sort: { - type: FilterTypes.Picker, - label: 'Sort', - value: 'popular', - options: [ - { label: 'Popular', value: 'popular' }, - { label: 'A-Z', value: 'title' }, - { label: 'Z-A', value: 'titlereverse' }, - { label: 'Latest Update', value: 'update' }, - { label: 'Latest Added', value: 'latest' }, - { label: 'Rating', value: 'rating' }, - ], - }, - } satisfies Filters; -} - -export default new ElloTL(); diff --git a/plugins/russian/ranobelib.ts b/plugins/russian/ranobelib.ts index 921c71b..f4f6c1d 100644 --- a/plugins/russian/ranobelib.ts +++ b/plugins/russian/ranobelib.ts @@ -18,7 +18,7 @@ class RLIB implements Plugin.PluginBase { name = 'RanobeLib'; site = 'https://ranobelib.me'; apiSite = 'https://api.lib.social/api/manga/'; - version = '2.0.0'; + version = '2.0.1'; icon = 'src/ru/ranobelib/icon.png'; webStorageUtilized = true; @@ -501,6 +501,12 @@ function jsonToHtml(json: HTML[], html: string = '') { (element.content ? jsonToHtml(element.content) : '
') + '

'; break; + case 'heading': + html += + '

' + + (element.content ? jsonToHtml(element.content) : '
') + + '

'; + break; case 'text': html += element.text; break; diff --git a/scripts/multisrc/lightnovelwp/filters/cpunovel.json b/scripts/multisrc/lightnovelwp/filters/cpunovel.json new file mode 100644 index 0000000..575df02 --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/cpunovel.json @@ -0,0 +1,164 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Wuxia", + "value": "wuxia" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/ellotl.json b/scripts/multisrc/lightnovelwp/filters/ellotl.json new file mode 100644 index 0000000..9fb1ec0 --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/ellotl.json @@ -0,0 +1,65 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genres", + "value": [], + "options": [ + { "label": "Action", "value": "action" }, + { "label": "Adventure", "value": "adventure" }, + { "label": "Comedy", "value": "comedy" }, + { "label": "Drama", "value": "drama" }, + { "label": "Ecchi", "value": "ecchi" }, + { "label": "Fantasy", "value": "fantasy" }, + { "label": "Gender Bender", "value": "gender-bender" }, + { "label": "Harem", "value": "harem" }, + { "label": "Martial Arts", "value": "martial-arts" }, + { "label": "Mature", "value": "mature" }, + { "label": "Mystery", "value": "mystery" }, + { "label": "Psychological", "value": "psychological" }, + { "label": "Romance", "value": "romance" }, + { "label": "School Life", "value": "school-life" }, + { "label": "Seinen", "value": "seinen" }, + { "label": "Shounen", "value": "shounen" }, + { "label": "Slice of Life", "value": "slice-of-life" }, + { "label": "Supernatural", "value": "supernatural" }, + { "label": "Tragedy", "value": "tragedy" } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { "label": "Japanese Web Novel", "value": "japanese-web-novel" }, + { "label": "Korean Web Novel", "value": "korean-web-novel" }, + { "label": "Light Novel (JP)", "value": "light-novel-jp" }, + { "label": "Web Novel", "value": "web-novel" } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { "label": "All", "value": "" }, + { "label": "Ongoing", "value": "ongoing" }, + { "label": "Hiatus", "value": "hiatus" }, + { "label": "Completed", "value": "completed" } + ] + }, + "order": { + "type": "Picker", + "label": "Sort", + "value": "popular", + "options": [ + { "label": "Popular", "value": "popular" }, + { "label": "A-Z", "value": "title" }, + { "label": "Z-A", "value": "titlereverse" }, + { "label": "Latest Update", "value": "update" }, + { "label": "Latest Added", "value": "latest" }, + { "label": "Rating", "value": "rating" } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/filters/ippotranslations.json b/scripts/multisrc/lightnovelwp/filters/ippotranslations.json new file mode 100644 index 0000000..864f838 --- /dev/null +++ b/scripts/multisrc/lightnovelwp/filters/ippotranslations.json @@ -0,0 +1,128 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Dropped", + "value": "dropped" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/scripts/multisrc/lightnovelwp/sources.json b/scripts/multisrc/lightnovelwp/sources.json index 8e11f91..19f5a37 100644 --- a/scripts/multisrc/lightnovelwp/sources.json +++ b/scripts/multisrc/lightnovelwp/sources.json @@ -15,8 +15,7 @@ "options": { "lang": "Arabic", "reverseChapters": true, - "customJs": "let ignore = $('article > style').text().trim().split(',');ignore.push(...(ignore.pop()?.match(/^\\.\\w+/) || []));ignore.map(tag => $(`p${tag}`).remove());", - "versionIncrements": 1 + "customJs": "let ignore = $('article > style').text().trim().split(',');ignore.push(...(ignore.pop()?.match(/^\\.\\w+/) || []));ignore.map(tag => $(`p${tag}`).remove());" } }, { @@ -52,8 +51,7 @@ "sourceName": "DaoTranslate", "options": { "lang": "English", - "reverseChapters": true, - "versionIncrements": 2 + "reverseChapters": true } }, { @@ -62,8 +60,7 @@ "sourceName": "جنة الروايات", "options": { "lang": "Arabic", - "reverseChapters": true, - "versionIncrements": 1 + "reverseChapters": true } }, { @@ -80,8 +77,7 @@ "sourceSite": "https://noveltr.com/", "sourceName": "NovelTR", "options": { - "lang": "Turkish", - "reverseChapters": true + "lang": "Turkish" } }, { @@ -100,7 +96,6 @@ "options": { "lang": "English", "reverseChapters": false, - "versionIncrements": 1, "customJs": "$('.genesistls-watermark').remove();$('.epcontent p').each((i, el) => { if ($(el).html() === ' ') $(el).remove(); });" } }, @@ -176,5 +171,31 @@ "lang": "Portuguese", "reverseChapters": true } + }, + { + "id": "ellotl", + "sourceSite": "https://ellotl.com/", + "sourceName": "ElloTL", + "options": { + "lang": "English", + "reverseChapters": true + } + }, + { + "id": "ippotranslations", + "sourceSite": "https://ippotranslations.com/", + "sourceName": "Ippotranslations", + "options": { + "lang": "English" + } + }, + { + "id": "cpunovel", + "sourceSite": "https://cpunovel.com/", + "sourceName": "CPUnovel", + "options": { + "lang": "English", + "reverseChapters": true + } } ] diff --git a/scripts/multisrc/lightnovelwp/template.ts b/scripts/multisrc/lightnovelwp/template.ts index 4dc74a1..4bb7789 100644 --- a/scripts/multisrc/lightnovelwp/template.ts +++ b/scripts/multisrc/lightnovelwp/template.ts @@ -1,4 +1,5 @@ -import { CheerioAPI, load } from 'cheerio'; +import { load } from 'cheerio'; +import { Parser } from 'htmlparser2'; import { fetchApi, fetchFile } from '@libs/fetch'; import { Plugin } from '@typings/plugin'; import { NovelStatus } from '@libs/novelStatus'; @@ -36,7 +37,7 @@ class LightNovelWPPlugin implements Plugin.PluginBase { this.icon = `multisrc/lightnovelwp/${metadata.id.toLowerCase()}/icon.png`; this.site = metadata.sourceSite; const versionIncrements = metadata.options?.versionIncrements || 0; - this.version = `1.0.${5 + versionIncrements}`; + this.version = `1.1.${versionIncrements}`; this.options = metadata.options ?? ({} as LightNovelWPOptions); this.filters = metadata.filters satisfies Filters; } @@ -48,43 +49,49 @@ class LightNovelWPPlugin implements Plugin.PluginBase { return url_parts.join('.'); } - async getCheerio(url: string, search: boolean): Promise { + async safeFecth(url: string, search: boolean): Promise { const r = await fetchApi(url); if (!r.ok && search != true) throw new Error( 'Could not reach site (' + r.status + ') try to open in webview.', ); - const $ = load(await r.text()); - const title = $('title').text().trim(); + const data = await r.text(); + const title = data.match(/(.*?)<\/title>/)?.[1]?.trim(); + if ( this.getHostname(url) != this.getHostname(r.url) || - title == 'Bot Verification' || - title == 'You are being redirected...' || - title == 'Un instant...' || - title == 'Just a moment...' || - title == 'Redirecting...' + (title && + (title == 'Bot Verification' || + title == 'You are being redirected...' || + title == 'Un instant...' || + title == 'Just a moment...' || + title == 'Redirecting...')) ) throw new Error('Captcha error, please open in webview'); - return $; + return data; } - parseNovels($: CheerioAPI): Plugin.NovelItem[] { + parseNovels(html: string): Plugin.NovelItem[] { const novels: Plugin.NovelItem[] = []; - $('div.listupd > article').each((i, elem) => { - const novelName = $(elem).find('h2').text(); - const image = $(elem).find('img'); - const novelUrl = $(elem).find('a').attr('href'); + const articles = html.match(/<article([\s\S]*?)<\/article>/g) || []; + articles.forEach(article => { + const novelName = article.match(/<a.*title="(.*?)"/)?.[1]; + const novelUrl = article.match(/<a href="(.*?)"/)?.[1]; + + if (novelName && novelUrl) { + const novelCover = + article.match(/<img.*src="(.*?)"(?:\sdata-src="(.*?)")?.*\/>/) || []; - if (novelUrl) { novels.push({ name: novelName, - cover: image.attr('data-src') || image.attr('src') || defaultCover, - path: novelUrl.replace(/https?:\/\/.*?\//, '/'), + cover: novelCover[2] || novelCover[1] || defaultCover, + path: novelUrl.replace(this.site, ''), }); } }); + return novels; } @@ -105,168 +112,289 @@ class LightNovelWPPlugin implements Plugin.PluginBase { url += `&${key}=${value}`; else if (filters[key].value) url += `&${key}=${filters[key].value}`; } - const $ = await this.getCheerio(url, false); - return this.parseNovels($); + const html = await this.safeFecth(url, false); + return this.parseNovels(html); } async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { - const $ = await this.getCheerio(this.site + novelPath, false); + const baseURL = this.site; + const html = await this.safeFecth(baseURL + novelPath, false); const novel: Plugin.SourceNovel = { - path: novelPath.replace(/https?:\/\/.*?\//, '/'), - name: 'Untitled', + path: novelPath, + name: '', + genres: '', + summary: '', + author: '', + artist: '', + status: '', + chapters: [] as Plugin.ChapterItem[], }; + let isParsingGenres = false; + let isReadingGenre = false; + let isReadingSummary = false; + let isParsingInfo = false; + let isReadingInfo = false; + let isReadingAuthor = false; + let isReadingArtist = false; + let isReadingStatus = false; + let isParsingChapterList = false; + let isReadingChapter = false; + let isReadingChapterInfo = 0; + let isPaidChapter = false; + const chapters: Plugin.ChapterItem[] = []; + let tempChapter = {} as Plugin.ChapterItem; - novel.name = $('h1.entry-title').text().trim(); - const image = $('img.wp-post-image'); - novel.cover = image.attr('data-src') || image.attr('src') || defaultCover; - switch ($('div.sertostat > span').attr('class')?.toLowerCase() || '') { - case 'completed': - novel.status = NovelStatus.Completed; - break; - case 'ongoing': - novel.status = NovelStatus.Ongoing; - break; - case 'hiatus': - novel.status = NovelStatus.OnHiatus; - break; - default: - novel.status = NovelStatus.Unknown; - break; + const parser = new Parser({ + onopentag(name, attribs) { + // name and cover + if ( + !novel.cover && + attribs['class']?.includes('ts-post-image' || 'wp-post-image') + ) { + novel.name = attribs['title']; + novel.cover = attribs['data-src'] || attribs['src'] || defaultCover; + } // genres + else if ( + attribs['class'] === 'genxed' || + attribs['class'] === 'sertogenre' + ) { + isParsingGenres = true; + } else if (isParsingGenres && name === 'a') { + isReadingGenre = true; + } // summary + else if ( + name === 'div' && + (attribs['class'] === 'entry-content' || + attribs['itemprop'] === 'description') + ) { + isReadingSummary = true; + } // author and status + else if (attribs['class'] === 'spe' || attribs['class'] === 'serl') { + isParsingInfo = true; + } else if (isParsingInfo && name === 'span') { + isReadingInfo = true; + } else if (name === 'div' && attribs['class'] === 'sertostat') { + isParsingInfo = true; + isReadingInfo = true; + isReadingStatus = true; + } + // chapters + else if (attribs['class'] === 'eplister eplisterfull') { + isParsingChapterList = true; + } else if (isParsingChapterList && name === 'li') { + isReadingChapter = true; + } else if (isReadingChapter) { + if (name === 'a') { + tempChapter.path = attribs['href'].replace(baseURL, '').trim(); + } else if (attribs['class'] === 'epl-num') { + isReadingChapterInfo = 1; + } else if (attribs['class'] === 'epl-title') { + isReadingChapterInfo = 2; + } else if (attribs['class'] === 'epl-date') { + isReadingChapterInfo = 3; + } else if (attribs['class'] === 'epl-price') { + isReadingChapterInfo = 4; + } + } + }, + ontext(data) { + // genres + if (isParsingGenres) { + if (isReadingGenre) { + novel.genres += data + ', '; + } + } // summary + else if (isReadingSummary) { + novel.summary += data.trim(); + } // author and status + else if (isParsingInfo) { + if (isReadingInfo) { + const detailName = data.toLowerCase().replace(':', '').trim(); + + if (isReadingAuthor) { + novel.author += data || 'Unknown'; + } else if (isReadingArtist) { + novel.artist += data || 'Unknown'; + } else if (isReadingStatus) { + switch (detailName) { + case 'مكتملة': + case 'completed': + case 'complété': + case 'completo': + case 'completado': + case 'tamamlandı': + novel.status = NovelStatus.Completed; + break; + case 'مستمرة': + case 'ongoing': + case 'en cours': + case 'em andamento': + case 'en progreso': + case 'devam ediyor': + novel.status = NovelStatus.Ongoing; + break; + case 'متوقفة': + case 'hiatus': + case 'en pause': + case 'hiato': + case 'pausa': + case 'pausado': + case 'duraklatıldı': + novel.status = NovelStatus.OnHiatus; + break; + default: + novel.status = NovelStatus.Unknown; + break; + } + } + + switch (detailName) { + case 'الكاتب': + case 'author': + case 'auteur': + case 'autor': + case 'yazar': + isReadingAuthor = true; + break; + case 'الحالة': + case 'status': + case 'statut': + case 'estado': + case 'durum': + isReadingStatus = true; + break; + case 'الفنان': + case 'artist': + case 'artiste': + case 'artista': + case 'çizer': + isReadingArtist = true; + break; + } + } + } // chapters + else if (isParsingChapterList) { + if (isReadingChapter) { + if (isReadingChapterInfo === 1) { + extractChapterNumber(data, tempChapter); + } else if (isReadingChapterInfo === 2) { + tempChapter.name = + data + .match( + RegExp( + `^${novel.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*(.+)`, + ), + )?.[1] + ?.trim() || data.trim(); + if (!tempChapter.chapterNumber) { + extractChapterNumber(data, tempChapter); + } + } else if (isReadingChapterInfo === 3) { + tempChapter.releaseTime = data; //new Date(data).toISOString(); + } else if (isReadingChapterInfo === 4) { + const detailName = data.toLowerCase().trim(); + switch (detailName) { + case 'free': + case 'gratuit': + case 'مجاني': + case 'livre': + case '': + isPaidChapter = false; + break; + default: + isPaidChapter = true; + break; + } + } + } + } + }, + onclosetag(name) { + // genres + if (isParsingGenres) { + if (isReadingGenre) { + isReadingGenre = false; // stop reading genre + } else { + isParsingGenres = false; // stop parsing genres + novel.genres = novel.genres?.slice(0, -2); // remove trailing comma + } + } // summary + else if (isReadingSummary) { + if (name === 'br') { + novel.summary += '\n'; + } else if (name === 'div') { + isReadingSummary = false; + } + } // author and status + else if (isParsingInfo) { + if (isReadingInfo) { + if (name === 'span') { + isReadingInfo = false; + if (isReadingAuthor && novel.author) { + isReadingAuthor = false; + } else if (isReadingArtist && novel.artist) { + isReadingArtist = false; + } else if (isReadingStatus && novel.status !== '') { + isReadingStatus = false; + } + } + } else if (name === 'div') { + isParsingInfo = false; + novel.author = novel.author?.trim(); + novel.artist = novel.artist?.trim(); + } + } // chapters + else if (isParsingChapterList) { + if (isReadingChapter) { + if (isReadingChapterInfo === 1) { + isReadingChapterInfo = 0; + } else if (isReadingChapterInfo === 2) { + isReadingChapterInfo = 0; + } else if (isReadingChapterInfo === 3) { + isReadingChapterInfo = 0; + } else if (isReadingChapterInfo === 4) { + isReadingChapterInfo = 0; + } else if (name === 'li') { + isReadingChapter = false; + if (!tempChapter.chapterNumber) tempChapter.chapterNumber = 0; + if (!isPaidChapter) chapters.push(tempChapter); + tempChapter = {} as Plugin.ChapterItem; + } + } else if (name === 'ul') { + isParsingChapterList = false; + } + } + }, + }); + + parser.write(html); + parser.end(); + + if (chapters.length) { + if (this.options?.reverseChapters) chapters.reverse(); + novel.chapters = chapters; } - let details = $('div.serl'); - if (!details.length) details = $('div.spe > span'); - details.each(function () { - const detailName = $(this) - .contents() - .first() - .text() - .replace(':', '') - .trim() - .toLowerCase(); - const detail = $(this).contents().last().text().trim().toLowerCase(); - - switch (detailName) { - case 'الكاتب': - case 'author': - case 'auteur': - case 'autor': - case 'yazar': - novel.author = detail; - break; - case 'الحالة': - case 'status': - case 'statut': - case 'estado': - case 'durum': - switch (detail) { - case 'مكتملة': - case 'completed': - case 'complété': - case 'completo': - case 'completado': - case 'tamamlandı': - novel.status = NovelStatus.Completed; - break; - case 'مستمرة': - case 'ongoing': - case 'en cours': - case 'em andamento': - case 'en progreso': - case 'devam ediyor': - novel.status = NovelStatus.Ongoing; - break; - case 'متوقفة': - case 'hiatus': - case 'en pause': - case 'hiato': - case 'pausa': - case 'pausado': - case 'duraklatıldı': - novel.status = NovelStatus.OnHiatus; - break; - default: - novel.status = NovelStatus.Unknown; - break; - } - break; - case 'الفنان': - case 'artist': - case 'artiste': - case 'artista': - case 'çizer': - novel.artist = detail; - break; - } - }); - - let genres = $('.sertogenre'); - if (!genres.length) genres = $('.genxed'); - novel.genres = genres - .children('a') - .map((i, el) => $(el).text()) - .toArray() - .join(','); - - let summary = $('.sersys > p') - .map((i, el) => $(el).text().trim()) - .toArray(); - if (!summary.length) - summary = $('.entry-content > p') - .map((i, el) => $(el).text().trim()) - .toArray(); - novel.summary = summary.join('\n'); - - const chapters: Plugin.ChapterItem[] = []; - $('.eplister li').each((i, elem) => { - const chapterName = - $(elem).find('.epl-num').text() + - ' ' + - $(elem).find('.epl-title').text(); - const chapterUrl = $(elem).find('a').attr('href') || ''; - const releaseTime = $(elem).find('.epl-date').text().trim(); - let isFreeChapter: boolean; - switch ($(elem).find('.epl-price').text().trim().toLowerCase()) { - case 'free': - case 'gratuit': - case 'مجاني': - case 'livre': - case '': - isFreeChapter = true; - break; - default: - isFreeChapter = false; - break; - } - if (isFreeChapter) - chapters.push({ - name: chapterName, - path: chapterUrl.replace(/https?:\/\/.*?\//, '/'), - releaseTime: releaseTime, - }); - }); - - if (this.options?.reverseChapters && chapters.length) chapters.reverse(); - - novel.chapters = chapters; return novel; } async parseChapter(chapterPath: string): Promise<string> { - const $ = await this.getCheerio(this.site + chapterPath, false); - try { - // CustomJS HERE - } catch (error) { - console.error('Error executing customJs:', error); - throw error; + const data = await this.safeFecth(this.site + chapterPath, false); + const chapterText = data.match( + /<div.*class="epcontent ([\s\S]*?)<div.*class="bottomnav"/g, + )?.[0]; + if (this.options?.customJs) { + try { + const $ = load(chapterText || data); + // CustomJS HERE + return $.html(); + } catch (error) { + console.error('Error executing customJs:', error); + throw error; + } } - return ( - $('.epcontent p') - .map((i, el) => $(el)) - .toArray() - .join('\n') || '' - ); + return chapterText || ''; } async searchNovels( @@ -274,9 +402,16 @@ class LightNovelWPPlugin implements Plugin.PluginBase { page: number, ): Promise<Plugin.NovelItem[]> { const url = this.site + 'page/' + page + '/?s=' + searchTerm; - const $ = await this.getCheerio(url, true); - return this.parseNovels($); + const html = await this.safeFecth(url, true); + return this.parseNovels(html); } fetchImage = fetchFile; } + +function extractChapterNumber(data: string, tempChapter: Plugin.ChapterItem) { + const tempChapterNumber = data.match(/(\d+)$/); + if (tempChapterNumber && tempChapterNumber[0]) { + tempChapter.chapterNumber = parseInt(tempChapterNumber[0]); + } +}