rynn-k / gists
xvideos.js javascript
const axios = require('axios');
const cheerio = require('cheerio');

class Xvideos {
    constructor(proxy = 'https://uncors.netlify.app/?destination=') {
        this.proxy = proxy;
    }
    
    search = async function (query, page = 1) {
        try {
            if (!query) throw new Error('Query is required.');
            if (isNaN(page)) throw new Error('Page must be a number.');
            
            const { data } = await axios.get(`${this.proxy}https://www.xvideos.com/?k=${query}&p=${page}`);
            const $ = cheerio.load(data);
            
            const results = [];
            $('div[id*="video_"]').each((_, el) => {
                const id = $(el).attr('data-id');
                const eid = $(el).attr('data-eid');
                const isChannel = $(el).attr('data-is-channel') === '1';
                
                const titleEl = $(el).find('.thumb-under p.title a');
                const title = titleEl.attr('title') || titleEl.clone().children('span').remove().end().text().trim();
                const url = $(el).find('.thumb-inside .thumb a').attr('href');
                const cover = $(el).find('.thumb-inside .thumb img').attr('data-src');
                const preview = $(el).find('.thumb-inside .thumb img').attr('data-pvv') || null;
                const resolution = $(el).find('.top-right-tags span').first().text().trim() || null;
                const duration = $(el).find('.thumb-under p.title a .duration').text().trim() || null;
                
                const metaBg = $(el).find('.thumb-under p.metadata .bg');
                const uploaderEl = metaBg.find('a').first();
                const uploaderName = uploaderEl.find('.name').text().trim() || null;
                const uploaderHref = uploaderEl.attr('href') || null;
                
                const metaText = metaBg.text();
                const viewsMatch = metaText.match(/([\d,.]+[kMBT]?)\s*Views/i);
                const views = viewsMatch ? viewsMatch[1].trim() : null;
                
                results.push({
                    id,
                    eid,
                    title,
                    uploader: {
                        name: uploaderName,
                        url: uploaderHref ? `https://www.xvideos.com${uploaderHref}` : null,
                        isChannel,
                    },
                    duration,
                    views,
                    resolution,
                    thumbnail: {
                        cover: cover || null,
                        preview: preview || null,
                    },
                    url: url ? `https://www.xvideos.com${url}` : null
                });
            });
            
            const totalResults = (() => {
                const txt = $('h2.page-title .sub').text().trim();
                const match = txt.match(/([\d,]+)/);
                return match ? parseInt(match[1].replace(/,/g, '')) : null;
            })();
            
            const lastPageEl = $('div.pagination ul li a.last-page').first();
            const totalPages = lastPageEl.length ? parseInt(lastPageEl.text().trim()) : null;
            if (totalPages && parseInt(page) > totalPages) throw new Error(`Page ${page} exceeds total pages (${totalPages}).`);
            
            return {
                page: parseInt(page),
                totalResults,
                totalPages,
                results,
            };
        } catch (error) {
            throw new Error(error.message);
        }
    }
    
    detail = async function (url) {
        try {
            if (!url.includes('www.xvideos.com')) throw new Error('Invalid url.');
            
            const { data } = await axios.get(`${this.proxy}${url}`);
            const $ = cheerio.load(data);
            
            let scriptContent = null;
            $('#video-player-bg script').each((_, el) => {
                const html = $(el).html() || '';
                if (html.includes('html5player.setThumbUrl')) {
                    scriptContent = html;
                    return false;
                }
            });
            const ex = (regex) => (scriptContent?.match(regex) || [])[1] || null;
            
            const confMatch = data.match(/window\.xv\.conf\s*=\s*(\{[\s\S]+?\});\s*<\/script>/);
            const conf = confMatch ? JSON.parse(confMatch[1]) : {};
            const videoData = conf?.data || {};
            const dynData = conf?.dyn || {};
            
            const title = dynData.video_title || $('meta[property="og:title"]').attr('content') || null;
            const tags = videoData.video_tags || [];
            
            const uploaderName = videoData.uploader || null;
            const uploaderUrl = videoData.uploader_url ? `https://www.xvideos.com${videoData.uploader_url}` : null;
            
            const h2Text = $('h2.page-title').text().trim();
            const durationRaw = $('h2.page-title .duration').text().trim() || null;
            const resolutionRaw = $('h2.page-title .video-hd-mark').text().trim() || null;
            const viewsRaw = $('#video-tabs #v-views strong').first().text().trim() || null;
            
            const ratingGoodPerc = $('#video-tabs .rating-good-perc').first().text().trim() || null;
            const ratingBadPerc = $('#video-tabs .rating-bad-perc').first().text().trim() || null;
            const votesGood = $('#video-tabs .rating-good-nbr').text().trim() || null;
            const votesBad = $('#video-tabs .rating-bad-nbr').text().trim() || null;
            const totalVotes = $('#video-tabs .rating-total-txt').text().trim() || null;
            
            const ldJsonRaw = $('script[type="application/ld+json"]').first().html();
            let uploadDateParsed = null;
            let durationISO = null;
            if (ldJsonRaw) {
                try {
                    const ld = JSON.parse(ldJsonRaw);
                    uploadDateParsed = ld.uploadDate || null;
                    durationISO = ld.duration || null;
                } catch (_) {}
            }
            
            return {
                id: String(videoData.id_video || dynData.id || ''),
                eid: videoData.encoded_id_video || null,
                title,
                uploader: {
                    name: uploaderName,
                    url: uploaderUrl,
                },
                duration: durationRaw || durationISO || null,
                views: viewsRaw || null,
                resolution: resolutionRaw || null,
                rating: {
                    good: ratingGoodPerc,
                    bad: ratingBadPerc,
                    totalVotes,
                },
                votes: {
                    good: votesGood,
                    bad: votesBad,
                },
                uploadDate: uploadDateParsed,
                tags,
                thumbnail: {
                    cover: ex(/html5player\.setThumbUrl\('(.*?)'\);/) || null,
                    cover169: ex(/html5player\.setThumbUrl169\('(.*?)'\);/) || null,
                    slide: ex(/html5player\.setThumbSlide\('(.*?)'\);/) || null,
                    slideBig: ex(/html5player\.setThumbSlideBig\('(.*?)'\);/) || null,
                },
                videos: (() => {
                    const low = ex(/html5player\.setVideoUrlLow\('(.*?)'\);/);
                    const high = ex(/html5player\.setVideoUrlHigh\('(.*?)'\);/);
                    const HLS = ex(/html5player\.setVideoHLS\('(.*?)'\);/);
                    const getRes = u => u?.match(/video_(\d+p)/)?.[1] || null;
                    return {
                        [`${getRes(low) || 'low'}`]: low || null,
                        [`${getRes(high) || 'high'}`]: high || null,
                        HLS: HLS || null,
                    };
                })()
            };
        } catch (error) {
            throw new Error(error.message);
        }
    }
};

// Usage:
const x = new Xvideos();
x.search('girl').then(console.log);
8085 bytes ยท Updated Mar 5, 2026