UNPKG

chimi-scraper

Version:

A TypeScript library for scraping game data from itch.io with a clean, scalable architecture

296 lines 11.1 kB
"use strict"; 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