'use strict'; const Promise = require('bluebird'); const util = require('util'); const { JSDOM } = require('jsdom'); const moment = require('moment'); const format = require('template-format'); const logger = require('../logger')(__filename); const qu = require('../utils/qu'); const http = require('../utils/http'); const slugify = require('../utils/slugify'); function getApiUrl(appId, apiKey) { const userAgent = 'Algolia for vanilla JavaScript (lite) 3.27.0;instantsearch.js 2.7.4;JS Helper 2.26.0'; const apiUrl = `https://${appId.toLowerCase()}-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=${userAgent}&x-algolia-application-id=${appId}&x-algolia-api-key=${apiKey}`; return { appId, apiKey, userAgent, apiUrl, }; } function getAvatarFallbacks(avatar) { if (!avatar) { return null; } return [ avatar.replace(/\d+x\d+/, '500x750'), avatar.replace(/\d+x\d+/, '240x360'), avatar.replace(/\d+x\d+/, '200x300'), avatar, ]; } async function fetchApiCredentials(referer, site) { if (site?.parameters?.appId && site?.parameters?.apiKey) { return getApiUrl(site.parameters.appId, site.parameters.apiKey); } const res = await http.get(referer); const body = res.body.toString(); const apiLine = body.split('\n').find((bodyLine) => bodyLine.match('apiKey')); if (!apiLine) { throw new Error(`No Gamma API key found for ${referer}`); } const apiSerial = apiLine.slice(apiLine.indexOf('{'), apiLine.indexOf('};') + 1); const apiData = JSON.parse(apiSerial); const { applicationID: appId, apiKey } = apiData.api.algolia; return getApiUrl(appId, apiKey); } function getAlbumUrl(albumPath, site) { if (site.parameters?.photos) { return /^http/.test(site.parameters.photos) ? `${site.parameters.photos}/${albumPath.split('/').slice(-2).join('/')}` : `${site.url}${site.parameters.photos}/${albumPath.split('/').slice(-2).join('/')}`; } if (site.url && site.parameters?.photos !== false) { return `${site.url}${albumPath}`; } return null; } async function fetchPhotos(url) { const res = await qu.get(url); return res.item; } function scrapePhotos({ query }, includeThumbnails = true) { return query.all('.preview .imgLink, .pgFooterThumb a').map((linkEl) => { const url = linkEl.href; if (/\/join|\/createaccount/.test(url)) { // URL links to join page instead of full photo, extract thumbnail // /createaccount is used by e.g. Tricky Spa native site const src = query.img(linkEl); if (/previews\//.test(src)) { // 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]; } if (!includeThumbnails) return null; return src; } // URL links to full photo return url; }).filter(Boolean); } async function getPhotos(albumPath, site, includeThumbnails = true) { const albumUrl = getAlbumUrl(albumPath, site); if (!albumUrl) { return []; } try { const item = await fetchPhotos(albumUrl); const photos = scrapePhotos(item, includeThumbnails); const lastPage = item.query.url('.Gamma_Paginator a.last')?.match(/\d+$/)[0]; if (lastPage) { const otherPages = Array.from({ length: Number(lastPage) }, (_value, index) => index + 1).slice(1); const otherPhotos = await Promise.map(otherPages, async (page) => { const pageItem = await fetchPhotos(`${albumUrl}/${page}`); return scrapePhotos(pageItem, includeThumbnails); }, { concurrency: 2, }); return photos.concat(otherPhotos.flat()); } return photos; } catch (error) { logger.warn(`Failed to fetch ${site.name} photos from ${albumUrl}: ${error.message}`); return []; } } async function getFullPhotos(entryId, site) { const res = await http.get(`${site.url}/media/signPhotoset/${entryId}`, { headers: { 'X-Requested-With': 'XMLHttpRequest', }, }); if (res.ok && typeof res.body === 'object') { // gives 200 OK even when redirected to signup page return Object.values(res.body); } return []; } async function getThumbs(entryId, site, parameters) { const referer = parameters?.referer || `${parameters?.networkReferer ? site.parent.url : site.url}/en/videos`; const { apiUrl } = await fetchApiCredentials(referer, site); const res = await http.post(apiUrl, { requests: [ { indexName: 'all_photosets', params: `query=&page=0&facets=[]&tagFilters=&facetFilters=[["set_id:${entryId}"]]`, }, ], }, { headers: { Referer: referer, }, }, { encodeJSON: true, }); if (res.ok && res.body.results?.[0]?.hits[0]?.set_pictures) { return res.body.results[0].hits[0].set_pictures.map((img) => img.thumb_path && ([ `https://images-fame.gammacdn.com/photo_set${img.thumb_path}`, `https://images01-fame.gammacdn.com/photo_set${img.thumb_path}`, `https://images02-fame.gammacdn.com/photo_set${img.thumb_path}`, `https://images03-fame.gammacdn.com/photo_set${img.thumb_path}`, `https://images04-fame.gammacdn.com/photo_set${img.thumb_path}`, `https://images-evilangel.gammacdn.com/photo_set${img.thumb_path}`, `https://transform.gammacdn.com/photo_set${img.thumb_path}`, ])).filter(Boolean); } return []; } async function getPhotosApi(entryId, site, parameters) { const [photos, thumbs] = await Promise.all([ getFullPhotos(entryId, site, parameters), getThumbs(entryId, site, parameters), ]); return photos.concat(thumbs.slice(photos.length)); } function getImageSources(source) { return [ `https://images-fame.gammacdn.com/movies${source}`, `https://images01-fame.gammacdn.com/movies${source}`, `https://images02-fame.gammacdn.com/movies${source}`, `https://images03-fame.gammacdn.com/movies${source}`, `https://images04-fame.gammacdn.com/movies${source}`, `https://images-evilangel.gammacdn.com/movies${source}`, `https://transform.gammacdn.com/movies${source}`, ]; } function curateTitle(title, channel) { // some videos are redundantly prefixed with the name of the site, i.e. Bubblegum Dungeon, Forbidden Seductions and Lady Gonzo return title.replace(new RegExp(`^\\s*${channel.name}\\s*[:|-]\\s`, 'i'), ''); } async function scrapeApiReleases(json, site) { return json.map((scene) => { if (site.parameters?.extract && scene.sitename !== site.parameters.extract) { return null; } if (site.parameters?.filterExclusive && scene.availableOnSite.length > 1) { return null; } const release = { entryId: scene.clip_id, description: scene.description, duration: scene.length, likes: scene.ratings_up, dislikes: scene.ratings_down, }; release.title = curateTitle(scene.title, site); release.path = `/${scene.url_title}/${release.entryId}`; if (site.parameters?.scene) release.url = `${site.parameters.scene}${release.path}`; else if (site.url && site.parameters?.scene !== false) release.url = `${site.url}/en/video${release.path}`; release.date = moment.utc(scene.release_date, 'YYYY-MM-DD').toDate(); release.director = scene.directors[0]?.name || null; release.actors = scene.actors.map((actor) => ({ entryId: actor.actor_id, name: actor.name, gender: actor.gender, avatar: [ `https://images03-openlife.gammacdn.com/actors/${actor.actor_id}/${actor.actor_id}_500x750.jpg`, `https://images03-openlife.gammacdn.com/actors/${actor.actor_id}/${actor.actor_id}_240x360.jpg`, `https://images03-openlife.gammacdn.com/actors/${actor.actor_id}/${actor.actor_id}_200x300.jpg`, ], })); /* master categories include e.g. 'transgender' for non-trans Wicked scenes release.tags = scene.master_categories .concat(scene.categories?.map((category) => category.name)) .filter(Boolean); // some categories don't have a name */ release.tags = scene.categories?.map((category) => category.name).filter(Boolean); // some categories don't have a name if (scene.availableOnSite.length > 1) { release.comment = `Also available on ${scene.availableOnSite.filter((sisterSite) => sisterSite !== site.slug).join(', ')}`; } const posterPath = scene.pictures.resized || (scene.pictures.nsfw?.top && Object.values(scene.pictures.nsfw.top)[0]); if (posterPath) { release.poster = [ `https://images-evilangel.gammacdn.com/movies${posterPath}`, `https://transform.gammacdn.com/movies${posterPath}`, ]; } // release.movie = `${site.url}/en/movie/${scene.url_movie_title}/${scene.movie_id}`; return release; }).filter(Boolean); } function scrapeAll(scenes, site, networkUrl, hasTeaser = true) { return scenes.map(({ query, el }) => { const release = {}; release.url = query.url('.sceneTitle a, .tlcTitle a', 'href', { origin: networkUrl ? site.parent.url : site.url }); release.title = query.cnt('.sceneTitle a', 'tlcTitle a', 'title'); release.entryId = el.dataset.itemid; release.date = query.date('.sceneDate, .tlcSpecsDate .tlcDetailsValue', ['MM-DD-YYYY', 'YYYY-MM-DD']); release.actors = query.cnts('.sceneActors a, .tlcActors a', ' title'); [release.likes, release.dislikes] = query.all('.value').map((likeEl) => query.number(likeEl)); release.poster = query.img('.imgLink img, .tlcImageItem', 'data-original') || query.img('.imgLink img, .tlcImageItem'); if (hasTeaser) { release.teaser = [ { src: `https://videothumb.gammacdn.com/600x339/${release.entryId}.mp4` }, { src: `https://videothumb.gammacdn.com/307x224/${release.entryId}.mp4` }, ]; } release.channel = query.el('.fromSite a', 'title')?.replace('.com', ''); return release; }); } async function scrapeScene({ query }, url, channel, baseRelease, mobileItem, options) { const release = { query }; // used by XEmpire scraper to resolve channel-specific details const json = query.html('script[type="application/ld+json"]'); const videoJson = query.htmls('script').find((script) => /ScenePlayerOptions/i.test(script)); const [data, data2] = json ? JSON.parse(json) : []; const videoData = videoJson && JSON.parse(videoJson.slice(videoJson.indexOf('{'), videoJson.indexOf('};') + 1)); release.entryId = (baseRelease?.path || new URL(url).pathname).match(/\/(\d{2,})(\/|$)/)?.[1]; release.title = videoData?.playerOptions?.sceneInfos.sceneTitle || data?.name; release.description = data?.description; release.date = query.date('.updatedDate', ['MM-DD-YYYY', 'YYYY-MM-DD']) || qu.extractDate(data?.dateCreated, 'YYYY-MM-DD') || videoData?.playerOptions?.sceneInfos.sceneReleaseDate; release.actors = (data?.actor || data2?.actor)?.map((actor) => ({ name: actor.name, gender: actor.gender, })) || []; release.duration = qu.durationToSeconds(data.duration); release.director = data?.director?.[0]?.name || data2?.director?.[0]?.name; release.tags = data?.keywords?.split(', ') || data2?.keywords?.split(', ') || []; release.stars = (data.aggregateRating.ratingValue / data.aggregateRating.bestRating) * 5 || null; release.channel = slugify(data?.productionCompany?.name || query.el('.studioLink a, .siteLink a', 'title') || query.cnt('.siteNameSpan')?.toLowerCase().replace('.com', '') || query.meta('meta[name="twitter:domain"]')?.replace('.com', ''), ''); if (videoData?.picPreview && new URL(videoData.picPreview).pathname.length > 1) { // sometimes links to just https://images02-fame.gammacdn.com/ const poster = new URL(videoData.picPreview); release.poster = [ videoData.picPreview, // prefer original URL with width and height parameters, without may give a square crop on e.g. XEmpire `${poster.origin}${poster.pathname}`, ]; } const photoLink = query.url('.picturesItem a'); const mobilePhotos = mobileItem?.query.imgs('.preview-displayer a img') || []; if (photoLink && options.includePhotos) { const photos = await getPhotos(photoLink, channel, mobilePhotos.length < 3); // only get thumbnails when less than 3 mobile photos are available if (photos.length < 7) { release.photos = [...photos, ...mobilePhotos]; // probably only teaser photos available, supplement with mobile album } else { release.photos = photos; } } else { release.photos = mobilePhotos; } const trailer = videoData && `${videoData.playerOptions.host}${videoData.url}`; if (trailer) { release.trailer = [ { src: trailer.replace('hd', 'sm'), quality: 240, }, { src: trailer.replace('hd', 'med'), quality: 360, }, { src: trailer.replace('hd', 'big'), quality: 480, }, { // probably 540p src: trailer, quality: parseInt(videoData.sizeOnLoad, 10), }, { src: trailer.replace('hd', '720p'), quality: 720, }, { src: trailer.replace('hd', '1080p'), quality: 1080, }, { src: trailer.replace('hd', '4k'), quality: 2160, }, ]; } const movieUrl = query.url('.dvdLink', 'href', { origin: channel.url }); if (movieUrl) { release.movie = { url: movieUrl, title: query.el('.dvdLink', 'title'), entryId: movieUrl.match(/\/(\d+)(\/|$)/)?.[1], covers: [qu.imgs('.dvdLink img')], }; } return release; } async function scrapeReleaseApi(data, site, options, movieScenes) { const release = {}; release.entryId = data.clip_id || data.movie_id; release.title = curateTitle(data.title, site); release.duration = data.length; release.date = (data.date && new Date(data.date * 1000)) || qu.parseDate(data.release_date || data.last_modified, 'YYYY-MM-DD'); release.director = data.directors[0]?.name || null; release.actors = data.actors.map((actor) => ({ entryId: actor.actor_id, name: actor.name, gender: actor.gender, url: options.parameters?.actors ? format(options.parameters.actors, { id: actor.actor_id, slug: actor.url_name }) : qu.prefixUrl(`/en/pornstar/${actor.url_name}/${data.actor_id}`, site.url), })); release.tags = data.categories.map((category) => category.name); if (data.pictures) { release.poster = [ ...(data.pictures['1920x1080'] ? getImageSources(data.pictures['1920x1080']) : []), ...(data.pictures.resized ? getImageSources(data.pictures.resized) : []), ...(data.pictures['960x544'] ? getImageSources(data.pictures['960x544']) : []), ]; } if (data.photoset_id && options.includePhotos && options.parameters?.includePhotos !== false) { release.photos = await getPhotosApi(data.photoset_id, site, options.parameters); } if (data.cover_path) { release.covers = [ getImageSources(`${data.cover_path}_front_400x625.jpg?width=450&height=636&format=webp`), getImageSources(`${data.cover_path}_back_400x625.jpg?width=450&height=636&format=webp`), ]; } release.teaser = `https://videothumb.gammacdn.com/500x281/${release.entryId}.mp4`; if (data.trailers) { release.trailer = Object.entries(data.trailers).map(([quality, source]) => ({ src: source, quality })); } if (data.movie_id && !data.movie_path && options.parameters.sceneMovies !== false) { release.movie = { entryId: data.movie_id, title: data.movie_title, url: qu.prefixUrl(`${data.url_movie_title}/${data.movie_id}`, options.parameters.movie ? options.parameters.movie : `${site.url}/en/movie`), }; } if (movieScenes?.length > 0) { release.scenes = await Promise.all(movieScenes.map((movieScene) => scrapeReleaseApi(movieScene, site, options))); } release.channel = data.sitename; release.qualities = data.download_sizes; return release; } async function fetchMovieTrailer(release) { if (!release.entryId) { return null; } const url = `https://www.evilangel.com/en/dvdtrailer/${release.entryId}`; const res = await qu.get(url); if (!res.ok) { return null; } const trailerHost = res.html.match(/"host":\s*"(.*\.com)"/)?.[1].replace(/\\\//g, '/'); const trailerPath = res.html.match(/"url":\s*"(.*\.mp4)"/)?.[1].replace(/\\\//g, '/'); if (trailerHost && trailerPath) { return qu.prefixUrl(trailerPath, trailerHost); } return null; } async function scrapeMovie({ query, el }, url, entity, baseRelease, options) { const release = {}; const { dataLayer } = query.exec('//script[contains(text(), "dataLayer")]', ['dataLayer']); const rawData = dataLayer?.[0]?.dvdDetails; const data = rawData?.dvdId && rawData; // dvdDetails is mostly empty in some cache states if (query.exists('.NotFound-Title')) { return null; } release.entryId = new URL(url).pathname.match(/\/(\d+)(\/|$)/)?.[1]; release.covers = [ query.img('.frontCoverImg', 'href'), query.img('.backCoverImg', 'href'), ]; release.description = query.cnt('.descriptionText'); release.date = qu.extractDate(data?.dvdReleaseDate) || query.date('.updatedOn', 'YYYY-MM-DD'); release.title = data?.dvdName || query.cnt('.dvdTitle'); release.director = query.el('.directedBy a', 'title'); release.actors = data?.dvdActors.map((actor) => ({ name: actor.actorName, entryId: actor.actorId })) || query.all('.actorCarousel a[href*="/pornstar"]').map((actorEl) => ({ entryId: query.url(actorEl, null).match(/\/(\d+)/)?.[1], name: query.cnt(actorEl, 'span'), href: query.url(actorEl, null, 'href', { origin: entity.url }), avatar: getAvatarFallbacks(query.img(actorEl)), })); release.tags = query.cnts('.dvdCol a'); release.scenes = scrapeAll(qu.initAll(el, 'div[data-itemtype*=scene], li[data-itemtype*=scene]'), entity, entity.url); if (options.includeTrailers) { release.trailer = await fetchMovieTrailer(release); } return release; } function scrapeActorSearch(html, url, actorName) { const { document } = new JSDOM(html).window; const actorLink = document.querySelector(`a[title="${actorName}" i]`); return actorLink ? actorLink.href : null; } async function fetchActorReleases(profileUrl, getActorReleasesUrl, page = 1, accReleases = [], context) { const { origin, pathname } = new URL(profileUrl); const profilePath = `/${pathname.split('/').slice(-2).join('/')}`; const url = (context.parameters.actorScenes && format(context.parameters.actorScenes, { path: profilePath, page })) || getActorReleasesUrl?.(profilePath, page); if (!url) { return []; } const res = await qu.get(url); if (!res.ok) { return []; } const releases = scrapeAll(res.item.html, null, origin); const nextPage = res.item.query.url('.Gamma_Paginator a.next'); if (nextPage) { return fetchActorReleases(profileUrl, getActorReleasesUrl, page + 1, accReleases.concat(releases), context); } return accReleases.concat(releases); } async function scrapeProfile({ query }, url, actorName, _siteSlug, getActorReleasesUrl, withReleases, context) { const avatar = query.img('img.actorPicture'); const hair = query.cnt('.actorProfile .attribute_hair_color'); const height = query.cnt('.actorProfile .attribute_height'); const weight = query.cnt('.actorProfile .attribute_weight'); const alias = query.cnt('.actorProfile .attribute_alternate_names'); const nationality = query.cnt('.actorProfile .attribute_home'); const profile = { name: actorName, }; if (avatar) { // larger sizes usually available, provide fallbacks const avatars = getAvatarFallbacks(avatar); profile.avatar = avatars; } profile.description = query.cnt('.actorBio p:not(.bioTitle)'); if (hair) profile.hair = hair.split(':')[1].trim(); if (height) profile.height = Number(height.match(/\d+/)[0]); if (weight) profile.weight = Number(weight.match(/\d+/)[0]); if (alias) profile.aliases = alias.split(':')[1].trim().split(', '); if (nationality) profile.nationality = nationality.split(':')[1].trim(); if ((getActorReleasesUrl || context.parameters.actorScenes) && withReleases) { profile.releases = await fetchActorReleases(url, getActorReleasesUrl, 1, [], context); } return profile; } function scrapeApiProfile(data, releases, siteSlug) { const profile = {}; if (data.male === 1) profile.gender = 'male'; if (data.female === 1) profile.gender = 'female'; if (data.shemale === 1 || data.trans === 1) profile.gender = 'transsexual'; if (data.description) profile.description = data.description.trim(); if (data.attributes.ethnicity) profile.ethnicity = data.attributes.ethnicity; if (data.attributes.eye_color) profile.eyes = data.attributes.eye_color; if (data.attributes.hair_color) profile.hair = data.attributes.hair_color; const avatarPaths = Object.values(data.pictures).reverse(); if (avatarPaths.length > 0) profile.avatar = avatarPaths.map((avatarPath) => `https://images01-evilangel.gammacdn.com/actors${avatarPath}`); if (releases) profile.releases = releases.map((release) => `https://${siteSlug}.com/en/video/${release.url_title}/${release.clip_id}`); return profile; } async function fetchLatestApi(site, page = 1, options, preData, upcoming = false) { const referer = options.parameters?.referer || `${options.parameters?.networkReferer ? site.parent.url : site.url}/en/videos`; const { apiUrl } = await fetchApiCredentials(referer, site); const res = await http.post(apiUrl, { requests: [ { indexName: 'all_scenes', params: `query=&hitsPerPage=36&maxValuesPerFacet=100&page=${page - 1}&facetFilters=[["lesbian:"],["bisex:"],["shemale:"],["upcoming:${upcoming ? 1 : 0}"]]&filters=sitename:${site.slug}`, // OR channels.id:${site.slug}`, }, ], }, { headers: { Referer: referer, }, }, { encodeJSON: true, }); if (res.status === 200 && res.body.results?.[0]?.hits) { return scrapeApiReleases(res.body.results[0].hits, site); } return res.status; } async function fetchUpcomingApi(site, page = 1, options, preData) { return fetchLatestApi(site, page, options, preData, true); } async function fetchSceneApi(url, site, baseRelease, options) { const referer = options.parameters?.referer || `${site.parameters?.networkReferer ? site.parent.url : site.url}/en/videos`; const { apiUrl } = await fetchApiCredentials(referer, site); const entryId = (baseRelease?.path || new URL(url).pathname).match(/\/(\d{2,})(\/|$)/)?.[1]; const res = await http.post(apiUrl, { requests: [ { indexName: 'all_scenes', params: `query=&page=0&facets=[]&tagFilters=&facetFilters=[["clip_id:${entryId}"]]`, }, { indexName: 'all_scenes', params: 'query=&page=0&hitsPerPage=1&attributesToRetrieve=[]&attributesToHighlight=[]&attributesToSnippet=[]&tagFilters=&analytics=false&clickAnalytics=false&facets=clip_id', }, ], }, { headers: { Referer: referer, }, }, { encodeJSON: true, }); if (res.status === 200 && res.body.results?.[0]?.hits.length > 0) { return scrapeReleaseApi(res.body.results[0].hits[0], site, options); } if (res.status === 200) { return null; } return res.status; } async function fetchMovieApi(url, site, baseRelease, options) { const referer = options.parameters?.referer || `${site.parameters?.networkReferer ? site.parent.url : site.url}/en/movies`; const { apiUrl } = await fetchApiCredentials(referer, site); const entryId = (baseRelease?.path || new URL(url).pathname).match(/\/(\d{2,})(\/|$)/)?.[1]; const res = await http.post(apiUrl, { requests: [ { indexName: 'all_movies', params: `query=&page=0&facets=[]&tagFilters=&facetFilters=[["movie_id:${entryId}"]]`, }, { indexName: 'all_scenes_latest_desc', params: `query=&page=0&facets=[]&tagFilters=&facetFilters=[["movie_id:${entryId}"]]`, }, /* { indexName: 'all_movies', params: 'query=&page=0&hitsPerPage=1&attributesToRetrieve=[]&attributesToHighlight=[]&attributesToSnippet=[]&tagFilters=&analytics=false&clickAnalytics=false&facets=clip_id', }, */ ], }, { headers: { Referer: referer, }, }, { encodeJSON: true, }); if (res.status === 200 && res.body.results?.[0]?.hits.length > 0) { return scrapeReleaseApi(res.body.results[0].hits[0], site, options, res.body.results[1]?.hits); } if (res.status === 200) { return null; } return res.status; } function getLatestUrl(site, page) { if (site.parameters?.latest) { if (/^http/.test(site.parameters.latest)) { return /%d/.test(site.parameters.latest) ? util.format(site.parameters.latest, page) : `${site.parameters.latest}${page}`; } return /%d/.test(site.parameters.latest) ? util.format(`${site.url}${site.parameters.latest}`, page) : `${site.url}${site.parameters.latest}${page}`; } return `${site.url}/en/videos/AllCategories/0/${page}`; } function getUpcomingUrl(site) { if (site.parameters?.upcoming) { return /^http/.test(site.parameters.upcoming) ? `${site.parameters.upcoming}` : `${site.url}${site.parameters.upcoming}`; } return `${site.url}/en/videos/AllCategories/0/1/upcoming`; } async function fetchLatest(site, page = 1) { const url = getLatestUrl(site, page); const res = await qu.getAll(url, 'li[data-itemtype=scene], div[data-itemtype*=scene]'); if (res.ok) { return scrapeAll(res.items, site); } return res.status; } async function fetchUpcoming(site) { const url = getUpcomingUrl(site); const res = await qu.getAll(url, 'li[data-itemtype=scene], div[data-itemtype*=scene]'); if (res.ok) { return scrapeAll(res.items, site, null, false); } return res.status; } function getDeepUrl(url, site, baseRelease, mobile) { const filter = new Set(['en', 'video', 'scene', site.slug, site.parent.slug]); const pathname = baseRelease?.path || new URL(url).pathname .split('/') .filter((component) => !filter.has(component)) .join('/'); // reduce to scene ID and title slug const sceneId = baseRelease?.entryId || pathname.match(/\/(\d+)\//)?.[1]; if (mobile && /%d/.test(mobile)) { return util.format(mobile, sceneId); } if (mobile && sceneId) { return `${mobile}${pathname}`; } if (site.parameters?.deep) { return `${site.parameters.deep}${pathname}`; } return url; } async function fetchScene(url, site, baseRelease, options) { if (site.parameters?.deep === false) { return baseRelease; } const deepUrl = getDeepUrl(url, site, baseRelease); const mobileUrl = options.includePhotos && getDeepUrl(url, site, baseRelease, site.parameters?.mobile || site.parent?.parameters?.mobile); if (deepUrl) { const [res, mobileRes] = await Promise.all([ qu.get(deepUrl), mobileUrl && qu.get(mobileUrl, null, { headers: { // don't redirect to main site 'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Mobile Safari/537.36', }, }), ]); if (res.status === 200) { const mobileItem = mobileRes?.status === 200 ? mobileRes.item : null; const scene = await scrapeScene(res.item, url, site, baseRelease, mobileItem, options); return { ...scene, deepUrl }; } } return null; } async function fetchActorScenes(actorName, apiUrl, siteSlug) { const res = await http.post(apiUrl, { requests: [ { indexName: 'all_scenes', params: `query=&filters=sitename:${siteSlug}&hitsPerPage=36&maxValuesPerFacet=100&page=0&facetFilters=[["lesbian:"],["bisex:"],["shemale:"],["actors.name:${actorName}"]]`, }, ], }, { headers: { Referer: `https://www.${siteSlug}.com/en/videos`, }, }, { encodeJSON: true, }); if (res.status === 200 && res.body.results[0].hits.length > 0) { return res.body.results[0].hits; } return []; } async function fetchProfile({ name: actorName }, context, include, altSearchUrl, getActorReleasesUrl) { const siteSlug = context.entity.slug || context.site?.slug || context.network?.slug; const actorSlug = actorName.toLowerCase().replace(/\s+/, '+'); const searchUrl = altSearchUrl ? `https://www.${siteSlug}.com/en/search/${actorSlug}/1/actor` : `https://www.${siteSlug}.com/en/search/${siteSlug}/actor/${actorSlug}`; const searchRes = await http.get(searchUrl); if (searchRes.status !== 200) { return null; } const actorUrl = scrapeActorSearch(searchRes.body.toString(), searchUrl, actorName); if (actorUrl) { const url = `https://${siteSlug}.com${actorUrl}`; const actorRes = await qu.get(url); if (actorRes.status !== 200) { return null; } return scrapeProfile(actorRes.item, url, actorName, siteSlug, getActorReleasesUrl, include.scenes, context); } return null; } async function fetchApiProfile({ name: actorName }, context, include) { const siteSlug = context.entity.slug || context.site?.slug || context.network?.slug; const actorSlug = encodeURI(actorName); const referer = `https://www.${siteSlug}.com/en/search`; const { apiUrl } = await fetchApiCredentials(referer); const res = await http.post(apiUrl, { requests: [ { indexName: 'all_actors', params: `query=${actorSlug}`, }, ], }, { headers: { Referer: referer, }, }, { encodeJSON: true, }); if (res.status === 200 && res.body.results[0].hits.length > 0) { const actorData = res.body.results[0].hits.find((actor) => slugify(actor.name) === slugify(actorName)); if (actorData) { const actorScenes = include.releases && await fetchActorScenes(actorData.name, apiUrl, siteSlug); return scrapeApiProfile(actorData, actorScenes, siteSlug); } } return null; } module.exports = { fetchApiLatest: fetchLatestApi, fetchApiProfile, fetchApiUpcoming: fetchUpcomingApi, fetchLatest, fetchLatestApi, fetchProfile, fetchScene, fetchSceneApi, fetchUpcoming, fetchUpcomingApi, api: { fetchLatest: fetchLatestApi, fetchUpcoming: fetchUpcomingApi, fetchProfile: fetchApiProfile, // fetchScene, fetchScene: fetchSceneApi, // scrapeMovie, fetchMovie: fetchMovieApi, }, getPhotos, scrapeApiProfile, scrapeApiReleases, scrapeProfile, scrapeAll, scrapeMovie, scrapeScene, };