import { load as parseHTML } from 'cheerio'; import { fetchApi, fetchFile } from '@libs/fetch'; import { FilterTypes, Filters } from '@libs/filterInputs'; import { Plugin } from '@typings/plugin'; class LnMTLPlugin implements Plugin.PagePlugin { id = 'lnmtl'; name = 'LnMTL'; icon = 'src/en/lnmtl/icon.png'; site = 'https://lnmtl.com/'; version = '1.0.1'; async sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } async popularNovels( page: number, { filters }: Plugin.PopularNovelsOptions, ): Promise { let link = this.site + 'novel?'; link += `orderBy=${filters.order.value}`; link += `&order=${filters.sort.value}`; link += `&filter=${filters.storyStatus.value}`; link += `&page=${page}`; const body = await fetchApi(link).then(result => result.text()); const loadedCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; loadedCheerio('.media').each((i, el) => { const novelName = loadedCheerio(el).find('h4').text(); const novelCover = loadedCheerio(el).find('img').attr('src'); const novelUrl = loadedCheerio(el).find('h4 > a').attr('href'); if (!novelUrl) return; const novel = { name: novelName, cover: novelCover, path: novelUrl.replace(this.site, ''), }; novels.push(novel); }); return novels; } async parseNovel( novelPath: string, ): Promise { const result = await fetchApi(this.site + novelPath); const body = await result.text(); const loadedCheerio = parseHTML(body); const volumes = JSON.parse( loadedCheerio('main') .next() .html() ?.match(/lnmtl.volumes = \[(.*?)\]/)![0] ?.replace('lnmtl.volumes = ', '') || '', ); const novel: Plugin.SourceNovel & { totalPages: number } = { path: novelPath, name: loadedCheerio('.novel-name').text() || 'Untitled', cover: loadedCheerio('div.novel').find('img').attr('src'), summary: loadedCheerio('div.description').text().trim(), totalPages: volumes.length, chapters: [], }; loadedCheerio('.panel-body > dl').each(function () { const detailName = loadedCheerio(this).find('dt').text().trim(); const detail = loadedCheerio(this).find('dd').text().trim(); switch (detailName) { case 'Authors': novel.author = detail; break; case 'Current status': novel.status = detail; break; } }); novel.genres = loadedCheerio('.panel-heading:contains(" Genres ")') .next() .find('a') .map((i, el) => loadedCheerio(el).text()) .toArray() .join(','); return novel; } async parsePage(novelPath: string, page: string): Promise { const result = await fetchApi(this.site + novelPath); const body = await result.text(); const loadedCheerio = parseHTML(body); const volumes = JSON.parse( loadedCheerio('main') .next() .html() ?.match(/lnmtl.volumes = \[(.*?)\]/)![0] ?.replace('lnmtl.volumes = ', '') || '', )[+page - 1]; const chapter: Plugin.ChapterItem[] = []; await this.sleep(1000); const volumeData = await fetchApi( `${this.site}chapter?volumeId=${volumes.id}`, ); const volumePage = await volumeData.json(); const firstPage = volumePage.data.map((chapter: ChapterEntry) => ({ name: `#${chapter.number} - ${chapter.title}`, path: `chapter/${chapter.slug}`, releaseTime: new Date(chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert })); chapter.push(...firstPage); for (let i = 2; i <= volumePage.last_page; i++) { await this.sleep(1000); const chapterData = await fetchApi( `${this.site}chapter?page=${i}&volumeId=${volumes.id}`, ); const chapterInfo = await chapterData.json(); const chapterDetails = chapterInfo.data.map((chapter: ChapterEntry) => ({ name: `#${chapter.number} ${chapter.title}`, path: `chapter/${chapter.slug}`, releaseTime: new Date(chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert })); chapter.push(...chapterDetails); } const chapters = chapter; return { chapters }; } async parseChapter(chapterPath: string): Promise { const result = await fetchApi(this.site + chapterPath); const body = await result.text(); const loadedCheerio = parseHTML(body); loadedCheerio('.original, script').remove(); loadedCheerio('sentence.translated').wrap('

'); let chapterText = loadedCheerio('.chapter-body').html()?.replace(/„/g, '“'); if (!chapterText) { chapterText = loadedCheerio('.alert.alert-warning').text(); } return chapterText; } async searchNovels( searchTerm: string, pageNo: number, ): Promise { const body = await fetchApi(this.site).then(r => r.text()); const loadedCheerio = parseHTML(body); const list = loadedCheerio('footer') .next() .next() .html() ?.match(/prefetch: '\/(.*json)/)![1]; const search = await fetch(`${this.site}${list}`); const data = await search.json(); const nov = data.filter((res: { name: string }) => res.name.toLowerCase().includes(searchTerm.toLowerCase()), ); const novels: Plugin.NovelItem[] = []; nov.map((res: { name: string; slug: string; image: string }) => { const novelName = res.name; const novelUrl = `novel/${res.slug}`; const novelCover = res.image; const novel = { path: novelUrl, name: novelName, cover: novelCover, }; novels.push(novel); }); return novels; } async fetchImage(url: string): Promise { return await fetchFile(url); } filters = { order: { value: 'favourites', label: 'Order by', options: [ { label: 'Favourites', value: 'favourites' }, { label: 'Name', value: 'name' }, { label: 'Addition Date', value: 'date' }, ], type: FilterTypes.Picker, }, sort: { value: 'desc', label: 'Sort by', options: [ { label: 'Descending', value: 'desc' }, { label: 'Ascending', value: 'asc' }, ], type: FilterTypes.Picker, }, storyStatus: { value: 'all', label: 'Status', options: [ { label: 'All', value: 'all' }, { label: 'Ongoing', value: 'ongoing' }, { label: 'Finished', value: 'finished' }, ], type: FilterTypes.Picker, }, } satisfies Filters; } export default new LnMTLPlugin(); interface ChapterEntry { number: number; title: string; slug: string; created_at: string; }