'use strict'; const unprint = require('unprint'); const slugify = require('../utils/slugify'); function scrapeAll(scenes, channel, url) { return scenes.map(({ query }) => { const release = {}; release.url = query.url('a', { origin: channel.origin }); release.entryId = new URL(release.url).pathname.match(/(\d+)\/?$/)?.[1]; release.title = query.content('.card__h'); release.date = query.date('.card__date', 'D MMMM, YYYY', { match: null }); release.actors = query.all('.card__links a').map((actorEl) => ({ name: unprint.query.content(actorEl), url: unprint.query.url(actorEl, null, { origin: channel.url }), })); const poster = query.sourceSet('picture source[type="image/jpeg"]', 'data-srcset') || query.sourceSet('picture source[type="image/jpeg"]', 'srcset') || query.sourceSet('.video__cover', 'srcset'); if (poster?.[0]) { release.poster = [ poster[0].replace(/small|tiny/, 'large'), ...poster, ].map((src) => ({ src, referer: url, })); const teaser = poster[0].replace(/\b(cover|hero|\d+)\/[a-z0-9_]+\.[a-z]+$/i, 'roll.webm'); // actually how site generates teaser URL release.teaser = { src: teaser, referer: url, }; } release.channel = channel.slug; // avoid being assigned to WankzVR network return release; }); } async function fetchLatest(channel, page) { const url = `${channel.url}/videos?o=d&p=${page}`; const res = await unprint.get(url, { selectAll: '.layout__content > .cards-list .card' }); // .cards-list is also used for hidden upcoming scenes if (res.ok) { return scrapeAll(res.context, channel, url); } return res.status; } async function getTrailerUrl(release, channel, cookies, referer) { const csrfToken = cookies.csrfst; if (!csrfToken) { return null; } const res = await unprint.post(`${channel.url}/ajax/player-config.json`, { item_id: release.entryId, }, { form: true, headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-Token': csrfToken, }, cookies, }); if (res.ok) { const trailers = res.data.streams.map((trailer) => ({ src: trailer.url, quality: Number(trailer.id?.match(/\d+/)?.[0] || trailer?.name.match(/\d+/)?.[0]), vr: true, referer, })); const poster = unprint.prefixUrl(res.data.poster, res.data.thumbCDN); return { trailers, poster: poster && { src: poster, referer, }, }; } return null; } async function scrapeScene({ query }, { url, entity, include, cookies }) { const release = {}; const data = query.json('script[type="application/ld+json"]'); release.entryId = new URL(url).pathname.match(/(\d+)\/?$/)?.[1]; release.title = query.content('.detail__title'); release.description = query.content('.detail__txt'); release.date = query.date('.detail__date', 'D MMMM, YYYY', { match: null }); release.duration = query.number('.time') * 60; release.actors = (query.all('.detail__header-lg .detail__models a') || query.all('.detail__header-sm .detail__models a')).map((el) => ({ name: unprint.query.content(el), url: unprint.query.url(el, null, { origin: entity.origin }), })); release.tags = query.contents('.tag-list .tag').concat(query.contents('.detail__specs-list .detail__specs-item')); release.photos = query.all('.photo-strip__slide').map((el) => ([ unprint.query.img(el, null, 'data-src'), unprint.query.img(el, 'img'), ].map((src) => ({ src, referer: url })))); if (data?.thumbnailUrl) { release.poster = [ data.thumbnailUrl.replace(/small|tiny/, 'large'), data.thumbnailUrl, ].map((src) => ({ src, referer: url })); } if (include.trailers || (!release.poster && include.poster)) { const { trailers, poster } = await getTrailerUrl(release, entity, cookies, url) || {}; release.trailer = trailers; release.poster = poster; } return release; } async function fetchActorScenes({ query }, url, entity, page = 1, accScenes = []) { const scenes = scrapeAll(unprint.initAll(query.all('.cards-list .card')), entity); const hasNextPage = !query.exists('.pagenav__link.inactive'); if (hasNextPage) { const { origin, pathname, searchParams } = new URL(url); searchParams.set('p', page + 1); const res = await unprint.get(`${origin}${pathname}?${searchParams}`); if (res.ok) { return fetchActorScenes(res.context, url, entity, page + 1, accScenes.concat(scenes)); } } return accScenes.concat(scenes); } async function scrapeProfile({ query }, url, entity, options) { const profile = {}; const bio = query.all('.person__meta__item').reduce((acc, el) => ({ ...acc, [slugify(unprint.query.content(el, '.person__meta__label'))]: unprint.query.text(el), }), {}); profile.description = query.content('.person__content'); profile.gender = entity.slug === 'tranzvr' ? 'transsexual' : 'female'; profile.age = Number(bio.age) || null; profile.birthPlace = bio.birthplace; // height shown in imperial with cm between brackets when requested from North American IP, but only in cm for European IPs profile.height = unprint.extractNumber(bio.height, { match: /(\d+)cm/, matchIndex: 1 }); profile.measurements = bio.measurements; profile.avatar = query.sourceSet('.person__avatar img').map((src) => ({ src, referer: url, })); if (options.includeActorScenes) { profile.scenes = await fetchActorScenes({ query }, url, entity); } return profile; } async function fetchProfile(baseActor, { entity }, options) { const url = `${entity.url}/${baseActor.slug}`; const res = await unprint.get(url); if (res.ok) { return scrapeProfile(res.context, url, entity, options); } return res.status; } module.exports = { fetchLatest, scrapeScene, fetchProfile, };