chimi-scraper
Version:
A TypeScript library for scraping game data from itch.io with a clean, scalable architecture
296 lines • 11.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ItchIO = void 0;
const cheerio = __importStar(require("cheerio"));
const game_parser_1 = require("../../models/game-parser");
const utils_1 = require("../../utils");
class ItchIO extends game_parser_1.GameParser {
constructor() {
super('https://itch.io', 'ItchIO', 'GAMES');
this.httpClient = new utils_1.HttpClient(this.baseUrl);
}
async search(query, page = 1) {
try {
const searchUrl = `/search?q=${encodeURIComponent(query)}&page=${page}`;
const html = await this.httpClient.get(searchUrl);
const $ = cheerio.load(html);
const results = [];
$('.game_cell').each((_, element) => {
const gameResult = this.parseGameCell($, element);
if (gameResult) {
results.push(gameResult);
}
});
const pagination = this.parsePagination($);
return {
currentPage: page,
results,
...pagination
};
}
catch (error) {
throw new Error(`Failed to search games: ${error}`);
}
}
async fetchNewAndPopular(page = 1) {
return this.fetchGamesByCategory('new-and-popular', page);
}
async fetchTopSellers(page = 1) {
return this.fetchGamesByCategory('top-sellers', page);
}
async fetchTopRated(page = 1) {
return this.fetchGamesByCategory('top-rated', page);
}
async fetchNewest(page = 1) {
return this.fetchGamesByCategory('newest', page);
}
async fetchGameInfo(gameUrl) {
try {
const url = gameUrl.startsWith('http') ? gameUrl : `${this.baseUrl}${gameUrl}`;
const html = await this.httpClient.get(url);
const $ = cheerio.load(html);
const title = $('.game_title').first().text().trim() || $('h1').first().text().trim();
const description = $('.formatted_description, .user_formatted').first().text().trim();
// Extract basic info
const developer = this.extractDeveloper($);
const cover = this.extractCoverImage($);
const platforms = this.extractPlatforms($);
const genres = this.extractGenres($);
const tags = this.extractTags($);
// Extract pricing info
const { price, isFree, isOnSale, originalPrice } = this.extractPricingInfo($);
// Extract media
const screenshots = this.extractScreenshots($);
const videos = this.extractVideos($);
// Extract ratings
const { rating, ratingCount } = this.extractRatingInfo($);
// Extract release date
const releaseDate = $('.game_info_panel_widget abbr, .release_date abbr').attr('title');
return {
id: utils_1.ParserUtils.extractIdFromUrl(url),
title,
url,
cover,
description,
developer,
platforms,
genres,
tags,
price,
isFree,
isOnSale,
originalPrice,
rating,
ratingCount,
releaseDate,
// Additional metadata
screenshots: screenshots.map(s => s.url),
videos: videos.map(v => v.url)
};
}
catch (error) {
throw new Error(`Failed to fetch game info: ${error}`);
}
}
async fetchGamesByCategory(category, page) {
try {
const categoryUrl = `/games/${category}${page > 1 ? `?page=${page}` : ''}`;
const html = await this.httpClient.get(categoryUrl);
const $ = cheerio.load(html);
const results = [];
$('.game_cell, .game_cell_data').each((_, element) => {
const gameResult = this.parseGameCell($, element);
if (gameResult) {
results.push(gameResult);
}
});
const pagination = this.parsePagination($);
return {
currentPage: page,
results,
...pagination
};
}
catch (error) {
throw new Error(`Failed to fetch ${category} games: ${error}`);
}
}
parseGameCell($, element) {
try {
const $game = $(element);
const title = $game.find('.game_title, .title').first().text().trim();
const gameLink = $game.find('.game_link, a').first().attr('href');
const url = gameLink ? utils_1.ParserUtils.toAbsoluteUrl(gameLink, this.baseUrl) : '';
if (!title || !url) {
return null;
}
const image = $game.find('.game_thumb img, img').first().attr('src');
const priceText = $game.find('.price, .game_price').first().text().trim();
const price = utils_1.ParserUtils.parsePrice(priceText);
const isFree = !price;
const platforms = utils_1.ParserUtils.parsePlatforms($, element);
const developer = $game.find('.game_author, .author').first().text().trim();
return {
id: utils_1.ParserUtils.extractIdFromUrl(url),
title,
url,
image,
price,
isFree,
platforms,
developer
};
}
catch (error) {
console.warn('Failed to parse game cell:', error);
return null;
}
}
parsePagination($) {
const paginationText = $('.pager_label').text();
const match = paginationText.match(/Page (\d+) of (\d+)/);
if (match) {
const currentPage = parseInt(match[1]);
const totalPages = parseInt(match[2]);
return {
hasNextPage: currentPage < totalPages,
totalPages
};
}
// Check for next page link
const hasNextPage = $('.next_page').length > 0;
return { hasNextPage };
}
extractDeveloper($) {
return $('.game_author a, .user_name').first().text().trim();
}
extractCoverImage($) {
return $('.game_thumb img, .header_image img, .cover_image img').first().attr('src');
}
extractPlatforms($) {
const platforms = [];
if ($('.icon-windows, .fa-windows').length > 0)
platforms.push('Windows');
if ($('.icon-apple, .fa-apple').length > 0)
platforms.push('macOS');
if ($('.icon-linux, .fa-linux').length > 0)
platforms.push('Linux');
if ($('.icon-android, .fa-android').length > 0)
platforms.push('Android');
if ($('.icon-html5, .fa-html5').length > 0)
platforms.push('Web');
return platforms;
}
extractGenres($) {
const genres = [];
$('.game_genre, .genre_tag, .classification_tag').each((_, element) => {
const genre = $(element).text().trim();
if (genre)
genres.push(genre);
});
return genres;
}
extractTags($) {
const tags = [];
$('.game_tag_link, .tag').each((_, element) => {
const tag = $(element).text().trim();
if (tag)
tags.push(tag);
});
return tags;
}
extractPricingInfo($) {
const priceElement = $('.buy_btn .price, .price').first();
const priceText = priceElement.text().trim();
let price = utils_1.ParserUtils.parsePrice(priceText);
let isFree = !price;
let isOnSale = false;
let originalPrice;
// Check for sale pricing
const originalPriceElement = $('.original_price');
if (originalPriceElement.length > 0) {
const originalPriceText = originalPriceElement.text().trim();
originalPrice = utils_1.ParserUtils.parsePrice(originalPriceText);
if (originalPrice) {
isOnSale = true;
}
}
return { price, isFree, isOnSale, originalPrice };
}
extractScreenshots($) {
const screenshots = [];
$('.screenshot img, .screenshot_list img').each((_, element) => {
const url = $(element).attr('src') || $(element).attr('data-src');
if (url) {
screenshots.push({ url });
}
});
return screenshots;
}
extractVideos($) {
const videos = [];
// YouTube embeds
$('iframe[src*="youtube.com"], iframe[src*="youtu.be"]').each((_, element) => {
const url = $(element).attr('src');
if (url) {
videos.push({
url,
type: 'trailer'
});
}
});
// Vimeo embeds
$('iframe[src*="vimeo.com"]').each((_, element) => {
const url = $(element).attr('src');
if (url) {
videos.push({
url,
type: 'trailer'
});
}
});
return videos;
}
extractRatingInfo($) {
const ratingText = $('.aggregate_rating, .rating_value').text();
const rating = ratingText ? parseFloat(ratingText) : undefined;
const ratingCountText = $('.rating_count').text();
const ratingCount = utils_1.ParserUtils.parseNumber(ratingCountText);
return { rating, ratingCount };
}
}
exports.ItchIO = ItchIO;
//# sourceMappingURL=itch.js.map