From 71bdd196dc90ce612622692b527e4f3b9940b832 Mon Sep 17 00:00:00 2001 From: nyagami Date: Thu, 4 Apr 2024 22:29:44 +0700 Subject: [PATCH] Refactor Htmlparser2 plugin (#869) * refactor (not work) * fix: Hako parseNovel * update Hako version * Handler interface * refactor: repo name --- .github/workflows/host_plugins.yml | 2 +- .github/workflows/typechecking.yml | 2 +- README.md | 8 +- libs/defaultCover.ts | 2 +- plugins/vietnamese/LNHako.ts | 441 +++++++++++++++++++---------- types/plugin.ts | 20 ++ 6 files changed, 320 insertions(+), 155 deletions(-) diff --git a/.github/workflows/host_plugins.yml b/.github/workflows/host_plugins.yml index 7a85fe8..be7497c 100644 --- a/.github/workflows/host_plugins.yml +++ b/.github/workflows/host_plugins.yml @@ -7,7 +7,7 @@ on: jobs: host: - if: github.repository == 'LNReader/lnreader-sources' + if: github.repository == 'LNReader/lnreader-plugins' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/typechecking.yml b/.github/workflows/typechecking.yml index 86cd7d1..ae6aa13 100644 --- a/.github/workflows/typechecking.yml +++ b/.github/workflows/typechecking.yml @@ -7,7 +7,7 @@ on: jobs: typechecking: - if: github.repository == 'LNReader/lnreader-sources' + if: github.repository == 'LNReader/lnreader-plugins' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index a4ab13f..ca9415e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # LNReader Plugins

-LNReader plugins counting -GitHub issues by-label -GitHub issues by-label +LNReader plugins counting +GitHub issues by-label +GitHub issues by-label

Repository to host plugins and related issues, and requests for @@ -35,7 +35,7 @@ Repository to host plugins and related issues, and requests for ```ts const githubUsername = "LNReader"; -const githubRepository = "lnreader-sources"; +const githubRepository = "lnreader-plugins"; ``` --- diff --git a/libs/defaultCover.ts b/libs/defaultCover.ts index bbc5754..6052d11 100644 --- a/libs/defaultCover.ts +++ b/libs/defaultCover.ts @@ -1,2 +1,2 @@ export const defaultCover = - 'https://github.com/LNReader/lnreader-sources/blob/main/icons/src/coverNotAvailable.jpg?raw=true'; + 'https://github.com/LNReader/lnreader-plugins/blob/main/icons/src/coverNotAvailable.jpg?raw=true'; diff --git a/plugins/vietnamese/LNHako.ts b/plugins/vietnamese/LNHako.ts index 6621aff..e6339f2 100644 --- a/plugins/vietnamese/LNHako.ts +++ b/plugins/vietnamese/LNHako.ts @@ -1,15 +1,25 @@ import { Parser } from 'htmlparser2'; import { fetchFile } from '@libs/fetch'; -import { Plugin } from '@typings/plugin'; +import { HTMLParser2Util, Plugin } from '@typings/plugin'; import { NovelStatus } from '@libs/novelStatus'; import { FilterTypes, Filters } from '@libs/filterInputs'; +enum ParseNovelAction { + Unknown = 'Unknown', + GetName = 'GetName', + GetSummary = 'GetSummary', + GetInfos = 'GetInfos', + GetGenres = 'GetGenres', + GetCover = 'GetCover', + GetVolumes = 'GetVolumes', +} + class HakoPlugin implements Plugin.PluginBase { id = 'ln.hako'; name = 'Hako'; icon = 'src/vi/hakolightnovel/icon.png'; site = 'https://ln.hako.vn'; - version = '1.0.1'; + version = '1.1.0'; parseNovels(url: string) { return fetch(url) .then(res => res.text()) @@ -72,164 +82,299 @@ class HakoPlugin implements Plugin.PluginBase { } return this.parseNovels(link); } + parseNovel(novelPath: string): Promise { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: '', + author: '', + artist: '', + summary: '', + genres: '', + status: '', + }; + const chapters: Plugin.ChapterItem[] = []; + const getNameHandler: HTMLParser2Util.Handler = { + isDone: false, + isStarted: false, + onopentag(name, attribs) { + if (name === 'a') { + this.isStarted = true; + } + }, + ontext(data) { + novel.name += data; + }, + onclosetag(name) { + if (this.isStarted) { + this.isDone = true; + } + }, + }; + const getSummaryHandler: HTMLParser2Util.Handler & { + newLine: boolean; + } = { + newLine: false, + ontext(data) { + if (this.newLine) { + this.newLine = false; + novel.summary += '\n' + data; + } else { + novel.summary += data; + } + }, + onclosetag(name) { + this.newLine = true; + }, + }; + const getGenresHandler: HTMLParser2Util.Handler = { + ontext(data) { + novel.genres += data; + }, + }; + enum InfoItem { + Author, + Artist, + Status, + Unknown, + } + const getInfosHandler: HTMLParser2Util.Handler & { info: InfoItem } = { + isStarted: false, + info: InfoItem.Unknown, + onopentag(name, attribs) { + if (attribs['class'] === 'info-item') { + switch (this.info) { + case InfoItem.Unknown: + if (!novel.author) { + this.info = InfoItem.Author; + } + break; + case InfoItem.Author: + this.info = InfoItem.Artist; + break; + case InfoItem.Artist: + this.info = InfoItem.Status; + break; + // we dont need the other info (if exist) + case InfoItem.Status: + this.info = InfoItem.Unknown; + break; + default: + break; + } + } + if (name === 'a') { + this.isStarted = true; + } + }, + ontext(data) { + if (this.isStarted) { + switch (this.info) { + case InfoItem.Author: + novel.author += data; + break; + case InfoItem.Artist: + novel.artist += data; + break; + case InfoItem.Status: + novel.status += data; + break; + default: + break; + } + } + }, + onclosetag(name) { + if (this.isStarted) { + this.isStarted = false; + } + if (name === 'a' && this.info === InfoItem.Status) { + this.isDone = true; + } + }, + }; + const getChapterListHandler: HTMLParser2Util.Handler & { + currentVolume: string; + num: number; + part: number; + readingTime: boolean; + tempChapter: Plugin.ChapterItem; + } = { + currentVolume: '', + num: 0, + part: 1, + isStarted: false, + readingTime: false, + tempChapter: {} as Plugin.ChapterItem, + onopentag(name, attribs) { + if (this.isStarted) { + if (name === 'a' && attribs['title'] !== null) { + const chapterName = attribs['title']; + let chapterNumber = Number( + chapterName.match(/Chương\s*(\d+)/i)?.[1], + ); + if (chapterNumber) { + if (this.num === chapterNumber) { + chapterNumber = this.num + this.part / 10; + this.part += 1; + } else { + this.num = chapterNumber; + this.part = 1; + } + } else { + chapterNumber = this.num + this.part / 10; + this.part++; + } + this.tempChapter = { + path: attribs['href'], + name: chapterName, + page: this.currentVolume, + chapterNumber: chapterNumber, + }; + } else if (attribs['class'] === 'chapter-time') { + this.readingTime = true; + } + } + }, + ontext(data) { + if (this.readingTime) { + const chapterTime = data.split('/').map(x => Number(x)); + this.tempChapter.releaseTime = new Date( + chapterTime[2], + chapterTime[1], + chapterTime[0], + ).toISOString(); + chapters.push(this.tempChapter); + this.readingTime = false; + this.tempChapter = {} as Plugin.ChapterItem; + } + }, + onclosetag() { + if (this.readingTime) this.readingTime = false; + }, + }; + const getVolumesHandler: HTMLParser2Util.Handler & { + isParsingChapterList: boolean; + } = { + isStarted: false, + isDone: false, + isParsingChapterList: false, + onopentag(name, attribs) { + if (attribs['class'] === 'sect-title') { + this.isStarted = true; + getChapterListHandler.currentVolume = ''; + } + if (name === 'ul') { + getChapterListHandler.isStarted = true; + getChapterListHandler.num = 0; + getChapterListHandler.part = 1; + } + getChapterListHandler.onopentag?.(name, attribs); + }, + ontext(data) { + if (this.isStarted) { + getChapterListHandler.currentVolume += data.trim(); + } + getChapterListHandler.ontext?.(data); + }, + onclosetag(name, isImplied) { + getChapterListHandler.onclosetag?.(name, isImplied); + this.isStarted = false; + if (name === 'ul') { + getChapterListHandler.isStarted = false; + } + }, + }; + const parseNovelRouter: HTMLParser2Util.HandlerRouter = { + handlers: { + Unknown: undefined, + GetName: getNameHandler, + GetCover: undefined, + GetSummary: getSummaryHandler, + GetGenres: getGenresHandler, + GetInfos: getInfosHandler, + GetVolumes: getVolumesHandler, + }, + action: ParseNovelAction.Unknown, + onopentag(name, attribs) { + if (attribs['class'] === 'series-name') { + this.action = ParseNovelAction.GetName; + } else if (!novel.cover && attribs['class']?.includes('img-in-ratio')) { + const background = attribs['style']; + if (background) { + novel.cover = background.substring( + background.indexOf('http'), + background.length - 2, + ); + } + } else if (attribs['class'] === 'summary-content') { + this.action = ParseNovelAction.GetSummary; + } else if (attribs['class'] === 'series-gerne-item') { + this.action = ParseNovelAction.GetGenres; + } else if (attribs['class'] === 'info-item') { + this.action = ParseNovelAction.GetInfos; + } else if (attribs['class']?.includes('volume-list')) { + this.action = ParseNovelAction.GetVolumes; + } + }, + onclosetag(name) { + switch (this.action) { + case ParseNovelAction.GetName: + if (this.handlers.GetName?.isDone) { + this.action = ParseNovelAction.Unknown; + } + break; + case ParseNovelAction.GetSummary: + if (name === 'div') { + this.action = ParseNovelAction.Unknown; + } + break; + case ParseNovelAction.GetGenres: + this.action = ParseNovelAction.Unknown; + novel.genres += ','; + break; + case ParseNovelAction.GetInfos: + if (this.handlers.GetInfos?.isDone) { + this.action = ParseNovelAction.Unknown; + } + break; + case ParseNovelAction.GetVolumes: + if (this.handlers.GetVolumes?.isDone) { + this.action = ParseNovelAction.Unknown; + } + default: + break; + } + }, + }; return fetch(this.site + novelPath) .then(res => res.text()) .then(html => { - const novel: Plugin.SourceNovel = { - path: novelPath, - name: '', - author: '', - artist: '', - summary: '', - genres: '', - status: '', - }; - let isReadingName = 0; - let isReadingSummary = 0; - let isReadingGenre = 0; - let isReadingInfo = 0; - let isReadingInfoValue = 0; - let isParsingVolume = false; - let isReadingVolume = false; - let currentVolume = ''; - let isParsingChapterList = false; - let num = 0, - part = 0; - let isReadingChapterTime = false; - const chapters: Plugin.ChapterItem[] = []; - let tempChapter = {} as Plugin.ChapterItem; const parser = new Parser({ - onopentag(name, attribs) { - if (attribs['class'] === 'series-name') { - isReadingName = 1; - } else if ( - !novel.cover && - attribs['class']?.includes('img-in-ratio') - ) { - const background = attribs['style']; - if (background) { - novel.cover = background.substring( - background.indexOf('http'), - background.length - 2, - ); - } - } else if (attribs['class'] === 'summary-content') { - isReadingSummary = 1; - } else if (attribs['class'] === 'series-gerne-item') { - isReadingGenre = 1; - } else if (attribs['class'] === 'info-item') { - isReadingInfo += 1; - } else if (attribs['class']?.includes('volume-list')) { - isParsingVolume = true; - num = 0; - part = 1; - } else if (isParsingVolume && attribs['class'] === 'sect-title') { - isReadingVolume = true; - currentVolume = ''; - } else if (isParsingVolume && name === 'ul') { - isParsingChapterList = true; - } else if ( - isParsingChapterList && - name === 'a' && - attribs['title'] !== null - ) { - const chapterName = attribs['title']; - let chapterNumber = Number( - chapterName.match(/Chương\s*(\d+)/i)?.[1], + onopentag(name, attributes) { + parseNovelRouter.onopentag?.(name, attributes); + if (parseNovelRouter.action) { + parseNovelRouter.handlers[parseNovelRouter.action]?.onopentag?.( + name, + attributes, ); - if (chapterNumber) { - if (num === chapterNumber) { - chapterNumber = num + part / 10; - part += 1; - } else { - num = chapterNumber; - part = 1; - } - } else { - chapterNumber = num + part / 10; - part++; - } - tempChapter = { - path: attribs['href'], - name: chapterName, - page: currentVolume, - chapterNumber: chapterNumber, - }; - } else if ( - isParsingChapterList && - attribs['class'] === 'chapter-time' - ) { - isReadingChapterTime = true; - } - if (isReadingName === 1 && name === 'a') { - isReadingName = 2; - } - if (isReadingInfo !== 0 && name === 'a') { - isReadingInfoValue = 1; } }, ontext(data) { - if (isReadingName === 2) { - novel.name += data; - } else if (isReadingSummary !== 0) { - if (isReadingSummary === 2) { - novel.summary += '\n' + data; - isReadingSummary = 1; - } else { - novel.summary += data; - } - } else if (isReadingGenre === 1) { - novel.genres += data; - } else if (isReadingInfoValue === 1 && isReadingInfo <= 3) { - if (isReadingInfo === 1) { - novel.author += data; - } else if (isReadingInfo === 2) { - novel.artist += data; - } else if (isReadingInfo === 3) { - novel.status += data; - } - } else if (isReadingVolume) { - currentVolume += data.trim(); - } else if (isReadingChapterTime) { - const chapterTime = data.split('/').map(x => Number(x)); - tempChapter.releaseTime = new Date( - chapterTime[2], - chapterTime[1], - chapterTime[0], - ).toISOString(); - chapters.push(tempChapter); - isReadingChapterTime = false; - tempChapter = {} as Plugin.ChapterItem; + if (parseNovelRouter.action) { + parseNovelRouter.handlers[parseNovelRouter.action]?.ontext?.( + data, + ); } }, - onclosetag(name) { - if (isReadingName === 2) { - isReadingName = 0; - } else if (isReadingSummary !== 0) { - if (name !== 'div') { - isReadingSummary = 2; - } else { - isReadingSummary = 0; - } - } else if (isReadingGenre === 1) { - isReadingGenre = 0; - novel.genres += ','; - } else if (isReadingVolume) { - isReadingVolume = false; - } else if (isReadingChapterTime) isReadingChapterTime = false; - if (name === 'ul') { - isParsingChapterList = false; - } - if (isReadingInfoValue === 1) { - isReadingInfoValue = 0; - if (isReadingInfo === 3) { - isReadingInfo = 0; - } - } - if (name === 'section') { - isParsingVolume = false; + onclosetag(name, isImplied) { + if (parseNovelRouter.action) { + parseNovelRouter.handlers[parseNovelRouter.action]?.onclosetag?.( + name, + isImplied, + ); } + parseNovelRouter.onclosetag?.(name, isImplied); }, }); diff --git a/types/plugin.ts b/types/plugin.ts index ee9ea1d..a440e8d 100644 --- a/types/plugin.ts +++ b/types/plugin.ts @@ -108,3 +108,23 @@ export namespace Plugin { parsePage(novelPath: string, page: string): Promise; } } + +export namespace HTMLParser2Util { + interface HandlerBase { + onopentag?(name: string, attribs: { [s: string]: string }): void; + ontext?(data: string): void; + onclosetag?(name: string, isImplied: boolean): void; + } + + export interface Handler extends HandlerBase { + isStarted?: boolean; + isDone?: boolean; + } + + // route htmlparser2 event to handlers + export interface HandlerRouter + extends HandlerBase { + handlers: Record; + action: ActionType; + } +}