206 lines
5.5 KiB
JavaScript
Executable File
206 lines
5.5 KiB
JavaScript
Executable File
'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,
|
|
};
|