itch.js

Search games and get details.

#games#search#utility
30
8 Des 2025, 10:07
RawEdit
javascript0 lines
/***
  @ Base: https://itch.io/
  @ Author: Shannz
  @ Note: Search game and get detail
***/

import axios from 'axios';
import * as cheerio from 'cheerio';
import { wrapper } from 'axios-cookiejar-support';
import { CookieJar } from 'tough-cookie';

const BASE_URL = 'https://itch.io';

const jar = new CookieJar();
const client = wrapper(axios.create({
    jar,
    withCredentials: true,
    headers: {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
        'Accept-Language': 'en-US,en;q=0.9',
        'Connection': 'keep-alive',
        'Upgrade-Insecure-Requests': '1',
        'Sec-Fetch-Dest': 'document',
        'Sec-Fetch-Mode': 'navigate',
        'Sec-Fetch-Site': 'same-origin',
        'Sec-Fetch-User': '?1'
    }
}));

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

const parseGameList = ($) => {
    const games = [];
    $('.game_cell').each((i, el) => {
        const item = $(el);
        const title = item.find('.game_title a').text().trim();
        const link = item.find('.game_title a').attr('href');
        const desc = item.find('.game_text').text().trim();
        const author = item.find('.game_author a').text().trim();
        
        const thumbEl = item.find('.game_thumb img');
        const thumbnail = thumbEl.attr('data-lazy_src') || thumbEl.attr('src');
        
        let price = "Free/NYP";
        if (item.find('.price_value').length > 0) price = item.find('.price_value').text().trim();
        else if (item.find('.sale_tag').length > 0) price = "On Sale";

        games.push({ title, price, author, description: desc, link, thumbnail });
    });
    return games;
};

export const itch = {
    home: async () => {
        try {
            const { data } = await client.get(`${BASE_URL}/games`, {
                headers: { 'Referer': 'https://itch.io/' }
            });
            const $ = cheerio.load(data);

            const games = parseGameList($);

            return { games };

        } catch (error) {
            console.error('❌ Error Home:', error.message);
            return null;
        }
    },

    detail: async (gameUrl) => {
        if (!gameUrl) return null;
        try {
            const { data } = await client.get(gameUrl, {
                headers: { 'Referer': `${BASE_URL}/games` }
            });
            const $ = cheerio.load(data);

            const title = $('h1.game_title').text().trim() || $('meta[property="og:title"]').attr('content');
            const short_desc = $('meta[name="description"]').attr('content');
            const cover_image = $('meta[property="og:image"]').attr('content');

            let price_info = "Free";
            const buyBtn = $('.buy_btn');
            if (buyBtn.length > 0) {
                price_info = buyBtn.text().trim();
            }

            const meta_info = {};
            $('.game_info_panel_widget table tr').each((i, el) => {
                const key = $(el).find('td:nth-child(1)').text().trim();
                const val = $(el).find('td:nth-child(2)').text().trim();
                if (key) meta_info[key] = val;
            });

            const available_files = [];
            $('.upload_list_widget .upload').each((i, el) => {
                const row = $(el);
                const fileName = row.find('.upload_name strong').text().trim();
                const fileSize = row.find('.file_size span').text().trim() || "Unknown size";
                const version = row.find('.upload_name .version_name').text().trim();

                const platforms = [];
                row.find('.upload_platforms span, .upload_traits span').each((_, icon) => {
                    const titleAttr = $(icon).attr('title');
                    if (titleAttr) {
                        platforms.push(titleAttr.replace('Download for ', '').replace('Play in ', ''));
                    }
                });

                let downloadUrl = "Locked/Paid Only";
                const lightboxBtn = row.find('.download_btn'); 
                const uploadId = row.attr('data-upload_id'); 

                if (lightboxBtn.length > 0) {
                    const cleanBaseUrl = gameUrl.replace(/\/$/, "");
                    if (uploadId) {
                        downloadUrl = `${cleanBaseUrl}/file/${uploadId}`;
                    }
                } else if (row.find('.buy_btn').length > 0) {
                    downloadUrl = "Must Purchase First";
                }

                available_files.push({
                    id: uploadId || 'unknown',
                    file_name: fileName,
                    version: version || null,
                    size: fileSize,
                    platforms: platforms.length > 0 ? platforms : ['Universal/Unknown'],
                    download_page: downloadUrl 
                });
            });

            const screenshots = [];
            $('.screenshot_list a').each((i, el) => {
                const shot = $(el).attr('href');
                if (shot) screenshots.push(shot);
            });

            const links = [];
            $('.right_col .links a').each((i, el) => {
                links.push({
                    text: $(el).text(),
                    url: $(el).attr('href')
                });
            });

            return {
                title,
                price_info,
                short_desc,
                cover_image,
                meta_info,
                links,
                screenshots,
                file_count: available_files.length,
                available_files
            };

        } catch (error) {
            console.error(`❌ Gagal Detail: ${error.message}`);
            return null;
        }
    }
};