Integrated Kink VR into main Kink scraper, fixed profile method.
This commit is contained in:
parent
bfb9581f12
commit
6a2772fac4
|
|
@ -6315,9 +6315,6 @@ const sites = [
|
||||||
url: 'https://kinkvr.com',
|
url: 'https://kinkvr.com',
|
||||||
tags: ['vr'],
|
tags: ['vr'],
|
||||||
parent: 'kink',
|
parent: 'kink',
|
||||||
parameters: {
|
|
||||||
layout: 'vr',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// KINK MEN
|
// KINK MEN
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -335,7 +335,7 @@ function curateProfileEntry(profile) {
|
||||||
hip: profile.hip,
|
hip: profile.hip,
|
||||||
penis_length: profile.penisLength,
|
penis_length: profile.penisLength,
|
||||||
penis_girth: profile.penisGirth,
|
penis_girth: profile.penisGirth,
|
||||||
circumcised: profile.circumcised,
|
circumcised: profile.isCircumcised,
|
||||||
natural_boobs: profile.naturalBoobs,
|
natural_boobs: profile.naturalBoobs,
|
||||||
height: profile.height,
|
height: profile.height,
|
||||||
weight: profile.weight,
|
weight: profile.weight,
|
||||||
|
|
@ -465,7 +465,7 @@ async function curateProfile(profile, actor) {
|
||||||
curatedProfile.penisLength = Number(profile.penisLength) || profile.penisLength?.match?.(/\d+/)?.[0] || null;
|
curatedProfile.penisLength = Number(profile.penisLength) || profile.penisLength?.match?.(/\d+/)?.[0] || null;
|
||||||
curatedProfile.penisGirth = Number(profile.penisGirth) || profile.penisGirth?.match?.(/\d+/)?.[0] || null;
|
curatedProfile.penisGirth = Number(profile.penisGirth) || profile.penisGirth?.match?.(/\d+/)?.[0] || null;
|
||||||
|
|
||||||
curatedProfile.circumcised = getBoolean(profile.circumcised);
|
curatedProfile.isCircumcised = getBoolean(profile.isCircumcised);
|
||||||
curatedProfile.naturalBoobs = getBoolean(profile.naturalBoobs);
|
curatedProfile.naturalBoobs = getBoolean(profile.naturalBoobs);
|
||||||
curatedProfile.hasTattoos = getBoolean(profile.hasTattoos);
|
curatedProfile.hasTattoos = getBoolean(profile.hasTattoos);
|
||||||
curatedProfile.hasPiercings = getBoolean(profile.hasPiercings);
|
curatedProfile.hasPiercings = getBoolean(profile.hasPiercings);
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,6 @@ module.exports = {
|
||||||
'8kmembers': kellymadison,
|
'8kmembers': kellymadison,
|
||||||
kink,
|
kink,
|
||||||
kinkmen: kink,
|
kinkmen: kink,
|
||||||
kinkvr: kink,
|
|
||||||
loveherfilms,
|
loveherfilms,
|
||||||
loveherfeet: loveherfilms,
|
loveherfeet: loveherfilms,
|
||||||
shelovesblack: loveherfilms,
|
shelovesblack: loveherfilms,
|
||||||
|
|
|
||||||
|
|
@ -268,8 +268,8 @@ async function scrapeProfile({ query, el }, channel, options) {
|
||||||
|
|
||||||
if (bio.penis_length) profile.penisLength = Number(bio.penis_length.match(/(\d+)\s*cm/i)?.[1] || inchesToCm(bio.penis_length.match(/(\d+\.?\d+)\s*in/i)?.[1])) || null;
|
if (bio.penis_length) profile.penisLength = Number(bio.penis_length.match(/(\d+)\s*cm/i)?.[1] || inchesToCm(bio.penis_length.match(/(\d+\.?\d+)\s*in/i)?.[1])) || null;
|
||||||
if (bio.penis_girth) profile.penisGirth = Number(bio.penis_girth.match(/(\d+)\s*cm/i)?.[1] || inchesToCm(bio.penis_girth.match(/(\d+\.?\d+)\s*in/i)?.[1])) || null;
|
if (bio.penis_girth) profile.penisGirth = Number(bio.penis_girth.match(/(\d+)\s*cm/i)?.[1] || inchesToCm(bio.penis_girth.match(/(\d+\.?\d+)\s*in/i)?.[1])) || null;
|
||||||
if (bio.circumcised && /yes/i.test(bio.circumcised)) profile.circumcised = true;
|
if (bio.circumcised && /yes/i.test(bio.circumcised)) profile.isCircumcised = true;
|
||||||
if (bio.circumcised && /no/i.test(bio.circumcised)) profile.circumcised = false;
|
if (bio.circumcised && /no/i.test(bio.circumcised)) profile.isCircumcised = false;
|
||||||
|
|
||||||
if (bio.natural_breasts && /yes/i.test(bio.natural_breasts)) profile.naturalBoobs = true;
|
if (bio.natural_breasts && /yes/i.test(bio.natural_breasts)) profile.naturalBoobs = true;
|
||||||
if (bio.natural_breasts && /no/i.test(bio.natural_breasts)) profile.naturalBoobs = false;
|
if (bio.natural_breasts && /no/i.test(bio.natural_breasts)) profile.naturalBoobs = false;
|
||||||
|
|
|
||||||
|
|
@ -79,52 +79,6 @@ async function fetchLatest(channel, page = 1) {
|
||||||
return res.status;
|
return res.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrapeAllVr(scenes, channel) {
|
|
||||||
return scenes.map(({ query }) => {
|
|
||||||
const release = {};
|
|
||||||
const url = query.url('a.image-link, a.video-title');
|
|
||||||
const { pathname } = new URL(url);
|
|
||||||
|
|
||||||
release.url = url;
|
|
||||||
// legacy ID in slug preferred to match old entries, but prepare for retirement just in case
|
|
||||||
release.entryId = pathname.match(/-(\d+)\/?$/)?.[1] || pathname.match(/\/vd\/(\d+)\//)[1];
|
|
||||||
|
|
||||||
release.title = query.content('.video-title');
|
|
||||||
release.description = query.content('.description');
|
|
||||||
|
|
||||||
release.date = query.date('.main-info', 'MMM Do YYYY', { match: /\w{3} \d+\w+ \d{4}/ });
|
|
||||||
|
|
||||||
release.actors = query.all('.actors a').map((actorEl) => ({
|
|
||||||
name: unprint.query.content(actorEl),
|
|
||||||
url: unprint.query.url(actorEl, null, { origin: channel.url }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
release.poster = query.sourceSet('.image-link img');
|
|
||||||
release.photos = query.dataset('.image-link div[data-gallery-images]', 'galleryImages')?.split(',').filter(Boolean); // can sometimes be ,,,, with no URLs
|
|
||||||
|
|
||||||
return release;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchLatestVr(channel, page = 1) {
|
|
||||||
const url = `${channel.url}/videos/page${page}/`;
|
|
||||||
|
|
||||||
const res = await unprint.get(url, {
|
|
||||||
selectAll: '#listView .video-list-view', // more details than #gridView
|
|
||||||
headers: {
|
|
||||||
Cookie: 'agreedToDisclaimer=true',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const scenes = scrapeAllVr(res.context, channel);
|
|
||||||
|
|
||||||
return scenes;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrapeScene({ query }, url, entity) {
|
function scrapeScene({ query }, url, entity) {
|
||||||
const release = { url };
|
const release = { url };
|
||||||
const data = query.json('div[data-setup]', { attribute: 'data-setup' });
|
const data = query.json('div[data-setup]', { attribute: 'data-setup' });
|
||||||
|
|
@ -200,73 +154,13 @@ async function fetchScene(url, channel) {
|
||||||
return res.status;
|
return res.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
const qualityMap = {
|
|
||||||
psvr: 1080, // as of recent, might've been lower in the past
|
|
||||||
'4k': 2160,
|
|
||||||
'5k': 2280,
|
|
||||||
'8k': 4320,
|
|
||||||
};
|
|
||||||
|
|
||||||
function scrapeSceneVr({ query }, url, channel) {
|
|
||||||
const release = {};
|
|
||||||
|
|
||||||
const { pathname } = new URL(url);
|
|
||||||
// legacy ID in slug preferred to match old entries, but prepare for retirement just in case
|
|
||||||
release.entryId = pathname.match(/-(\d+)\/?$/)?.[1] || pathname.match(/\/vd\/(\d+)\//)[1];
|
|
||||||
|
|
||||||
release.title = query.content('.page-title');
|
|
||||||
release.description = query.content('#collapseDescription .accordion-body') || query.attribute('meta[name="description"]', 'content');
|
|
||||||
|
|
||||||
release.date = query.date('.video-description-list', 'MMMM D, YYYY');
|
|
||||||
|
|
||||||
release.actors = query.all('.video-description-list a[href*="/girl"]').map((actorEl) => ({
|
|
||||||
name: unprint.query.content(actorEl),
|
|
||||||
url: unprint.query.url(actorEl, null, { origin: channel.url }),
|
|
||||||
})); // no sign of boys
|
|
||||||
|
|
||||||
release.tags = query.contents('.video-description-list a[href*="/category"]');
|
|
||||||
|
|
||||||
release.poster = query.poster('dl8-video');
|
|
||||||
|
|
||||||
release.photos = query.sourceSets('.carousel .item img');
|
|
||||||
|
|
||||||
if (query.exists('dl8-video source[src*=".mp4"]')) {
|
|
||||||
// sometimes the trailer URL is missing the filename, it won't play on their site either
|
|
||||||
release.trailer = {
|
|
||||||
src: query.video('dl8-video source'),
|
|
||||||
vr: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
release.qualities = query
|
|
||||||
.contents('#downloadsData a')
|
|
||||||
.map((button) => qualityMap[button.match(/download (\w+)/i)?.[1]?.toLowerCase()])
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
return release;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchSceneVr(url, channel) {
|
|
||||||
const res = await unprint.get(url, {
|
|
||||||
headers: {
|
|
||||||
Cookie: 'agreedToDisclaimer=true',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
return scrapeSceneVr(res.context, url, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function scrapeProfile({ query }, actorUrl) {
|
async function scrapeProfile({ query }, actorUrl) {
|
||||||
const profile = { url: actorUrl };
|
const profile = { url: actorUrl };
|
||||||
|
|
||||||
profile.entryId = actorUrl.match(/\/model\/(\d+)\//)?.[1] || query.attribute('h1 + button[data-id]', 'data-id');
|
profile.entryId = actorUrl.match(/\/model\/(\d+)\//)?.[1] || query.attribute('h1 + button[data-id]', 'data-id');
|
||||||
profile.description = query.content('.content-container #expand-text')?.trim();
|
profile.description = query.contents('#bioDescription p').join(' ');
|
||||||
|
|
||||||
const tags = query.contents('.content-container a[href*="/tag"]').map((tag) => tag.toLowerCase().trim());
|
const tags = query.contents('#bioDescription + div a').map((tag) => tag.toLowerCase().trim()); // no /tag on Kink Men
|
||||||
|
|
||||||
if (tags.includes('brunette') || tags.includes('brunet')) profile.hairColor = 'brown';
|
if (tags.includes('brunette') || tags.includes('brunet')) profile.hairColor = 'brown';
|
||||||
if (tags.includes('blonde') || tags.includes('blond')) profile.hairColor = 'blonde';
|
if (tags.includes('blonde') || tags.includes('blond')) profile.hairColor = 'blonde';
|
||||||
|
|
@ -283,7 +177,8 @@ async function scrapeProfile({ query }, actorUrl) {
|
||||||
if (tags.includes('pierced nipples')) profile.hasPiercings = true;
|
if (tags.includes('pierced nipples')) profile.hasPiercings = true;
|
||||||
if (tags.includes('tattoo')) profile.hasTattoos = true;
|
if (tags.includes('tattoo')) profile.hasTattoos = true;
|
||||||
|
|
||||||
if (tags.includes('foreskin')) profile.hasForeskin = true;
|
if (tags.includes('foreskin')) profile.isCircumcised = false;
|
||||||
|
if (tags.includes('circumcised')) profile.isCircumcised = true;
|
||||||
|
|
||||||
if ((tags.includes('big dick') || tags.includes('foreskin'))
|
if ((tags.includes('big dick') || tags.includes('foreskin'))
|
||||||
&& (tags.includes('fake boobs') || tags.includes('big tits'))) profile.gender = 'transsexual';
|
&& (tags.includes('fake boobs') || tags.includes('big tits'))) profile.gender = 'transsexual';
|
||||||
|
|
@ -300,11 +195,12 @@ async function getActorUrl({ name: actorName, url }, networkUrl) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// const searchRes = await tab.goto(`${networkUrl}/search?type=performers&q=${actorName}`);
|
// const searchRes = await tab.goto(`${networkUrl}/search?type=performers&q=${actorName}`);
|
||||||
const searchApiRes = await unprint.browser(`https://www.kink.com/api/v2/search/suggestions/performers?term=${actorName}`);
|
const res = await unprint.get(`https://www.kink.com/api/v2/search/suggestions/performers?term=${actorName}`, {
|
||||||
|
interface: 'request',
|
||||||
|
});
|
||||||
|
|
||||||
if (searchApiRes.status === 200) {
|
if (res.status === 200) {
|
||||||
const data = searchApiRes.context.query.json('body pre');
|
const actorId = res.data.find((actor) => actor.label === actorName)?.id;
|
||||||
const actorId = data.find((actor) => actor.label === actorName)?.id;
|
|
||||||
|
|
||||||
if (actorId) {
|
if (actorId) {
|
||||||
const actorUrl = `${networkUrl}/model/${actorId}/${slugify(actorName)}`;
|
const actorUrl = `${networkUrl}/model/${actorId}/${slugify(actorName)}`;
|
||||||
|
|
@ -333,82 +229,8 @@ async function fetchProfile(actor, entity) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getActorUrlVr(actor, entity) {
|
|
||||||
if (actor.url) {
|
|
||||||
return actor.url;
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await unprint.get(`${entity.url}/search/`, {
|
|
||||||
selectAll: '#actors option',
|
|
||||||
headers: {
|
|
||||||
Cookie: 'agreedToDisclaimer=true',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const actors = res.context.map(({ query }) => ({
|
|
||||||
name: query.content(),
|
|
||||||
id: query.attribute(null, 'value'),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const targetActor = actors.find((actorOption) => actor.slug === slugify(actorOption.name));
|
|
||||||
|
|
||||||
if (targetActor?.id) {
|
|
||||||
return `${entity.url}/girl/${targetActor.id}/${slugify(targetActor.name)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrapeProfileVr({ query }, url) {
|
|
||||||
const profile = { url };
|
|
||||||
|
|
||||||
const keys = query.contents('.info .key');
|
|
||||||
const values = query.contents('.info .value', { filter: false });
|
|
||||||
const bio = Object.fromEntries(keys.map((key, index) => [slugify(key, '_'), values[index]]));
|
|
||||||
|
|
||||||
profile.description = query.content('#readMoreFull');
|
|
||||||
profile.avatar = query.sourceSet('.images img');
|
|
||||||
|
|
||||||
if (bio.birthdate) profile.dateOfBirth = unprint.extractDate(bio.birthdate, 'MMMM DD, YYYY');
|
|
||||||
if (bio.country) profile.birthPlace = bio.country;
|
|
||||||
if (bio.cup) profile.cup = bio.cup;
|
|
||||||
if (bio.height) profile.height = Number(bio.height.match(/(\d+) cm/i)?.[1]) || null;
|
|
||||||
if (bio.weight) profile.weight = Number(bio.weight.match(/(\d+) kg/i)?.[1]) || null;
|
|
||||||
|
|
||||||
profile.socials = query.urls('.value.social a');
|
|
||||||
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchProfileVr(actor, entity) {
|
|
||||||
const url = await getActorUrlVr(actor, entity);
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
const res = await unprint.get(url, {
|
|
||||||
headers: {
|
|
||||||
Cookie: 'agreedToDisclaimer=true',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
return scrapeProfileVr(res.context, url, entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
fetchLatest,
|
fetchLatest,
|
||||||
fetchScene,
|
fetchScene,
|
||||||
fetchProfile,
|
fetchProfile,
|
||||||
vr: {
|
|
||||||
fetchLatest: fetchLatestVr,
|
|
||||||
fetchScene: fetchSceneVr,
|
|
||||||
fetchProfile: fetchProfileVr,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ const actors = [
|
||||||
{ entity: 'spicevids', name: 'Remy LaCroix', fields: ['avatar', 'gender', 'description', 'height', 'measurements', 'dateOfBirth', 'weight'] },
|
{ entity: 'spicevids', name: 'Remy LaCroix', fields: ['avatar', 'gender', 'description', 'height', 'measurements', 'dateOfBirth', 'weight'] },
|
||||||
{ entity: 'twistys', name: 'Remy LaCroix', fields: ['avatar', 'gender', 'description', 'height', 'measurements', 'dateOfBirth', 'weight', 'birthPlace', 'hairColor', 'ethnicity', 'naturalBoobs', 'hasPiercings'] },
|
{ entity: 'twistys', name: 'Remy LaCroix', fields: ['avatar', 'gender', 'description', 'height', 'measurements', 'dateOfBirth', 'weight', 'birthPlace', 'hairColor', 'ethnicity', 'naturalBoobs', 'hasPiercings'] },
|
||||||
{ entity: 'mypervyfamily', name: 'Anissa Kate', fields: ['avatar', 'gender'] },
|
{ entity: 'mypervyfamily', name: 'Anissa Kate', fields: ['avatar', 'gender'] },
|
||||||
|
{ entity: 'gaywire', name: 'Andy Adler', fields: ['avatar', 'gender'] },
|
||||||
// aylo > adult mobile
|
// aylo > adult mobile
|
||||||
{ entity: 'adultmobile', name: 'Scarlett Alexis', fields: ['avatar', 'gender'] },
|
{ entity: 'adultmobile', name: 'Scarlett Alexis', fields: ['avatar', 'gender'] },
|
||||||
{ entity: 'doghousedigital', name: 'Scarlett Alexis', fields: ['avatar', 'gender'] },
|
{ entity: 'doghousedigital', name: 'Scarlett Alexis', fields: ['avatar', 'gender'] },
|
||||||
|
|
@ -182,6 +183,9 @@ const actors = [
|
||||||
// missax
|
// missax
|
||||||
{ entity: 'missax', name: 'Alexis Fawx', fields: ['avatar', 'description'] },
|
{ entity: 'missax', name: 'Alexis Fawx', fields: ['avatar', 'description'] },
|
||||||
{ entity: 'allherluv', name: 'Krissy Lynn', fields: ['avatar', 'description'] },
|
{ entity: 'allherluv', name: 'Krissy Lynn', fields: ['avatar', 'description'] },
|
||||||
|
// kink
|
||||||
|
{ entity: 'kink', name: 'Remy LaCroix', fields: ['avatar', 'description', 'hairColor', 'naturalBoobs', 'ethnicity'] },
|
||||||
|
{ entity: 'kinkmen', name: 'Christian Wilde', fields: ['avatar', 'description', 'hairColor', 'hasTattoos', 'isCircumcised'] },
|
||||||
// etc.
|
// etc.
|
||||||
{ entity: 'analvids', name: 'Veronica Leal', fields: ['avatar', 'gender', 'birthCountry', 'nationality', 'age', 'aliases', 'nationality'] },
|
{ entity: 'analvids', name: 'Veronica Leal', fields: ['avatar', 'gender', 'birthCountry', 'nationality', 'age', 'aliases', 'nationality'] },
|
||||||
{ entity: 'bangbros', name: 'Kira Perez', fields: ['avatar', 'gender', 'ethnicity', 'hairColor'] },
|
{ entity: 'bangbros', name: 'Kira Perez', fields: ['avatar', 'gender', 'ethnicity', 'hairColor'] },
|
||||||
|
|
@ -227,7 +231,7 @@ async function validateUrl(url, mime = 'image/') {
|
||||||
const res = await fetch(href);
|
const res = await fetch(href);
|
||||||
|
|
||||||
const type = res.headers.get('content-type');
|
const type = res.headers.get('content-type');
|
||||||
const resolvedType = url.expectType?.[type] || type;
|
const resolvedType = url.expectType?.[type] || type || 'image/jpeg';
|
||||||
|
|
||||||
return resolvedType.includes(mime);
|
return resolvedType.includes(mime);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue