| import axios from "axios"; | |
| import * as cheerio from "cheerio"; | |
| export function getSlugFromUrl(url) { | |
| try { | |
| const parsed = new URL(url); | |
| const parts = parsed.pathname.split("/").filter(Boolean); | |
| return parts[parts.length - 1] || ''; | |
| } catch (error) { | |
| try { | |
| const parts = url.split("/").filter(Boolean); | |
| return parts[parts.length - 1] || ''; | |
| } catch { | |
| return ''; | |
| } | |
| } | |
| } | |
| function parseUpdateToMs(updateText) { | |
| const now = Date.now(); | |
| const match = updateText.match(/(\d+)\s*(detik|menit|jam|hari|minggu|bulan|tahun)/i); | |
| if (!match) return 0; | |
| const value = parseInt(match[1]); | |
| const unit = match[2].toLowerCase(); | |
| const msPerUnit = { | |
| 'detik': 1000, | |
| 'menit': 60 * 1000, | |
| 'jam': 60 * 60 * 1000, | |
| 'hari': 24 * 60 * 60 * 1000, | |
| 'minggu': 7 * 24 * 60 * 60 * 1000, | |
| 'bulan': 30 * 24 * 60 * 60 * 1000, | |
| 'tahun': 365 * 24 * 60 * 60 * 1000 | |
| }; | |
| const ms = msPerUnit[unit] || 0; | |
| return now - (value * ms); | |
| } | |
| function resizeThumbnail(url, width = 540, height = 350) { | |
| if (!url) return url; | |
| if (url.includes('?resize=')) { | |
| return url.replace(/\?resize=\d+,\d+/, `?resize=${width},${height}`); | |
| } | |
| return url; | |
| } | |
| export class Komiku { | |
| constructor() { | |
| this.BASE_URL = "https://komiku.org"; | |
| this.API_URL = "https://api.komiku.org"; | |
| this.CREATED_BY = "Ditzzy"; | |
| this.NOTE = "Thank you for using this scrape, I hope you appreciate me for making this scrape by not deleting wm"; | |
| } | |
| wrapResponse(data) { | |
| return { | |
| created_by: this.CREATED_BY, | |
| note: this.NOTE, | |
| results: data | |
| }; | |
| } | |
| async search(query, postType = "manga") { | |
| try { | |
| const { data } = await axios.get(`${this.API_URL}/?post_type=${postType}&s=${encodeURIComponent(query)}`); | |
| const $ = cheerio.load(data); | |
| const results = []; | |
| const $containers = $('div.bge'); | |
| for (let index = 0; index < $containers.length; index++) { | |
| const el = $containers[index]; | |
| try { | |
| let thumbnailUrl = ''; | |
| const imgElement = $(el).find('img').first(); | |
| if (imgElement.length > 0) { | |
| thumbnailUrl = imgElement.attr('src') || imgElement.attr('data-src') || ''; | |
| thumbnailUrl = resizeThumbnail(thumbnailUrl); | |
| } | |
| let type = ''; | |
| let genre = ''; | |
| const typeGenreElement = $(el).find('div.tpe1_inf, .tpe1_inf'); | |
| if (typeGenreElement.length > 0) { | |
| const text = typeGenreElement.text().trim(); | |
| const parts = text.split(/\s+/); | |
| if (parts.length > 0) { | |
| type = parts[0].replace(/<\/?b>/g, '').trim(); | |
| genre = parts.slice(1).join(' ').trim(); | |
| } | |
| } | |
| let title = ''; | |
| let mangaUrl = ''; | |
| const h3Element = $(el).find('h3').first(); | |
| if (h3Element.length > 0) { | |
| title = h3Element.text().trim(); | |
| const parentLink = h3Element.parent('a'); | |
| if (parentLink.length > 0) { | |
| mangaUrl = parentLink.attr('href') || ''; | |
| } else { | |
| const nearbyLink = h3Element.closest('div').find('a[href*="/manga/"]').first(); | |
| if (nearbyLink.length > 0) { | |
| mangaUrl = nearbyLink.attr('href') || ''; | |
| } | |
| } | |
| } | |
| if (!title || !mangaUrl) { | |
| $(el).find('a[href*="/manga/"]').each((_, linkEl) => { | |
| const h3 = $(linkEl).find('h3'); | |
| if (h3.length > 0) { | |
| title = h3.text().trim(); | |
| mangaUrl = $(linkEl).attr('href') || ''; | |
| return false; | |
| } | |
| }); | |
| } | |
| if (mangaUrl && !mangaUrl.startsWith('http')) { | |
| mangaUrl = this.BASE_URL + mangaUrl; | |
| } | |
| let lastUpdateMs = 0; | |
| $(el).find('p').each((_, pEl) => { | |
| const text = $(pEl).text().trim(); | |
| if (text.toLowerCase().includes('update')) { | |
| lastUpdateMs = parseUpdateToMs(text); | |
| return false; | |
| } | |
| }); | |
| let firstChapter = null; | |
| let latestChapter = null; | |
| $(el).find('div.new1, .new1').each((_, newEl) => { | |
| const link = $(newEl).find('a'); | |
| if (link.length > 0) { | |
| const spans = link.find('span'); | |
| if (spans.length >= 2) { | |
| const label = spans.first().text().trim().toLowerCase(); | |
| const chapterTitle = spans.last().text().trim(); | |
| const chapterUrl = link.attr('href') || ''; | |
| const fullUrl = chapterUrl && !chapterUrl.startsWith('http') | |
| ? this.BASE_URL + chapterUrl | |
| : chapterUrl; | |
| const chapterSlug = getSlugFromUrl(chapterUrl); | |
| if (label.includes('awal') || label.includes('first')) { | |
| firstChapter = { | |
| title: chapterTitle, | |
| url: fullUrl, | |
| slug: chapterSlug | |
| }; | |
| } else if (label.includes('terbaru') || label.includes('latest')) { | |
| latestChapter = { | |
| title: chapterTitle, | |
| url: fullUrl, | |
| slug: chapterSlug | |
| }; | |
| } | |
| } | |
| } | |
| }); | |
| if (title && mangaUrl) { | |
| const slug = getSlugFromUrl(mangaUrl); | |
| const detail = await this.getDetail(slug); | |
| results.push({ | |
| title, | |
| mangaUrl, | |
| thumbnailUrl, | |
| type, | |
| genre, | |
| lastUpdateMs, | |
| firstChapter, | |
| latestChapter, | |
| detail | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('Error parsing search item:', error); | |
| } | |
| } | |
| return this.wrapResponse(results); | |
| } catch (error) { | |
| if (axios.isAxiosError(error)) { | |
| console.error('Axios error on search:', { | |
| message: error.message, | |
| status: error.response?.status, | |
| statusText: error.response?.statusText | |
| }); | |
| } else { | |
| console.error('Error on search:', error); | |
| } | |
| return []; | |
| } | |
| } | |
| async getDetail(slug) { | |
| try { | |
| const { data } = await axios.get(`${this.BASE_URL}/manga/${slug}`); | |
| const $ = cheerio.load(data); | |
| let results = null; | |
| $('.series').each((_, el) => { | |
| const keyMap = { | |
| 'Judul Komik': 'title', | |
| 'Judul Indonesia': 'indonesia_title', | |
| 'Jenis Komik': 'type', | |
| 'Pengarang': 'author', | |
| 'Status': 'status' | |
| }; | |
| const info = {}; | |
| $(el).find('table.inftable tr').each((_, el) => { | |
| const key = $(el).find('td:first-child').text().trim(); | |
| const value = $(el).find('td:last-child').text().trim(); | |
| if (keyMap[key]) { | |
| info[keyMap[key]] = value; | |
| } | |
| }); | |
| const genre = []; | |
| $('ul.genre li.genre span[itemprop="genre"]').each((_, el) => { | |
| genre.push($(el).text().trim()); | |
| }); | |
| const synopsis = $('p.desc').text().trim(); | |
| let thumbnailUrl = $('div.ims img[itemprop="image"]').attr("src")?.trim() || ''; | |
| thumbnailUrl = resizeThumbnail(thumbnailUrl); | |
| const chapters = []; | |
| $('table#Daftar_Chapter tr:not(:first-child)').each((_, el) => { | |
| const chapter = $(el).find('td.judulseries a span').text().trim(); | |
| const slug_chapter = $(el).find('td.judulseries a').attr('href')?.replace(/\//g, '') || ''; | |
| const views = $(el).find('td.pembaca i').text().trim(); | |
| const date = $(el).find('td.tanggalseries').text().trim(); | |
| chapters.push({ chapter, slug_chapter, views, date }); | |
| }); | |
| results = { | |
| ...info, | |
| thumbnailUrl, | |
| synopsis, | |
| genre, | |
| chapters | |
| }; | |
| }); | |
| return this.wrapResponse(results); | |
| } catch (error) { | |
| if (axios.isAxiosError(error)) { | |
| console.error('Axios error fetching detail:', { | |
| message: error.message, | |
| status: error.response?.status, | |
| statusText: error.response?.statusText | |
| }); | |
| } else { | |
| console.error('Error fetching detail:', error); | |
| } | |
| return this.wrapResponse(null); | |
| } | |
| } | |
| async readChapter(chapterSlug) { | |
| try { | |
| const { data } = await axios.get(`${this.BASE_URL}/${chapterSlug}/`); | |
| const $ = cheerio.load(data); | |
| const title = $('#Judul h1').text().trim(); | |
| const images = []; | |
| $('#Baca_Komik img').each((_, el) => { | |
| const imageUrl = $(el).attr('src') || ''; | |
| const index = parseInt($(el).attr('id') || '0'); | |
| if (imageUrl && index) { | |
| images.push({ | |
| index, | |
| imageUrl | |
| }); | |
| } | |
| }); | |
| images.sort((a, b) => a.index - b.index); | |
| const chapterNumber = chapterSlug.match(/chapter-(\d+)/)?.[1] || ''; | |
| const seriesTitle = title.split('Chapter')[0].trim(); | |
| const seriesSlug = chapterSlug.split('-chapter-')[0]; | |
| const seriesUrl = `${this.BASE_URL}/manga/${seriesSlug}`; | |
| const result = { | |
| title, | |
| chapterNumber, | |
| seriesTitle, | |
| seriesUrl, | |
| totalImages: images.length, | |
| images, | |
| }; | |
| return this.wrapResponse(result); | |
| } catch (error) { | |
| if (axios.isAxiosError(error)) { | |
| console.error('Axios error reading chapter:', { | |
| message: error.message, | |
| status: error.response?.status, | |
| statusText: error.response?.statusText | |
| }); | |
| } else { | |
| console.error('Error reading chapter:', error); | |
| } | |
| return this.wrapResponse(null); | |
| } | |
| } | |
| async getLatestPopularManga() { | |
| try { | |
| const { data } = await axios.get(this.BASE_URL); | |
| const $ = cheerio.load(data); | |
| const results = []; | |
| $(".home #Komik_Hot_Manga article.ls2").each((_, el) => { | |
| try { | |
| const title = $(el).find(".ls2j h3 a").text().trim(); | |
| const mangaUrlPath = $(el).find(".ls2j h3 a").attr("href"); | |
| const mangaUrl = mangaUrlPath ? `${this.BASE_URL}${mangaUrlPath}` : ''; | |
| const slug = mangaUrl ? getSlugFromUrl(mangaUrl) : ''; | |
| let thumbnailUrl = | |
| $(el).find("img").attr("data-src") || | |
| $(el).find("img").attr("src") || | |
| ''; | |
| thumbnailUrl = resizeThumbnail(thumbnailUrl); | |
| const genreView = $(el).find(".ls2t").text().trim(); | |
| const latestChapter = $(el).find(".ls2l").text().trim(); | |
| const chapterUrlPath = $(el).find(".ls2l").attr("href"); | |
| const chapterUrl = chapterUrlPath ? `${this.BASE_URL}${chapterUrlPath}` : ''; | |
| if (title && mangaUrl) { | |
| results.push({ | |
| title, | |
| mangaUrl, | |
| thumbnailUrl, | |
| genreView, | |
| slug, | |
| latestChapter, | |
| chapterUrl | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('Error parsing manga item:', error); | |
| } | |
| }); | |
| return this.wrapResponse(results); | |
| } catch (error) { | |
| if (axios.isAxiosError(error)) { | |
| console.error('Axios error fetching latest manga:', { | |
| message: error.message, | |
| status: error.response?.status, | |
| statusText: error.response?.statusText | |
| }); | |
| } else { | |
| console.error('Error fetching latest manga:', error); | |
| } | |
| return []; | |
| } | |
| } | |
| async getLatestPopularManhwa() { | |
| try { | |
| const { data } = await axios.get(this.BASE_URL); | |
| const $ = cheerio.load(data); | |
| const results = []; | |
| $(".home #Komik_Hot_Manhwa article.ls2").each((_, el) => { | |
| try { | |
| const title = $(el).find(".ls2j h3 a").text().trim(); | |
| const mangaUrlPath = $(el).find(".ls2j h3 a").attr("href"); | |
| const mangaUrl = mangaUrlPath ? `${this.BASE_URL}${mangaUrlPath}` : ''; | |
| const slug = mangaUrl ? getSlugFromUrl(mangaUrl) : ''; | |
| let thumbnailUrl = | |
| $(el).find("img").attr("data-src") || | |
| $(el).find("img").attr("src") || | |
| ''; | |
| thumbnailUrl = resizeThumbnail(thumbnailUrl); | |
| const genreView = $(el).find(".ls2t").text().trim(); | |
| const latestChapter = $(el).find(".ls2l").text().trim(); | |
| const chapterUrlPath = $(el).find(".ls2l").attr("href"); | |
| const chapterUrl = chapterUrlPath ? `${this.BASE_URL}${chapterUrlPath}` : ''; | |
| if (title && mangaUrl) { | |
| results.push({ | |
| title, | |
| mangaUrl, | |
| thumbnailUrl, | |
| genreView, | |
| slug, | |
| latestChapter, | |
| chapterUrl | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('Error parsing manga item:', error); | |
| } | |
| }); | |
| return this.wrapResponse(results); | |
| } catch (error) { | |
| if (axios.isAxiosError(error)) { | |
| console.error('Axios error fetching latest manga:', { | |
| message: error.message, | |
| status: error.response?.status, | |
| statusText: error.response?.statusText | |
| }); | |
| } else { | |
| console.error('Error fetching latest manga:', error); | |
| } | |
| return []; | |
| } | |
| } | |
| async getLatestPopularManhua() { | |
| try { | |
| const { data } = await axios.get(this.BASE_URL); | |
| const $ = cheerio.load(data); | |
| const results = []; | |
| $(".home #Komik_Hot_Manhua article.ls2").each((_, el) => { | |
| try { | |
| const title = $(el).find(".ls2j h3 a").text().trim(); | |
| const mangaUrlPath = $(el).find(".ls2j h3 a").attr("href"); | |
| const mangaUrl = mangaUrlPath ? `${this.BASE_URL}${mangaUrlPath}` : ''; | |
| const slug = mangaUrl ? getSlugFromUrl(mangaUrl) : ''; | |
| let thumbnailUrl = | |
| $(el).find("img").attr("data-src") || | |
| $(el).find("img").attr("src") || | |
| ''; | |
| thumbnailUrl = resizeThumbnail(thumbnailUrl); | |
| const genreView = $(el).find(".ls2t").text().trim(); | |
| const latestChapter = $(el).find(".ls2l").text().trim(); | |
| const chapterUrlPath = $(el).find(".ls2l").attr("href"); | |
| const chapterUrl = chapterUrlPath ? `${this.BASE_URL}${chapterUrlPath}` : ''; | |
| if (title && mangaUrl) { | |
| results.push({ | |
| title, | |
| mangaUrl, | |
| thumbnailUrl, | |
| genreView, | |
| slug, | |
| latestChapter, | |
| chapterUrl | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('Error parsing manga item:', error); | |
| } | |
| }); | |
| return this.wrapResponse(results); | |
| } catch (error) { | |
| if (axios.isAxiosError(error)) { | |
| console.error('Axios error fetching latest manga:', { | |
| message: error.message, | |
| status: error.response?.status, | |
| statusText: error.response?.statusText | |
| }); | |
| } else { | |
| console.error('Error fetching latest manga:', error); | |
| } | |
| return []; | |
| } | |
| } | |
| } |