'use strict'; const unprint = require('unprint'); const slugify = require('../utils/slugify'); const { convert } = require('../utils/convert'); // const { getImageWithFallbacks } = require('./elevatedx'); function curateSources(item) { if (!item) { return null; } if (item.sources) { return item.sources .toSorted((sourceA, sourceB) => sourceB.resolution - sourceA.resolution) .map((source) => source.path); } return item.previewImage; } function scrapeScene(data, channel) { const release = {}; release.entryId = data.slug?.toLowerCase(); // _id can't be used for API lookup release.url = data.slug && `${channel.origin}/tour/trailers/${data.slug}.html`; release.title = data.title; release.description = data.description; release.date = new Date(data.releaseDateVideo); release.actors = data.models?.map((actor) => ({ name: actor.modelName, url: actor.slug && `${channel.origin}/tour/models/${actor.slug}.html`, entryId: actor._id, })); release.tags = data.categories?.map((category) => category.title); if (data.type?.toLowerCase() === 'bts') { release.tags = release.tags.concat('bts'); } release.poster = curateSources(data.thumb); release.photos = [...data.photos?.map((photo) => curateSources(photo)) || []].filter(Boolean); if (data.thumbHover && data.thumbHover.baseName !== '849') { // placeholder image release.photos = release.photos.concat([curateSources(data.thumbHover)]); } const trailerType = data.trailer?.type === 'previewTrailer' ? 'teaser' : 'trailer'; release[trailerType] = data.trailer?.sources?.map((source) => ({ src: source.path, quality: source.quality || source.height || null, // only available on teaser expectType: { 'application/octet-stream': 'video/mp4', }, })); return release; } async function fetchLatest(channel, page = 1, { parameters }) { // bonus-type scenes are third-party, don't include const url = `${channel.origin.replace('www.', 'api.')}/v1/content-sets?types[]=Content&types[]=Tease&types[]=BTS&sort=latest&limit=27&offset=${(page - 1) * 27}`; const res = await unprint.get(url, { interface: 'request', headers: { 'X-Site-Id': parameters.xSiteId, }, }); if (res.ok && res.data?.items) { return res.data.items.map((item) => scrapeScene(item, channel)); } return res.status; } /* no entry ID, enable when two entry IDs are supported async function fetchUpcoming(channel, _page, { parameters }) { // unsure if site ID parameter is necessary when the header is present, but the site uses it const url = `${channel.origin.replace('www.', 'api.')}/v1/content-sets/upcoming?siteId${parameters.xSiteId}`; const res = await unprint.get(url, { interface: 'request', headers: { 'X-Site-Id': parameters.xSiteId, }, }); if (res.ok && res.data) { return res.data.map((item) => scrapeScene(item, channel)); } return res.status; } */ async function fetchScene(sceneUrl, entity, _baseRelease, { parameters }) { // deep data not identical, contains photoset and full trailer const slug = new URL(sceneUrl).pathname.match(/\/trailers\/(.*?)\.html/)[1]; const url = `${entity.origin.replace('www.', 'api.')}/v1/content-sets/${slug}`; const res = await unprint.get(url, { interface: 'request', headers: { 'X-Site-Id': parameters.xSiteId, }, }); if (res.ok && res.data) { return scrapeScene(res.data, entity); } return res.status; } function scrapeProfile({ query }, url) { const profile = { url }; const bio = Object.fromEntries(query.all('ul[class*="HeroModel_list"] li').map((bioEl) => [ slugify(unprint.query.content(bioEl, 'p:first-child'), '_'), unprint.query.content(bioEl, 'p:last-child'), ])); profile.description = query.content('p[class*="HeroModel_text"]'); profile.birthPlace = bio.place_of_birth; profile.dateOfBirth = unprint.extractDate(bio.date_of_birth, ['MMMM DD, YYYY', 'MM/DD/YYYY']); profile.ethnicity = bio.ethnicity; profile.measurements = bio.measurements; profile.height = convert(bio.height, 'cm'); profile.weight = convert(bio.weight, 'lb', 'kg'); profile.hairColor = bio.hair_color; profile.eyes = bio.eye_color; profile.foot = unprint.extractNumber(bio.feet_size, { match: /\((\d+(.\d+)?) eur\)/i, matchIndex: 1 }); if (/(natural)|(real)/i.test(bio.boob_type || bio.tits_type)) { profile.naturalBoobs = true; } if (/(enhanced)|(fake)/i.test(bio.boob_type || bio.tits_type)) { profile.naturalBoobs = false; } if (bio.tattoos) { profile.hasTattoos = !/none/i.test(bio.tattoos); profile.tattoos = profile.hasTattoos ? bio.tattoos : null; } if (bio.piercings) { profile.hasPiercings = !/none/i.test(bio.piercings); profile.piercings = profile.hasPiercings ? bio.piercings : null; } profile.avatar = query.sourceSet('picture[class*="modelImage"] img'); return profile; } async function getActorUrl(actor, { entity, parameters }) { if (actor.url) { return actor.url; } const url = `${entity.origin.replace('www.', 'api.')}/v1/models?limit=12&offset=0&query=&sort=latest&modelsNames[]=${slugify(actor.name, '+')}`; const res = await unprint.get(url, { interface: 'request', headers: { 'X-Site-Id': parameters.xSiteId, }, }); if (res.ok) { const actorSlug = res.data.items?.find((item) => slugify(item.modelName) === actor.slug)?.slug; if (actorSlug) { return `${entity.origin}/tour/models/${actorSlug}.html`; } } return null; } async function fetchProfile(actor, context) { const actorUrl = await getActorUrl(actor, context); if (!actorUrl) { return null; } const res = await unprint.get(actorUrl); if (res.ok) { return scrapeProfile(res.context, actorUrl, context); } return res.status; } module.exports = { fetchLatest, // fetchUpcoming, fetchProfile, fetchScene, };