traxxx/src/scrapers/teamskeet.js

205 lines
6.3 KiB
JavaScript
Executable File

'use strict';
const qu = require('../utils/qu');
const http = require('../utils/http');
const slugify = require('../utils/slugify');
const { lbsToKg, feetInchesToCm } = require('../utils/convert');
function getChannelSlug(channelName, entity) {
if (entity.type === 'channel') {
return entity.slug;
}
if (!channelName) {
return null;
}
const channelSlug = slugify(channelName, '', { removePunctuation: true });
const channel = entity.children.find((child) => new RegExp(channelSlug).test(child.slug));
return channel?.slug || null;
}
function scrapeScene(scene, channel, parameters) {
const release = {};
release.entryId = scene.id;
release.url = `${channel.type === 'network' || channel.parameters?.layout === 'organic' ? channel.url : channel.parent.url}/movies/${release.entryId}`;
release.title = scene.title;
release.description = scene.description;
release.date = qu.extractDate(scene.publishedDate);
release.actors = scene.models?.map((model) => model.modelName) || [];
release.actors = scene.models?.map((model) => ({
name: model.modelName,
avatar: parameters.avatars && `${parameters.avatars}/${slugify(model.modelName, '_')}.jpg`,
url: `${channel.url}/models/${model.modelId}`,
}));
release.poster = [
// scene.img.replace('med.jpg', 'hi.jpg'), // this image is not always from the same scene! for example on Petite Teens 18
scene.img,
];
release.teaser = scene.videoTrailer;
if (scene.video) {
release.trailer = `https://cloudflarestream.com/${scene.video}/manifest/video.mpd?parentOrigin=${encodeURIComponent(channel.url)}`;
}
release.tags = scene.tags;
release.likes = scene.stats?.likeCount;
release.dislikes = scene.stats?.dislikeCount;
release.channel = getChannelSlug(scene.site?.name || scene.site?.nickName, channel);
return release;
}
function scrapeAll(scenes, channel, parameters) {
return scenes.map((scene) => scrapeScene(scene, channel, parameters));
}
function scrapeProfile(actor, entity, parameters) {
const profile = {};
profile.url = `${entity.url}/models/${actor.id}`;
profile.description = actor.modelBio;
if (actor.bio.about && !/\band\b/.test(actor.bio.about)) {
const bio = actor.bio.about.split(/\n/).filter(Boolean).reduce((acc, item) => {
const [key, value] = item.match(/(.+): (.+)/).slice(1);
return { ...acc, [slugify(key, '_')]: value.trim() };
}, {});
// birthdate seems never/rarely correct
if (bio.measurements) {
profile.measurements = bio.measurements;
} else {
const breastSize = actor.bio.breastSize?.match(/(\d+)(\w+)/)?.slice(1) || actor.bio.about.match(/Measurements: (\d+)(\w+)/)?.slice(1);
if (breastSize) {
[profile.bust, profile.cup] = breastSize;
}
}
profile.birthPlace = bio.birth_location;
profile.nationality = bio.nationality;
profile.ethnicity = bio.ethnicity;
profile.hairColor = bio.hair_color;
const piercings = actor.bio.about.match(/Piercings: (\w+)/i)?.[1];
const tattoos = actor.bio.about.match(/Tattoos: (\w+)/i)?.[1];
if (/yes|various/i.test(piercings)) profile.hasPiercings = true;
else if (/no/i.test(piercings)) profile.hasPiercings = false;
else if (bio.piercings) {
profile.hasPiercings = true;
profile.piercings = piercings;
}
if (/yes|various/i.test(tattoos)) profile.hasTattoos = true;
else if (/no/i.test(tattoos)) profile.hasTattoos = false;
else if (bio.tattoos) {
profile.hasTattoos = true;
profile.tattoos = tattoos;
}
}
if (actor.bio.heightFeet && actor.bio.heightInches) {
profile.height = feetInchesToCm(actor.bio.heightFeet, actor.bio.heightInches);
}
if (actor.bio.weight) {
profile.weight = lbsToKg(actor.bio.weight);
}
profile.avatar = actor.img;
profile.banner = actor.cover;
profile.scenes = actor.movies?.map((scene) => scrapeScene(scene, entity, parameters));
return profile;
}
async function fetchLatest(channel, page = 1, { parameters }) {
const res = await http.get(`https://store2.psmcdn.net/${parameters.endpoint}-videoscontent/_search?q=site.seo.seoSlug:"${parameters.id}"&sort=publishedDate:desc&size=30&from=${(page - 1) * 30}`);
if (res.ok) {
return scrapeAll(res.body.hits.hits.map(({ _source: scene }) => scene), channel, parameters);
}
return res.status;
}
async function fetchLatestOrganic(channel, page, context) {
const res = await http.get(`https://store.psmcdn.net/${context.parameters.endpoint}/newestMovies/items.json?orderBy="$key"&startAt="${context.cursor || 'aaaaaaaa'}"&limitToFirst=100`);
if (res.ok) {
const scenes = scrapeAll(Object.values(res.body), channel, context.parameters);
return {
// cursor implies page > 1 and first scene is last scene on previous page,
// it probably won't trip up the pagination logic, but avoid the duplicate anyway
scenes: context.cursor ? scenes.slice(1) : scenes,
context: {
cursor: Object.keys(res.body).at(-1), // official page seems to derive cursor from last scene, too
},
};
}
return res.status;
}
async function fetchScene(url, channel, baseScene, { parameters }) {
if (parameters.layout !== 'organic' && baseScene?.entryId) {
// overview and deep data is the same in elastic API, don't hit server unnecessarily
return baseScene;
}
const sceneSlug = new URL(url).pathname.match(/\/([\w-]+$)/)[1];
const res = await http.get(parameters.layout === 'organic'
? `https://store.psmcdn.net/${parameters.endpoint}/moviesContent/${sceneSlug}.json`
: `https://store2.psmcdn.net/${parameters.endpoint}-videoscontent/_doc/${sceneSlug}`);
if (res.ok && res.body.found) {
return scrapeScene(res.body._source, channel, parameters);
}
if (res.ok && parameters.layout === 'organic' && res.body.id) {
return scrapeScene(res.body, channel, parameters);
}
return res.status;
}
async function fetchProfile(baseActor, { entity, parameters }) {
// const url = format(parameters.profiles, { slug: baseActor.slug });
const url = parameters.layout === 'organic'
? `https://store.psmcdn.net/${parameters.endpoint}/modelsContent/${baseActor.slug}.json`
: `https://store2.psmcdn.net/${parameters.endpoint}-modelscontent/_doc/${baseActor.slug}`;
const res = await qu.get(url);
if (res.ok && res.body) {
return scrapeProfile(parameters.layout === 'organic' ? res.body : res.body._source || res.body, entity, parameters);
}
return res.status;
}
module.exports = {
fetchLatest,
fetchScene,
fetchProfile,
organic: {
fetchLatest: fetchLatestOrganic,
fetchScene,
},
};