'use strict'; const Promise = require('bluebird'); const bhttp = require('bhttp'); const cheerio = require('cheerio'); const { JSDOM } = require('jsdom'); const moment = require('moment'); const defaultTags = { hardx: [], darkx: ['interracial'], eroticax: [], lesbianx: ['lesbian'], allblackx: ['ebony', 'bbc'], }; async function fetchPhotos(url) { const res = await bhttp.get(url); return res.body.toString(); } function scrapePhotos(html) { const $ = cheerio.load(html, { normalizeWhitespace: true }); return $('.preview .imgLink').toArray().map((linkEl) => { const url = $(linkEl).attr('href'); if (url.match('/join')) { // URL links to join page instead of full photo, extract thumbnail const src = $(linkEl).find('img').attr('src'); if (src.match('previews/')) { // resource often serves full photo at a modifier URL anyway, add as primary source const highRes = src .replace('previews/', '') .replace('_tb.jpg', '.jpg'); // keep original thumbnail as fallback in case full photo is not available return [highRes, src]; } return src; } // URL links to full photo return url; }); } async function getPhotos(albumPath, siteDomain) { const albumUrl = `https://${siteDomain}${albumPath}`; try { const html = await fetchPhotos(albumUrl); const $ = cheerio.load(html, { normalizeWhitespace: true }); const photos = scrapePhotos(html); const pages = $('.paginatorPages a').map((pageIndex, pageElement) => $(pageElement).attr('href')).toArray(); const otherPhotos = await Promise.map(pages, async (page) => { const pageUrl = `https://${siteDomain}${page}`; const pageHtml = await fetchPhotos(pageUrl); return scrapePhotos(pageHtml); }, { concurrency: 2, }); return photos.concat(otherPhotos.flat()); } catch (error) { console.error(`Failed to fetch XEmpire photos from ${albumPath}: ${error.message}`); return []; } } function scrape(html, site) { const $ = cheerio.load(html, { normalizeWhitespace: true }); const scenesElements = $('li[data-itemtype=scene]').toArray(); return scenesElements.map((element) => { const sceneLinkElement = $(element).find('.sceneTitle a'); const url = `${site.url}${sceneLinkElement.attr('href')}`; const title = sceneLinkElement.attr('title'); const entryId = $(element).attr('data-itemid'); const date = moment .utc($(element).find('.sceneDate').text(), 'MM-DD-YYYY') .toDate(); const actors = $(element).find('.sceneActors a') .map((actorIndex, actorElement) => $(actorElement).attr('title')) .toArray(); const [likes, dislikes] = $(element).find('.value') .toArray() .map(value => Number($(value).text())); const poster = $(element).find('.imgLink img').attr('data-original'); const trailer = `https://videothumb.gammacdn.com/307x224/${entryId}.mp4`; return { url, entryId, title, actors, director: 'Mason', date, poster, trailer: { src: trailer, quality: 224, }, rating: { likes, dislikes, }, site, }; }); } async function scrapeScene(html, url, site) { const $ = cheerio.load(html, { normalizeWhitespace: true }); const json = $('script[type="application/ld+json"]').html(); const json2 = $('script:contains("dataLayer = ")').html(); const videoJson = $('script:contains("window.ScenePlayerOptions")').html(); const data = JSON.parse(json)[0]; const data2 = JSON.parse(json2.slice(json2.indexOf('[{'), -1))[0]; const videoData = JSON.parse(videoJson.slice(videoJson.indexOf('{"id":'), videoJson.indexOf('};') + 1)); const entryId = data2.sceneDetails.sceneId || new URL(url).pathname.split('/').slice(-1)[0]; const title = data2.sceneDetails.sceneTitle || $('meta[name="twitter:title"]').attr('content'); const description = data2.sceneDetails.sceneDescription || data.description || $('meta[name="twitter:description"]').attr('content'); // date in data object is not the release date of the scene, but the date the entry was added const date = moment.utc($('.updatedDate').first().text(), 'MM-DD-YYYY').toDate(); const actors = (data2.sceneDetails.sceneActors || data.actor).map(actor => actor.actorName || actor.name); const stars = (data.aggregateRating.ratingValue / data.aggregateRating.bestRating) * 5; const duration = moment.duration(data.duration.slice(2).split(':')).asSeconds(); const siteDomain = $('meta[name="twitter:domain"]').attr('content') || 'allblackx.com'; // only AllBlackX has no twitter domain, no other useful hints available const siteSlug = siteDomain && siteDomain.split('.')[0].toLowerCase(); const siteUrl = siteDomain && `https://www.${siteDomain}`; const poster = videoData.picPreview; const trailer = `${videoData.playerOptions.host}${videoData.url}`; const photos = await getPhotos($('.picturesItem a').attr('href'), siteDomain, site); const rawTags = data.keywords.split(', '); const tags = [...defaultTags[siteSlug], ...rawTags]; return { url: `${siteUrl}/en/video/${new URL(url).pathname.split('/').slice(-2).join('/')}`, entryId, title, date, actors, director: 'Mason', description, duration, poster, photos, trailer: { src: trailer, quality: parseInt(videoData.sizeOnLoad, 10), }, tags, rating: { stars, }, site, channel: siteSlug, }; } function scrapeActorSearch(html, url, actorName) { const { document } = new JSDOM(html).window; const actorLink = document.querySelector(`a[title="${actorName}" i]`); return actorLink ? actorLink.href : null; } function scrapeProfile(html, url, actorName) { const { document } = new JSDOM(html).window; const avatarEl = document.querySelector('img.actorPicture'); const descriptionEl = document.querySelector('.actorBio p:not(.bioTitle)'); const profile = { name: actorName, }; if (avatarEl) profile.avatar = avatarEl.src; if (descriptionEl) profile.description = descriptionEl.textContent.trim(); profile.releases = Array.from(document.querySelectorAll('.sceneList .scene a.imgLink'), el => `https://xempire.com${el.href}`); return profile; } async function fetchLatest(site, page = 1) { const res = await bhttp.get(`${site.url}/en/videos/AllCategories/0/${page}`); return scrape(res.body.toString(), site); } async function fetchUpcoming(site) { const res = await bhttp.get(`${site.url}/en/videos/AllCategories/0/1/upcoming`); return scrape(res.body.toString(), site); } async function fetchScene(url, site) { const res = await bhttp.get(url); return scrapeScene(res.body.toString(), url, site); } async function fetchProfile(actorName) { const actorSlug = actorName.toLowerCase().replace(/\s+/, '+'); const searchUrl = `https://www.xempire.com/en/search/xempire/actor/${actorSlug}`; const searchRes = await bhttp.get(searchUrl); if (searchRes.statusCode !== 200) { return null; } const actorUrl = scrapeActorSearch(searchRes.body.toString(), searchUrl, actorName); if (actorUrl) { const url = `https://xempire.com${actorUrl}`; const actorRes = await bhttp.get(url); if (actorRes.statusCode !== 200) { return null; } return scrapeProfile(actorRes.body.toString(), url, actorName); } return null; } module.exports = { fetchLatest, fetchProfile, fetchUpcoming, fetchScene, };