From db023e78837b8785c17f74b37ae2ac61085c9b34 Mon Sep 17 00:00:00 2001 From: NeXIRa-28 Date: Thu, 30 Apr 2026 23:31:31 +0300 Subject: [PATCH 1/4] Add files via upload --- plugins/english/lightnovelworld.ts | 240 +++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 plugins/english/lightnovelworld.ts diff --git a/plugins/english/lightnovelworld.ts b/plugins/english/lightnovelworld.ts new file mode 100644 index 000000000..a13cb2619 --- /dev/null +++ b/plugins/english/lightnovelworld.ts @@ -0,0 +1,240 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; + +class LightNovelWorldPlugin implements Plugin.PluginBase { + id = 'lightnovelworld'; + name = 'LightNovelWorld'; + icon = 'src/en/lightnovelworld/icon.png'; + site = 'https://www.lightnovelworld.org'; + version = '1.0.0'; + + imageRequestInit: Plugin.ImageRequestInit = { + headers: { + Referer: 'https://www.lightnovelworld.org', + }, + }; + + filters = { + order: { + label: 'Sort By', + options: [ + { label: 'Popular', value: '' }, + { label: 'Latest Updates', value: '?orderby=updatedtime' }, + { label: 'New Novels', value: '?orderby=releasetime' }, + { label: 'Rating', value: '?orderby=rating' }, + ], + type: FilterTypes.Picker, + value: '', + }, + status: { + label: 'Status', + options: [ + { label: 'All', value: '' }, + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Completed', value: 'completed' }, + { label: 'Hiatus', value: 'hiatus' }, + ], + type: FilterTypes.Picker, + value: '', + }, + } satisfies Filters; + + // ─── Popular / Browse ────────────────────────────────────────────────────── + + async popularNovels( + page: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + const { showLatestNovels, filters } = options; + + let url: string; + + if (showLatestNovels) { + // "Latest" tab + url = `${this.site}/latest-updates-04061612?page=${page}`; + } else { + const statusSegment = filters.status.value + ? `/${filters.status.value}` + : ''; + const orderQuery = filters.order.value || ''; + url = `${this.site}/novel-list${statusSegment}${orderQuery}&page=${page}`; + } + + const result = await fetchApi(url); + const body = await result.text(); + const $ = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + // Novel cards appear in different containers depending on the page + $('li.novel-item, div.novel-item').each((_, el) => { + const anchor = $(el).find('a.novel-cover, a').first(); + const imgEl = $(el).find('img'); + const titleEl = $(el).find('.novel-title, h4, h3').first(); + + const novelUrl = anchor.attr('href') || ''; + const name = titleEl.text().trim() || anchor.attr('title') || ''; + const cover = imgEl.attr('data-src') || imgEl.attr('src') || defaultCover; + + if (name && novelUrl) { + novels.push({ + name, + url: novelUrl.startsWith('http') + ? novelUrl + : `${this.site}${novelUrl}`, + cover, + }); + } + }); + + return novels; + } + + // ─── Novel Details + Chapter List ───────────────────────────────────────── + + async parseNovelAndChapters(novelUrl: string): Promise { + const result = await fetchApi(novelUrl); + const body = await result.text(); + const $ = parseHTML(body); + + const novel: Plugin.SourceNovel = { + url: novelUrl, + name: + $('h1.novel-title').text().trim() || + $('h1').first().text().trim() || + '', + cover: + $('.cover-wrap img, .novel-cover img').attr('data-src') || + $('.cover-wrap img, .novel-cover img').attr('src') || + defaultCover, + author: + $('.author a, span[itemprop="author"]').text().trim() || 'Unknown', + genres: $('.categories a, .genre-item') + .map((_, el) => $(el).text().trim()) + .get() + .join(', '), + summary: $('.summary, .description, #info .content') + .text() + .trim(), + status: this.parseStatus($('.novel-status, .status').text()), + }; + + // ── Chapter list (paginated) ────────────────────────────────────────── + const chapters: Plugin.ChapterItem[] = []; + + // Determine how many chapter pages there are + const lastPageHref = $('ul.pagination a:last-child, .pagination .last') + .attr('href'); + let totalPages = 1; + if (lastPageHref) { + const match = lastPageHref.match(/page=(\d+)/); + if (match) totalPages = parseInt(match[1], 10); + } + + for (let p = 1; p <= totalPages; p++) { + const chapUrl = `${novelUrl}?tab=chapters&page=${p}&chorder=asc`; + const chapRes = await fetchApi(chapUrl); + const chapBody = await chapRes.text(); + const $c = parseHTML(chapBody); + + $c('ul#chapterlist li, ul.chapter-list li').each((_, el) => { + const a = $c(el).find('a'); + const href = a.attr('href') || ''; + const chName = + a.find('.chapter-title').text().trim() || + a.attr('title') || + a.text().trim(); + const releaseTime = + $c(el).find('.chapter-update, time').attr('datetime') || + $c(el).find('.chapter-update, time').text().trim() || + ''; + const numMatch = chName.match(/chapter\s*([\d.]+)/i); + const chapterNumber = numMatch ? parseFloat(numMatch[1]) : 0; + + if (href && chName) { + chapters.push({ + name: chName, + url: href.startsWith('http') ? href : `${this.site}${href}`, + releaseTime, + chapterNumber, + }); + } + }); + } + + novel.chapters = chapters; + return novel; + } + + // ─── Chapter Content ─────────────────────────────────────────────────────── + + async parseChapter(chapterUrl: string): Promise { + const result = await fetchApi(chapterUrl); + const body = await result.text(); + const $ = parseHTML(body); + + // Remove ads / navigation clutter + $( + '.ad, .ads, .adsense, #patreon-adsense, .chapter-nav, .chapter-warning, script', + ).remove(); + + const content = + $('#chapter-container, .chapter-content, .text-left').first().html() || + ''; + + return content; + } + + // ─── Search ──────────────────────────────────────────────────────────────── + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = `${this.site}/search?keywords=${encodeURIComponent(searchTerm)}&page=${pageNo}`; + + const result = await fetchApi(url); + const body = await result.text(); + const $ = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + $('li.novel-item, div.novel-item').each((_, el) => { + const anchor = $(el).find('a').first(); + const imgEl = $(el).find('img'); + const titleEl = $(el).find('.novel-title, h4, h3').first(); + + const novelUrl = anchor.attr('href') || ''; + const name = titleEl.text().trim() || anchor.attr('title') || ''; + const cover = imgEl.attr('data-src') || imgEl.attr('src') || defaultCover; + + if (name && novelUrl) { + novels.push({ + name, + url: novelUrl.startsWith('http') + ? novelUrl + : `${this.site}${novelUrl}`, + cover, + }); + } + }); + + return novels; + } + + // ─── Helpers ─────────────────────────────────────────────────────────────── + + private parseStatus(raw: string): string { + const s = raw.toLowerCase(); + if (s.includes('ongoing')) return NovelStatus.Ongoing; + if (s.includes('completed')) return NovelStatus.Completed; + if (s.includes('hiatus')) return NovelStatus.OnHiatus; + return NovelStatus.Unknown; + } +} + +export default new LightNovelWorldPlugin(); From 43cc4866a9969b92cfd501370363b51da909058e Mon Sep 17 00:00:00 2001 From: NeXIRa-28 Date: Thu, 30 Apr 2026 23:51:48 +0300 Subject: [PATCH 2/4] Add files via upload --- public/static/src/en/icon.png.jpeg | Bin 0 -> 4482 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/static/src/en/icon.png.jpeg diff --git a/public/static/src/en/icon.png.jpeg b/public/static/src/en/icon.png.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..f781364ebe3eb3efbbc9706bfe95cda70264bf33 GIT binary patch literal 4482 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 literal 0 HcmV?d00001 From f55b30651f6074d31172c2259d548e5ee7cec1b7 Mon Sep 17 00:00:00 2001 From: NeXIRa-28 Date: Thu, 30 Apr 2026 23:52:47 +0300 Subject: [PATCH 3/4] Rename public/static/src/en/icon.png.jpeg to public/static/src/en/lightnovelworld/icon.png.jpeg --- .../src/en/{ => lightnovelworld}/icon.png.jpeg | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename public/static/src/en/{ => lightnovelworld}/icon.png.jpeg (100%) diff --git a/public/static/src/en/icon.png.jpeg b/public/static/src/en/lightnovelworld/icon.png.jpeg similarity index 100% rename from public/static/src/en/icon.png.jpeg rename to public/static/src/en/lightnovelworld/icon.png.jpeg From 111f700aa4ae6ccdf41c8417337a4aecbdf5c5bf Mon Sep 17 00:00:00 2001 From: NeXIRa-28 Date: Thu, 30 Apr 2026 23:56:00 +0300 Subject: [PATCH 4/4] Rename icon.png.jpeg to icon.png --- .../en/lightnovelworld/{icon.png.jpeg => icon.png} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename public/static/src/en/lightnovelworld/{icon.png.jpeg => icon.png} (100%) diff --git a/public/static/src/en/lightnovelworld/icon.png.jpeg b/public/static/src/en/lightnovelworld/icon.png similarity index 100% rename from public/static/src/en/lightnovelworld/icon.png.jpeg rename to public/static/src/en/lightnovelworld/icon.png