mangakakalot-api
Version:
Lightweight scraper library for MangaKakalot.gg (no server)
504 lines (434 loc) • 16.5 kB
JavaScript
const axios = require("axios");
const cheerio = require("cheerio");
const baseUrl = "https://www.mangakakalot.gg";
const ANILIST_API = "https://graphql.anilist.co";
// Common headers for all requests
const headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Referer': 'https://www.mangakakalot.gg/',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5'
};
// AniList GraphQL query for high quality images
const searchQuery = `
query ($search: String) {
Media (search: $search, type: MANGA) {
id
title {
romaji
english
}
coverImage {
extraLarge
large
}
bannerImage
}
}
`;
// Function to search AniList for high quality images
async function getAniListImages(title) {
try {
const { data } = await axios.post(ANILIST_API, {
query: searchQuery,
variables: { search: title }
});
const media = data?.data?.Media;
if (!media) return null;
return {
id: media.id,
poster: media.coverImage?.extraLarge || media.coverImage?.large || null,
banner: media.bannerImage || null
};
} catch (error) {
console.error("Error fetching AniList images:", error.message);
return null;
}
}
// Function to scrape manga search results
const scrapeMangaSearch = async (query, page = 1) => {
try {
const searchUrl = `${baseUrl}/search/story/${query}${page > 1 ? '?page=' + page : ''}`;
const { data } = await axios.get(searchUrl, { headers });
const $ = cheerio.load(data);
const mangas = [];
// Extract manga items from search results - updated selectors
$('.panel_story_list .story_item').each((i, el) => {
const title = $(el).find('.story_name a').text().trim();
const url = $(el).find('.story_name a').attr('href');
const id = url ? url.split('/manga/')[1] : '';
const image = $(el).find('img').attr('src');
// Extract chapters
const chapters = [];
$(el).find('.story_chapter a').each((i, chapterEl) => {
const chapterName = $(chapterEl).text().trim();
const chapterUrl = $(chapterEl).attr('href');
const chapterId = chapterUrl ? chapterUrl.split('/').pop() : '';
chapters.push({
id: chapterId,
name: chapterName
});
});
// Extract author, updated date, and views
const authorText = $(el).find('.story_item_right span:contains("Author")').text().trim();
const author = authorText.replace('Author(s) :', '').trim();
const updatedText = $(el).find('.story_item_right span:contains("Updated")').text().trim();
const updated = updatedText.replace('Updated :', '').trim();
const viewsText = $(el).find('.story_item_right span:contains("View")').text().trim();
const views = viewsText.replace('View :', '').trim();
mangas.push({
id,
title,
image,
latestChapters: chapters,
author,
updated,
views: views.replace(/,/g, '') || 0
});
});
// Extract pagination info - updated selectors to handle the actual format
const currentPage = parseInt($('.panel_page_number .page_select').text().trim()) || page;
const hasNextPage = $('.panel_page_number .page_last').length > 0 &&
!$('.panel_page_number .page_last').hasClass('page_blue');
let totalPages = 1;
const lastPageText = $('.panel_page_number .page_last').text().trim();
const totalPagesMatch = lastPageText.match(/Last\((\d+)\)/);
if (totalPagesMatch && totalPagesMatch[1]) {
totalPages = parseInt(totalPagesMatch[1]);
}
// Extract total manga count
let totalMangas = 0;
const totalMangasText = $('.panel_page_number .group_qty .page_blue').text().trim();
const totalMangasMatch = totalMangasText.match(/Total: (\d+) stories/);
if (totalMangasMatch && totalMangasMatch[1]) {
totalMangas = parseInt(totalMangasMatch[1]);
}
return {
mangas,
currentPage,
hasNextPage,
totalPages,
totalMangas
};
} catch (error) {
console.error("Error scraping manga search:", error);
throw error;
}
};
// Function to scrape manga details
const scrapeMangaDetails = async (mangaId) => {
try {
const mangaUrl = `${baseUrl}/manga/${mangaId}`;
const { data } = await axios.get(mangaUrl, { headers });
const $ = cheerio.load(data);
// Extract basic manga info - updated selectors
const title = $('.manga-info-text h1').text().trim();
const image = $('.manga-info-pic img').attr('src');
// Extract alternative titles - updated selector
const altTitles = $('.manga-info-text .story-alternative').text().replace('Alternative :', '').trim();
// Extract author and status - updated selectors
const authorText = $('.manga-info-text li:contains("Author")').text().trim();
const author = authorText.replace('Author(s) :', '').trim();
const statusText = $('.manga-info-text li:contains("Status")').text().trim();
const status = statusText.replace('Status :', '').trim();
// Extract genres - updated selector
const genres = [];
$('.manga-info-text li.genres a').each((i, el) => {
genres.push($(el).text().trim());
});
// Extract description - updated selector
const description = $('#contentBox').text().trim();
// Extract chapters - updated selectors
const chapters = [];
$('.chapter-list .row').each((i, el) => {
const chapterLink = $(el).find('span:first-child a');
const chapterName = chapterLink.text().trim();
const chapterUrl = chapterLink.attr('href');
const chapterId = chapterUrl ? chapterUrl.split('/').pop() : '';
const date = $(el).find('span:last-child').text().trim();
const views = $(el).find('span:nth-child(2)').text().trim();
chapters.push({
id: chapterId,
name: chapterName,
date,
views: parseInt(views) || 0
});
});
// Try to get high quality images from AniList
const anilistData = await getAniListImages(title);
return {
id: mangaId,
title,
altTitles,
image,
author,
status,
genres,
description: description, // Now including description in the response
chapters,
anilistId: anilistData?.id || null,
poster: anilistData?.poster || image,
banner: anilistData?.banner || null
};
} catch (error) {
console.error("Error scraping manga details:", error);
throw error;
}
};
// Function to scrape chapter images
const scrapeChapterImages = async (mangaId, chapterId) => {
try {
const chapterUrl = `${baseUrl}/manga/${mangaId}/${chapterId}`;
const { data } = await axios.get(chapterUrl, { headers });
const $ = cheerio.load(data);
// Extract chapter title
const title = $('.info-top-chapter h2').text().trim();
// Extract manga title
const mangaTitle = $('.breadcrumb p span:nth-child(2) a').text().trim();
// Extract chapter images
const images = [];
$('.container-chapter-reader img').each((i, el) => {
const imageUrl = $(el).attr('src');
if (imageUrl) {
images.push(imageUrl);
}
});
// Extract chapter navigation
const prevChapter = $('.btn-navigation-chap a.next').attr('href');
const nextChapter = $('.btn-navigation-chap a.back').attr('href');
// Extract all chapters for navigation
const allChapters = [];
$('.navi-change-chapter option').each((i, el) => {
const chapterName = $(el).text().trim();
const chapterUrl = $(el).attr('data-c');
const chapterId = chapterUrl ? chapterUrl.split('/').pop() : '';
allChapters.push({
id: chapterId,
name: chapterName
});
});
return {
id: chapterId,
title,
mangaId,
mangaTitle,
images,
prevChapter: prevChapter ? prevChapter.split('/').pop() : null,
nextChapter: nextChapter ? nextChapter.split('/').pop() : null,
allChapters
};
} catch (error) {
console.error("Error scraping chapter images:", error);
throw error;
}
};
// Function to scrape latest manga updates
const scrapeLatestMangas = async (page = 1) => {
try {
// Updated URL to match the actual site structure
const latestUrl = `${baseUrl}/manga-list/latest-manga${page > 1 ? '?page=' + page : ''}`;
const { data } = await axios.get(latestUrl, { headers });
const $ = cheerio.load(data);
return scrapeMangaList($);
} catch (error) {
console.error("Error scraping latest mangas:", error);
throw error;
}
};
// Function to scrape popular manga
const scrapePopularMangas = async (page = 1) => {
try {
// Updated URL to match the actual site structure
const popularUrl = `${baseUrl}/manga-list/hot-manga${page > 1 ? '?page=' + page : ''}`;
const { data } = await axios.get(popularUrl, { headers });
const $ = cheerio.load(data);
return scrapeMangaList($);
} catch (error) {
console.error("Error scraping popular mangas:", error);
throw error;
}
};
// Function to scrape newest manga
const scrapeNewestMangas = async (page = 1) => {
try {
// Updated URL to match the actual site structure
const newestUrl = `${baseUrl}/manga-list/new-manga${page > 1 ? '?page=' + page : ''}`;
const { data } = await axios.get(newestUrl, { headers });
const $ = cheerio.load(data);
return scrapeMangaList($);
} catch (error) {
console.error("Error scraping newest mangas:", error);
throw error;
}
};
// Function to scrape completed manga
const scrapeCompletedMangas = async (page = 1) => {
try {
// Updated URL to match the actual site structure
const completedUrl = `${baseUrl}/manga-list/completed-manga${page > 1 ? '?page=' + page : ''}`;
const { data } = await axios.get(completedUrl, { headers });
const $ = cheerio.load(data);
return scrapeMangaList($);
} catch (error) {
console.error("Error scraping completed mangas:", error);
throw error;
}
};
// Function to scrape popular now manga (from homepage)
const scrapePopularNowMangas = async () => {
try {
// Get the homepage data
const { data } = await axios.get(baseUrl, { headers });
const $ = cheerio.load(data);
const mangas = [];
// Find the items in the carousel/slider section
$('.slide .owl-carousel .item').each((i, el) => {
const title = $(el).find('.slide-caption h3 a').text().trim();
const url = $(el).find('.slide-caption h3 a').attr('href');
const id = url ? url.split('/manga/')[1] : '';
const image = $(el).find('img').attr('src');
// Get the latest chapter
const latestChapterName = $(el).find('.slide-caption > a').text().trim();
const latestChapterUrl = $(el).find('.slide-caption > a').attr('href');
const latestChapterId = latestChapterUrl ? latestChapterUrl.split('/').pop() : '';
mangas.push({
id,
title,
image,
latestChapter: {
id: latestChapterId,
name: latestChapterName
}
});
});
return {
mangas,
count: mangas.length
};
} catch (error) {
console.error("Error scraping popular now mangas:", error);
throw error;
}
};
// Function to scrape homepage data
const scrapeHomePage = async () => {
try {
// Get the homepage data
const { data } = await axios.get(baseUrl, { headers });
const $ = cheerio.load(data);
// Result object structure
const result = {
popularManga: {
title: $('.slide .title.update-slide').text().trim(),
mangas: []
},
latestUpdates: {
title: $('.daily-update .title.update-title').text().trim(),
mangas: []
}
};
// Scrape popular manga slider
$('.slide .owl-carousel .item').each((i, el) => {
const title = $(el).find('.slide-caption h3 a').text().trim();
const url = $(el).find('.slide-caption h3 a').attr('href');
const id = url ? url.split('/manga/')[1] : '';
const image = $(el).find('img').attr('src');
// Get the latest chapter
const latestChapterName = $(el).find('.slide-caption > a').text().trim();
const latestChapterUrl = $(el).find('.slide-caption > a').attr('href');
const latestChapterId = latestChapterUrl ? latestChapterUrl.split('/').pop() : '';
result.popularManga.mangas.push({
id,
title,
image,
latestChapter: {
id: latestChapterId,
name: latestChapterName
}
});
});
// Scrape latest updates
$('#contentstory .itemupdate').each((i, el) => {
const title = $(el).find('h3 a').text().trim();
const url = $(el).find('h3 a').attr('href');
const id = url ? url.split('/manga/')[1] : '';
const image = $(el).find('.cover img').attr('src') || $(el).find('.cover img').attr('data-src');
// Get the latest chapters
const chapters = [];
$(el).find('ul li').each((i, chapterEl) => {
if (i === 0) return; // Skip the first li which contains the title
const chapterName = $(chapterEl).find('a').text().trim();
const chapterUrl = $(chapterEl).find('a').attr('href');
const chapterId = chapterUrl ? chapterUrl.split('/').pop() : '';
const timestamp = $(chapterEl).find('i').text().trim();
if (chapterName && chapterUrl) {
chapters.push({
id: chapterId,
name: chapterName,
timestamp
});
}
});
result.latestUpdates.mangas.push({
id,
title,
image,
latestChapters: chapters
});
});
return result;
} catch (error) {
console.error("Error scraping homepage:", error);
throw error;
}
};
// Helper function to scrape manga list pages
const scrapeMangaList = ($) => {
const mangas = [];
$('.list-truyen-item-wrap').each((i, el) => {
const title = $(el).find('h3 a').text().trim();
const url = $(el).find('h3 a').attr('href');
const id = url ? url.split('/manga/')[1] : '';
const image = $(el).find('a.list-story-item img').attr('src');
const latestChapter = $(el).find('a.list-story-item-wrap-chapter').text().trim();
const views = $(el).find('span.aye_icon').text().trim();
// Extract genres from description
let genres = [];
const description = $(el).find('p').text().trim();
const genreMatch = description.match(/popular manga covering in ([^,]+(?:,[^,]+)*)/);
if (genreMatch && genreMatch[1]) {
genres = genreMatch[1].split(',').map(genre => genre.trim());
}
mangas.push({
id,
title,
image,
latestChapter,
views: parseInt(views) || 0,
genres
});
});
// Extract pagination info
const currentPage = parseInt($('.panel_page_number .page_select').text().trim()) || 1;
const hasNextPage = $('.panel_page_number .page_last').length > 0;
const totalPages = $('.panel_page_number .page_last').text().match(/\((\d+)\)/)?.[1] || 0;
const totalMangas = $('.panel_page_number .group_qty .page_blue').text().match(/Total: (\d+) stories/)?.[1] || 0;
return {
mangas,
currentPage,
hasNextPage,
totalPages: parseInt(totalPages),
totalMangas: parseInt(totalMangas)
};
};
module.exports = {
scrapeChapterImages,
scrapeMangaDetails,
scrapeMangaSearch,
scrapePopularMangas,
scrapeLatestMangas,
scrapeNewestMangas,
scrapeCompletedMangas,
scrapePopularNowMangas,
scrapeHomePage
};