From db313999e076ba4368680bcc4ad3aa0e77a76b74 Mon Sep 17 00:00:00 2001 From: Lukas Richter <53831711+Batorian@users.noreply.github.com> Date: Thu, 25 Apr 2024 17:46:33 +0200 Subject: [PATCH] Add ElloTL plugin (#867) * checkpoint * wip * wip * changed parseNovels logic * added console.log statements for better fixing * searchNovels and parseChapter * wip * wip * wip * finished feature implementation. * removed all console.log * fixed chapter logic * fixed chapter logic * ElloTL (#1) Implemented ElloTL * fixed date * fixed chapter number * Ellotl (#2) fixed chapter logic * fixed how chapters are getting parsed * fixed how chapters are getting parsed * Ellotl (#3) * checkpoint * wip * wip * changed parseNovels logic * added console.log statements for better fixing * searchNovels and parseChapter * wip * wip * wip * finished feature implementation. * removed all console.log * fixed chapter logic * fixed chapter logic * fixed date * fixed chapter number * fixed how chapters are getting parsed * fixed how chapters are getting parsed * fixed novel summary * changed chapter name extraction * cleaner chapter content * cleaner chapter content fix * trimmed novel summary * fixed novel path and chapter path. * fixed parse chapter using old path (#5) --- icons/src/en/ellotl/icon.png | Bin 0 -> 590 bytes plugins/english/ellotl.ts | 409 +++++++++++++++++++++++++++++++++++ 2 files changed, 409 insertions(+) create mode 100644 icons/src/en/ellotl/icon.png create mode 100644 plugins/english/ellotl.ts diff --git a/icons/src/en/ellotl/icon.png b/icons/src/en/ellotl/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f6a00c135e152dc2eb5d139bd4c7ecb1e1b0773f GIT binary patch literal 590 zcmV-U0CSxXoYzijOZVNZEOQ^SGSJL+s7+PRyI0V8s00n0RGH*4}tx$m?iZuZ? zj6Q-r$Qm!uf~Sh>F1xEhD#If&0v+jrn0&v$%IF>;WBUb~TwYkR>nQAtz?H$zVhAT- ze=}O}A0t5KKrjNS*NO!k!SRoevH$l`ZWKy0`#$yGyQAAn7BWOUhVb@Bo3pwpBV_#93 zi^SzBOI`L@LDL>j!HZx(4rHpDia_8|G`*nZ3(jZ7$-5G(({@B1oNY_qrNs#DebL!?0R?>P2o&(m zFH*qn55@_^zlz|?Ca_=dvW@K(h_{V+x8Ut4pF+E*i@F_8 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 isParsingContent = false; + let chapterContent = ''; + let notContent = false; + const parser = new Parser({ + onopentag(name, attribs) { + if (attribs['class']?.includes('epcontent' || 'entry-content')) { + isParsingContent = true; + chapterContent += '<' + name; + for (let attrib in attribs) { + chapterContent += ` ${attrib}="${attribs[attrib]}"`; + } + chapterContent += '>'; + } + if ( + attribs['class']?.includes( + 'wp-block-buttons' || 'wp-block-spacer' || 'wp-element-button', + ) + ) { + notContent = true; + } + if (name === 'span' && attribs['class']?.includes('maxbutton')) { + notContent = true; + } + if (isParsingContent && !notContent) { + chapterContent += '<' + name; + for (let attrib in attribs) { + chapterContent += ` ${attrib}="${attribs[attrib]}"`; + } + chapterContent += '>'; + } + }, + ontext(data) { + if (isParsingContent && !notContent) { + chapterContent += data; + } + }, + onclosetag(name) { + if (isParsingContent && !notContent) { + chapterContent += ``; + } + if (notContent) { + notContent = false; + } + if (isParsingContent && !notContent && 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();