From cdc9335daad8cf67ce68b183033498179bcf709b Mon Sep 17 00:00:00 2001 From: Rojikku Date: Tue, 13 Jan 2026 19:07:01 -0500 Subject: [PATCH] fix: New KDTnovel domain and format --- plugins/english/kdtnovels.ts | 377 ------------------ .../lightnovelwp/filters/kdtnovels.json | 172 ++++++++ plugins/multisrc/lightnovelwp/sources.json | 9 + .../multisrc/lightnovelwp/kdtnovels/icon.png | Bin 0 -> 4118 bytes public/static/src/en/kdtnovels/icon.png | Bin 10696 -> 0 bytes 5 files changed, 181 insertions(+), 377 deletions(-) delete mode 100644 plugins/english/kdtnovels.ts create mode 100644 plugins/multisrc/lightnovelwp/filters/kdtnovels.json create mode 100644 public/static/multisrc/lightnovelwp/kdtnovels/icon.png delete mode 100644 public/static/src/en/kdtnovels/icon.png diff --git a/plugins/english/kdtnovels.ts b/plugins/english/kdtnovels.ts deleted file mode 100644 index cbd340fb8..000000000 --- a/plugins/english/kdtnovels.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { load as parseHTML } from 'cheerio'; -import { fetchApi } from '@libs/fetch'; -import { Plugin } from '@/types/plugin'; -import { defaultCover } from '@libs/defaultCover'; -import { NovelStatus } from '@libs/novelStatus'; - -class KDTNovels implements Plugin.PluginBase { - id = 'kdtnovels'; - name = 'KDT Novels'; - version = '1.0.0'; - icon = 'src/en/kdtnovels/icon.png'; - site = 'https://kdtnovels.com/'; - - /** - * Parse novel items from HTML using common selectors - */ - private parseNovelItems(html: string): Plugin.NovelItem[] { - const $ = parseHTML(html); - const novels: Plugin.NovelItem[] = []; - - $('div.c-tabs-item__content').each((_, element) => { - const $element = $(element); - - // Extract cover image - const coverImg = $element.find('div.tab-thumb img').first(); - const cover = - coverImg.attr('data-src') || coverImg.attr('src') || defaultCover; - - // Extract title and URL - const titleLink = $element.find('div.post-title > h3 > a').first(); - const name = titleLink.text().trim(); - const href = titleLink.attr('href'); - - // Only add if we have required data - if (name && href) { - // Convert full URL to relative path, remove leading and trailing slashes - const path = href.replace(this.site, '').replace(/^\/+|\/+$/g, ''); - - novels.push({ - name, - path, - cover: cover || defaultCover, - }); - } - }); - - return novels; - } - - /** - * Parse status text and map it to NovelStatus enum - */ - private parseNovelStatus(statusText: string): string { - if (!statusText) { - return NovelStatus.Unknown; - } - - const normalizedStatus = statusText.toLowerCase().trim(); - - // Map common status patterns to NovelStatus enum values - if ( - normalizedStatus.includes('ongoing') || - normalizedStatus.includes('on going') - ) { - return NovelStatus.Ongoing; - } - - if ( - normalizedStatus.includes('completed') || - normalizedStatus.includes('complete') - ) { - return NovelStatus.Completed; - } - - if (normalizedStatus.includes('licensed')) { - return NovelStatus.Licensed; - } - - if ( - normalizedStatus.includes('finished') || - normalizedStatus.includes('publishing finished') - ) { - return NovelStatus.PublishingFinished; - } - - if ( - normalizedStatus.includes('cancelled') || - normalizedStatus.includes('canceled') - ) { - return NovelStatus.Cancelled; - } - - if ( - normalizedStatus.includes('hiatus') || - normalizedStatus.includes('on hiatus') - ) { - return NovelStatus.OnHiatus; - } - - // If no pattern matches, return the original text or Unknown - return statusText || NovelStatus.Unknown; - } - - async popularNovels( - pageNo: number, - options: Plugin.PopularNovelsOptions, - ): Promise { - // Construct URL based on whether we want latest or popular novels - const orderBy = options.showLatestNovels ? 'latest' : 'views'; - const url = `${this.site}/page/${pageNo}/?s&post_type=wp-manga&m_orderby=${orderBy}`; - - try { - // Fetch the search results page - const response = await fetchApi(url); - const html = await response.text(); - - // Use common parsing method - return this.parseNovelItems(html); - } catch (error) { - console.error('Error fetching popular novels:', error); - return []; - } - } - - async searchNovels( - searchTerm: string, - pageNo: number, - ): Promise { - // Construct search URL with proper query parameters - const url = `${this.site}/page/${pageNo}/?s=${encodeURIComponent(searchTerm)}&post_type=wp-manga&op&author&artist&release&adult`; - - try { - // Fetch the search results page - const response = await fetchApi(url); - const html = await response.text(); - - // Use common parsing method - const novels = this.parseNovelItems(html); - - // Handle empty search results gracefully - if (novels.length === 0) { - console.log( - `No search results found for term: "${searchTerm}" on page ${pageNo}`, - ); - } - - return novels; - } catch (error) { - console.error('Error searching novels:', error); - return []; - } - } - - async parseNovel(novelPath: string): Promise { - const novelUrl = this.site + novelPath; - - try { - // Fetch the novel details page - const response = await fetchApi(novelUrl); - const html = await response.text(); - - // Parse HTML with Cheerio - const $ = parseHTML(html); - - const novel: Plugin.SourceNovel = { - name: '', - path: novelPath, - cover: defaultCover, - author: '', - artist: '', - genres: '', - summary: '', - status: '', - chapters: [], - }; - - // Extract novel metadata using provided CSS selectors - - // Extract novel title - const titleElement = $('.manga-title').first(); - novel.name = titleElement.text().trim() || 'Unknown Title'; - - // Extract cover image - const coverElement = $('div.summary_image img').first(); - const coverSrc = - coverElement.attr('data-src') || coverElement.attr('src'); - novel.cover = coverSrc || defaultCover; - - // Extract genres - const genreElements = $('div.genres-content a'); - const genres: string[] = []; - genreElements.each((_, element) => { - const genre = $(element).text().trim(); - if (genre) { - genres.push(genre); - } - }); - novel.genres = genres.join(','); - - // Extract summary/synopsis - const summaryElements = $('div.manga-excerpt p'); - const summaryParts: string[] = []; - summaryElements.each((_, element) => { - const text = $(element).text().trim(); - if (text) { - summaryParts.push(text); - } - }); - novel.summary = summaryParts.join('\n\n'); - - // Extract status - const statusElement = $('div.manga-status span:nth-child(2)').first(); - const rawStatus = statusElement.text().trim(); - novel.status = this.parseNovelStatus(rawStatus); - - // Extract chapter list - chapters are loaded dynamically via AJAX - let chapters: Plugin.ChapterItem[] = []; - - try { - // Construct AJAX URL for chapter list - const ajaxUrl = `${novelUrl}/ajax/chapters/?t=1`; - - // Make POST request to get chapter list HTML fragment - const chapterResponse = await fetchApi(ajaxUrl, { - method: 'POST', - }); - const chapterHtml = await chapterResponse.text(); - - // Parse the HTML fragment - const $chapters = parseHTML(chapterHtml); - - // Extract chapters using existing parsing logic - const chapterElements = $chapters('li.free-chap'); - - chapterElements.each((index, element) => { - const $element = $chapters(element); - - // Extract chapter link and name - const chapterLink = $element.find('a').first(); - const chapterName = chapterLink.text().trim(); - const chapterHref = chapterLink.attr('href'); - - // Extract release date - const releaseDateElement = $element - .find('span.chapter-release-date') - .first(); - const releaseTime = releaseDateElement.text().trim() || null; - - // Extract chapter number from title (opportunistic parsing) - let chapterNumber = chapterElements.length - index; // Default fallback (reverse order) - - // Try multiple patterns for chapter number extraction (supporting decimals) - let chapterNumberMatch = chapterName.match(/Ch\s*(\d+(?:\.\d+)?)/i); - if (!chapterNumberMatch) { - chapterNumberMatch = chapterName.match(/c(\d+(?:\.\d+)?)/i); - } - if (chapterNumberMatch) { - chapterNumber = parseFloat(chapterNumberMatch[1]); - } - - if (chapterName && chapterHref) { - // Extract just the pathname from the URL, removing leading and trailing slashes - let chapterPath: string; - if (chapterHref.startsWith('http')) { - const url = new URL(chapterHref); - chapterPath = url.pathname.replace(/^\/+|\/+$/g, ''); - } else { - chapterPath = chapterHref.replace(/^\/+|\/+$/g, ''); - } - - chapters.push({ - name: chapterName, - path: chapterPath, - releaseTime: releaseTime, - chapterNumber: chapterNumber, - }); - } - }); - - // Reverse the chapters array since they come in reverse order (latest first) - chapters.reverse(); - } catch (chapterError) { - console.error('Error fetching chapter list:', chapterError); - // Fall back to empty chapters array if AJAX request fails - chapters = []; - } - - novel.chapters = chapters; - - return novel; - } catch (error) { - console.error('Error parsing novel:', error); - // Return basic novel object with minimal data on error - return { - name: 'Error loading novel', - path: novelPath, - cover: defaultCover, - chapters: [], - }; - } - } - - async parseChapter(chapterPath: string): Promise { - const chapterUrl = this.site + chapterPath; - - try { - // Implement HTTP request to fetch chapter page - const response = await fetchApi(chapterUrl); - const html = await response.text(); - - // Parse HTML with Cheerio - const $ = parseHTML(html); - - // Identify main chapter content container using CSS selectors - const contentContainer = $('div.reading-content').first(); - - if (contentContainer.length === 0) { - console.warn('Chapter content container not found'); - return ''; - } - - // Remove hidden input elements used for tracking (infinite scrolling) - contentContainer.find('input[type="hidden"]').remove(); - - // Handle embedded images - ensure they have proper attributes - contentContainer.find('img').each((_, img) => { - const $img = $(img); - - // Handle lazy-loaded images by moving data-src to src if needed - const dataSrc = $img.attr('data-src'); - if (dataSrc && !$img.attr('src')) { - $img.attr('src', dataSrc); - } - - // Ensure images have alt text for accessibility - if (!$img.attr('alt')) { - $img.attr('alt', 'Chapter image'); - } - }); - - // Handle special formatting elements - preserve paragraph structure - // Ensure proper spacing between paragraphs and other block elements - contentContainer.find('p, div, br').each((_, element) => { - const $element = $(element); - - // Ensure paragraphs have proper spacing - if (element.tagName?.toLowerCase() === 'p' && $element.text().trim()) { - // Paragraph already has proper HTML structure - } - - // Handle div elements that might contain text - if ( - element.tagName?.toLowerCase() === 'div' && - $element.text().trim() - ) { - // Preserve div structure for special formatting - } - }); - - // Extract the HTML content while preserving formatting - let chapterContent = contentContainer.html(); - - // Filter out HTML comments - if (chapterContent) { - chapterContent = chapterContent.replace(//g, ''); - } - - // Return properly formatted HTML string - return chapterContent?.trim() || ''; - } catch (error) { - console.error('Error parsing chapter:', error); - return ''; - } - } -} - -export default new KDTNovels(); diff --git a/plugins/multisrc/lightnovelwp/filters/kdtnovels.json b/plugins/multisrc/lightnovelwp/filters/kdtnovels.json new file mode 100644 index 000000000..3675b0c72 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/kdtnovels.json @@ -0,0 +1,172 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "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": "Genderswap", + "value": "genderswap" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Monster Girls", + "value": "monster-girls" + }, + { + "label": "Monsters", + "value": "monsters" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "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": "Survival", + "value": "survival" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Light Novel (JP)", + "value": "light-novel-jp" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/sources.json b/plugins/multisrc/lightnovelwp/sources.json index 02e6f9661..c83dd10dd 100644 --- a/plugins/multisrc/lightnovelwp/sources.json +++ b/plugins/multisrc/lightnovelwp/sources.json @@ -308,5 +308,14 @@ "lang": "English", "reverseChapters": true } + }, + { + "id": "kdtnovels", + "sourceSite": "https://kdtnovels.net/", + "sourceName": "KDT Novels", + "options": { + "lang": "English", + "reverseChapters": true + } } ] diff --git a/public/static/multisrc/lightnovelwp/kdtnovels/icon.png b/public/static/multisrc/lightnovelwp/kdtnovels/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..10692abb9c38937fa4df68bac6fb1d30bbf00897 GIT binary patch literal 4118 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 UKw1F3g#Z8m07*qoM6N<$f^V1J2LJ#7 literal 0 HcmV?d00001 diff --git a/public/static/src/en/kdtnovels/icon.png b/public/static/src/en/kdtnovels/icon.png deleted file mode 100644 index b962d7edd7c9a32ba997aa74b59ff18168f9343e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10696 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