238 lines
7.1 KiB
JavaScript
Executable File
238 lines
7.1 KiB
JavaScript
Executable File
'use strict';
|
|
|
|
const unprint = require('unprint');
|
|
|
|
const slugify = require('../utils/slugify');
|
|
const { stripQuery } = require('../utils/url');
|
|
|
|
function scrapeAll(scenes, entity) {
|
|
return scenes.map(({ query }) => {
|
|
const release = {};
|
|
const networkUrl = entity.type === 'channel' ? entity.parent.url : entity.url;
|
|
|
|
const href = query.url('a[href*="/shoot"]');
|
|
|
|
release.url = `${networkUrl}${href}`;
|
|
|
|
release.shootId = href.split('/').slice(-1)[0];
|
|
release.entryId = release.shootId;
|
|
|
|
release.title = query.content('.card-body a[href*="/shoot"]').trim();
|
|
release.date = query.date('small > span', 'MMM D, YYYY');
|
|
|
|
release.actors = query.all('a[href*="/model"]').map((actorEl) => ({
|
|
name: unprint.query.content(actorEl),
|
|
url: unprint.query.url(actorEl, null, { origin: networkUrl }),
|
|
}));
|
|
|
|
const poster = query.img('.ratio-thumbnail img');
|
|
|
|
release.poster = [
|
|
stripQuery(poster).replace('_thumb', '_full'),
|
|
stripQuery(poster),
|
|
poster,
|
|
].filter(Boolean).map((src) => ({
|
|
src,
|
|
expectType: {
|
|
PNG: 'image/png',
|
|
},
|
|
}));
|
|
|
|
try {
|
|
release.photos = JSON.parse(query.attribute('.ratio-thumbnail img', 'data-cycle'))
|
|
.map((src) => Array.from(new Set([
|
|
stripQuery(src).replace('_thumb', '_full'),
|
|
stripQuery(src),
|
|
src,
|
|
])).filter(Boolean).map((source) => ({
|
|
src: source,
|
|
expectType: {
|
|
PNG: 'image/png',
|
|
},
|
|
})));
|
|
} catch (error) {
|
|
// no photos
|
|
}
|
|
|
|
release.trailer = `https://cdnp.kink.com/imagedb/${release.entryId}/trailer/${release.entryId}_trailer_high.mp4`;
|
|
|
|
release.channel = slugify(query.content('.shoot-thumbnail-footer a[href*="/channel"]'), '');
|
|
release.rating = query.number('.thumb-up') / 10;
|
|
|
|
return release;
|
|
});
|
|
}
|
|
|
|
async function fetchLatest(channel, page = 1) {
|
|
const url = `${channel.parent.url}/search?type=shoots&channelIds=${channel.parameters?.slug || channel.slug}&sort=published&page=${page}`;
|
|
|
|
const res = await unprint.browserRequest(url, {
|
|
selectAll: '.container .card',
|
|
});
|
|
|
|
if (res.status === 200) {
|
|
// const items = unprint.initAll(html, '.container .card');
|
|
|
|
const scenes = scrapeAll(res.context, channel);
|
|
|
|
return scenes;
|
|
}
|
|
|
|
return res.status;
|
|
}
|
|
|
|
function scrapeScene({ query }, url, entity) {
|
|
const release = { url };
|
|
const data = query.json('div[data-setup]', { attribute: 'data-setup' });
|
|
|
|
release.shootId = data?.id || new URL(url).pathname.split('/')[2];
|
|
release.entryId = data?.id || release.shootId;
|
|
|
|
release.title = data?.title || query.attribute('#shootPage #favoriteShootButton', 'data-title') || query.content('#shootPage h1');
|
|
release.description = query.content('//*[contains(text(), \'Description\')]/following-sibling::span/p');
|
|
|
|
release.date = query.date('.shoot-detail-legend', 'MMM D, YYYY');
|
|
|
|
release.duration = data?.duration
|
|
? data.duration / 1000
|
|
: query.duration('#shootPage .clock');
|
|
|
|
release.actors = query.elements('#shootPage h1 + span a[href*="/model"]').map((actorEl) => ({
|
|
name: unprint.query.content(actorEl).replace(/,\s*/, ''),
|
|
url: unprint.query.url(actorEl, null, { origin: entity.type === 'channel' ? entity.parent.url : entity.url }),
|
|
}));
|
|
|
|
release.director = query.content('.director-name')?.trim();
|
|
|
|
const poster = data?.posterUrl || query.poster();
|
|
|
|
release.poster = [
|
|
stripQuery(poster),
|
|
poster,
|
|
].filter(Boolean).map((src) => ({
|
|
src,
|
|
expectType: {
|
|
PNG: 'image/png',
|
|
},
|
|
}));
|
|
|
|
release.photos = query.json('#galleryImagesContainer', { attribute: 'data-images' })?.map((src) => [
|
|
src.fullPath,
|
|
src.thumbFullPath,
|
|
].filter(Boolean).map((source) => ({
|
|
src: source,
|
|
expectType: {
|
|
PNG: 'image/png',
|
|
},
|
|
})));
|
|
|
|
release.trailer = [
|
|
...(data?.trailer?.sources?.map((source) => ({
|
|
src: source.url,
|
|
quality: source.resolution,
|
|
})) || []),
|
|
`https://cdnp.kink.com/imagedb/${release.entryId}/trailer/${release.entryId}_trailer_high.mp4`,
|
|
];
|
|
|
|
release.tags = query.contents('#shootPage a[href*="/tag"]').map((tag) => tag.trim());
|
|
release.channel = data?.channelName?.name || slugify(query.url('.shoot-detail-legend a[href*="/channel"]')?.split('/').slice(-1)[0], '');
|
|
|
|
release.qualities = data?.resolutions
|
|
? Object.entries(data.resolutions).filter(([, enabled]) => enabled).map(([res]) => parseInt(res, 10))
|
|
: null;
|
|
|
|
return release;
|
|
}
|
|
|
|
async function fetchScene(url, channel) {
|
|
const res = await unprint.browserRequest(url);
|
|
|
|
if (res.status === 200) {
|
|
const scene = scrapeScene(res.context, url, channel);
|
|
|
|
return scene;
|
|
}
|
|
|
|
return res.status;
|
|
}
|
|
|
|
async function scrapeProfile({ query }, actorUrl) {
|
|
const profile = { url: actorUrl };
|
|
|
|
profile.entryId = actorUrl.match(/\/model\/(\d+)\//)?.[1] || query.attribute('h1 + button[data-id]', 'data-id');
|
|
profile.description = query.content('.content-container #expand-text')?.trim();
|
|
|
|
const tags = query.contents('.content-container a[href*="/tag"]').map((tag) => tag.toLowerCase().trim());
|
|
|
|
if (tags.includes('brunette') || tags.includes('brunet')) profile.hairColor = 'brown';
|
|
if (tags.includes('blonde') || tags.includes('blond')) profile.hairColor = 'blonde';
|
|
if (tags.includes('black hair')) profile.hairColor = 'black';
|
|
if (tags.includes('redhead')) profile.hairColor = 'red';
|
|
|
|
if (tags.includes('natural boobs')) profile.naturalBoobs = true;
|
|
if (tags.includes('fake boobs')) profile.naturalBoobs = false;
|
|
|
|
if (tags.includes('white')) profile.ethnicity = 'white';
|
|
if (tags.includes('latin')) profile.ethnicity = 'latin';
|
|
if (tags.includes('Black')) profile.ethnicity = 'black';
|
|
|
|
if (tags.includes('pierced nipples')) profile.hasPiercings = true;
|
|
if (tags.includes('tattoo')) profile.hasTattoos = true;
|
|
|
|
if (tags.includes('foreskin')) profile.hasForeskin = true;
|
|
|
|
if ((tags.includes('big dick') || tags.includes('foreskin'))
|
|
&& (tags.includes('fake boobs') || tags.includes('big tits'))) profile.gender = 'transsexual';
|
|
|
|
[profile.avatar, ...profile.photos] = query.imgs('.kink-slider-images img:not([data-src*="missing"])', { attribute: 'data-src' });
|
|
profile.social = query.urls('.content-container a[href*="twitter.com"], .content-container a[href*="x.com"]');
|
|
|
|
return profile;
|
|
}
|
|
|
|
async function getActorUrl({ name: actorName, url }, networkUrl) {
|
|
if (url) {
|
|
return url;
|
|
}
|
|
|
|
// const searchRes = await tab.goto(`${networkUrl}/search?type=performers&q=${actorName}`);
|
|
const searchApiRes = await unprint.browserRequest(`https://www.kink.com/api/v2/search/suggestions/performers?term=${actorName}`);
|
|
|
|
if (searchApiRes.status === 200) {
|
|
const data = searchApiRes.context.query.json('body pre');
|
|
const actorId = data.find((actor) => actor.label === actorName)?.id;
|
|
|
|
if (actorId) {
|
|
const actorUrl = `${networkUrl}/model/${actorId}/${slugify(actorName)}`;
|
|
|
|
return actorUrl;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function fetchProfile(actor, entity) {
|
|
const networkUrl = entity.type === 'channel' ? entity.parent.url : entity.url;
|
|
const actorUrl = await getActorUrl(actor, networkUrl);
|
|
|
|
if (actorUrl) {
|
|
const actorRes = await unprint.browserRequest(actorUrl);
|
|
|
|
if (actorRes.status === 200) {
|
|
return scrapeProfile(actorRes.context, actorUrl);
|
|
}
|
|
|
|
return actorRes.status;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
module.exports = {
|
|
// beforeNetwork,
|
|
fetchLatest,
|
|
fetchScene,
|
|
fetchProfile,
|
|
};
|