diff --git a/public/static/src/en/lightnoveltranslations/icon.png b/public/static/src/en/lightnoveltranslations/icon.png new file mode 100644 index 000000000..f4d51dd26 Binary files /dev/null and b/public/static/src/en/lightnoveltranslations/icon.png differ diff --git a/src/plugins/english/lightnoveltranslation.ts b/src/plugins/english/lightnoveltranslation.ts new file mode 100644 index 000000000..d9db992eb --- /dev/null +++ b/src/plugins/english/lightnoveltranslation.ts @@ -0,0 +1,208 @@ +import { fetchApi, fetchProto, fetchText } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; +import { Filters } from '@libs/filterInputs'; +import { load as loadCheerio } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; +// import { isUrlAbsolute } from '@libs/isAbsoluteUrl'; +// import { storage, localStorage, sessionStorage } from '@libs/storage'; +// import { encode, decode } from 'urlencode'; +// import dayjs from 'dayjs'; +import { Parser } from 'htmlparser2'; + +class LNTPlugin implements Plugin.PluginBase { + id = 'lightnoveltranslations'; + name = 'Light Novel Translations'; + icon = 'src/en/lightnoveltranslations/icon.png'; + site = 'https://lightnovelstranslations.com/'; + version = '1.0.0'; + filters: Filters | undefined = undefined; + imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; + + //flag indicates whether access to LocalStorage, SesesionStorage is required. + webStorageUtilized?: boolean; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site + 'read/'; + link += `page/${pageNo}`; + link += `?sortby=${showLatestNovels ? 'most-recent' : 'most-liked'}`; + + const body = await fetchApi(link); + const html = await body.text(); + + const loadedCheerio = loadCheerio(html); + + const baseUrl = this.site; + const novels: Plugin.NovelItem[] = []; + loadedCheerio('div.read_list-story-item').each((i, el) => { + let tempNovel = {} as Plugin.NovelItem; + let img = loadedCheerio(el) + .find('.item_thumb') + .find('img') + .first() + .attr('src'); + let path = loadedCheerio(el) + .find('.item_thumb') + .find('a') + .first() + .attr('href'); + path = path ? path.slice(baseUrl.length) : ''; + let title = loadedCheerio(el) + .find('.item_thumb') + .find('a') + .first() + .attr('title'); + tempNovel.name = title ? title : ''; + tempNovel.path = path; + tempNovel.cover = img; + novels.push(tempNovel); + }); + + return novels; + } + async parseNovel(novelPath: string): Promise { + let url = this.site + novelPath; + const body = await fetchApi(url); + const html = await body.text().then(r => r.replace(/>\s+<')); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: '', + totalPages: 1, + summary: '', + author: '', + status: '', + chapters: [], + }; + + const loadedCheerio = loadCheerio(html); + novel.cover = loadedCheerio('div.novel-image').find('img').attr('src'); + novel.status = loadedCheerio('div.novel_status').text().trim(); + switch (novel.status) { + 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; + } + novel.name = loadedCheerio('div.novel_title') + .find('h3') + .first() + .text() + .trim(); + novel.author = loadedCheerio('div.novel_detail_info') + .find('li') + .filter(function () { + return loadedCheerio(this).text().includes('Author'); + }) + .first() + .text() + .trim(); + + const body2 = await fetchApi(url.replace('?tab=table_contents', '')); + const html2 = await body2.text().then(r => r.replace(/>\s+<')); + const loadedCheerio2 = loadCheerio(html2); + + novel.summary = loadedCheerio2('div.novel_text') + .find('p') + .first() + .text() + .trim(); + + const baseUrl = this.site; + const chapters: Plugin.ChapterItem[] = []; + + loadedCheerio('li.chapter-item.unlock').each((i, el) => { + const chapterName = loadedCheerio(el).find('a').text().trim(); + const chapterPath = loadedCheerio(el).find('a').attr('href'); + if (chapterPath) { + const chapter: Plugin.ChapterItem = { + name: chapterName, + path: chapterPath.slice(baseUrl.length), + }; + chapters.push(chapter); + } + }); + novel.chapters = chapters; + return novel; + } + async parseChapter(chapterPath: string): Promise { + // parse chapter text here + const body = await fetchApi(this.site + chapterPath); + const html = await body.text().then(r => r.replace(/>\s+<')); + + const loadedCheerio = loadCheerio(html); + + const chapterText = loadedCheerio('div.text_story'); + chapterText.find('div.ads_content').remove(); + + return chapterText.html() || ''; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + // get novels using the search term + + if (pageNo !== 1) return []; + const searchUrl = this.site + '/read'; + const formData = new FormData(); + formData.append('field-search', searchTerm); + + let results = await fetchApi(searchUrl, { + method: 'POST', + body: formData, + }); + + const body = await results.text(); + const loadedCheerio = loadCheerio(body); + const novels: Plugin.NovelItem[] = []; + loadedCheerio('div.read_list-story-item').each((i, el) => { + let tempNovel = {} as Plugin.NovelItem; + let img = loadedCheerio(el) + .find('.item_thumb') + .find('img') + .first() + .attr('src'); + let path = loadedCheerio(el) + .find('.item_thumb') + .find('a') + .first() + .attr('href'); + path = path ? path.slice(this.site.length) : ''; + let title = loadedCheerio(el) + .find('.item_thumb') + .find('a') + .first() + .attr('title'); + tempNovel.name = title ? title : ''; + tempNovel.path = path; + tempNovel.cover = img; + novels.push(tempNovel); + }); + + type SearchEntry = { + title: string; + thumbnail: string; + permalink: string; + }; + return novels; + } + + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? '/book/' : '/chapter/') + path; +} + +export default new LNTPlugin();