From 6a2772fac48ebc748abe79ffd9b5eca78910aff5 Mon Sep 17 00:00:00 2001 From: DebaucheryLibrarian Date: Wed, 21 Jan 2026 01:32:51 +0100 Subject: [PATCH] Integrated Kink VR into main Kink scraper, fixed profile method. --- seeds/02_sites.js | 3 - src/actors.js | 4 +- src/scrapers/actors.js | 1 - src/scrapers/hush.js | 4 +- src/scrapers/kink.js | 196 ++--------------------------------------- tests/profiles.js | 6 +- 6 files changed, 18 insertions(+), 196 deletions(-) diff --git a/seeds/02_sites.js b/seeds/02_sites.js index 8641441e..191780aa 100755 --- a/seeds/02_sites.js +++ b/seeds/02_sites.js @@ -6315,9 +6315,6 @@ const sites = [ url: 'https://kinkvr.com', tags: ['vr'], parent: 'kink', - parameters: { - layout: 'vr', - }, }, // KINK MEN { diff --git a/src/actors.js b/src/actors.js index 9fb59a18..9ee6a4c0 100755 --- a/src/actors.js +++ b/src/actors.js @@ -335,7 +335,7 @@ function curateProfileEntry(profile) { hip: profile.hip, penis_length: profile.penisLength, penis_girth: profile.penisGirth, - circumcised: profile.circumcised, + circumcised: profile.isCircumcised, natural_boobs: profile.naturalBoobs, height: profile.height, weight: profile.weight, @@ -465,7 +465,7 @@ async function curateProfile(profile, actor) { curatedProfile.penisLength = Number(profile.penisLength) || profile.penisLength?.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.hasTattoos = getBoolean(profile.hasTattoos); curatedProfile.hasPiercings = getBoolean(profile.hasPiercings); diff --git a/src/scrapers/actors.js b/src/scrapers/actors.js index 55d722ea..de68d154 100644 --- a/src/scrapers/actors.js +++ b/src/scrapers/actors.js @@ -214,7 +214,6 @@ module.exports = { '8kmembers': kellymadison, kink, kinkmen: kink, - kinkvr: kink, loveherfilms, loveherfeet: loveherfilms, shelovesblack: loveherfilms, diff --git a/src/scrapers/hush.js b/src/scrapers/hush.js index 93bc0f93..9f10da1a 100755 --- a/src/scrapers/hush.js +++ b/src/scrapers/hush.js @@ -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_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 && /no/i.test(bio.circumcised)) profile.circumcised = false; + if (bio.circumcised && /yes/i.test(bio.circumcised)) profile.isCircumcised = true; + 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 && /no/i.test(bio.natural_breasts)) profile.naturalBoobs = false; diff --git a/src/scrapers/kink.js b/src/scrapers/kink.js index aa5585b0..10fb3201 100755 --- a/src/scrapers/kink.js +++ b/src/scrapers/kink.js @@ -79,52 +79,6 @@ async function fetchLatest(channel, page = 1) { 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) { const release = { url }; const data = query.json('div[data-setup]', { attribute: 'data-setup' }); @@ -200,73 +154,13 @@ async function fetchScene(url, channel) { 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) { 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(); + 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('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('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')) && (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 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) { - const data = searchApiRes.context.query.json('body pre'); - const actorId = data.find((actor) => actor.label === actorName)?.id; + if (res.status === 200) { + const actorId = res.data.find((actor) => actor.label === actorName)?.id; if (actorId) { const actorUrl = `${networkUrl}/model/${actorId}/${slugify(actorName)}`; @@ -333,82 +229,8 @@ async function fetchProfile(actor, entity) { 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 = { fetchLatest, fetchScene, fetchProfile, - vr: { - fetchLatest: fetchLatestVr, - fetchScene: fetchSceneVr, - fetchProfile: fetchProfileVr, - }, }; diff --git a/tests/profiles.js b/tests/profiles.js index 17f26bc7..c27b689a 100644 --- a/tests/profiles.js +++ b/tests/profiles.js @@ -86,6 +86,7 @@ const actors = [ { 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: 'mypervyfamily', name: 'Anissa Kate', fields: ['avatar', 'gender'] }, + { entity: 'gaywire', name: 'Andy Adler', fields: ['avatar', 'gender'] }, // aylo > adult mobile { entity: 'adultmobile', name: 'Scarlett Alexis', fields: ['avatar', 'gender'] }, { entity: 'doghousedigital', name: 'Scarlett Alexis', fields: ['avatar', 'gender'] }, @@ -182,6 +183,9 @@ const actors = [ // missax { entity: 'missax', name: 'Alexis Fawx', 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. { entity: 'analvids', name: 'Veronica Leal', fields: ['avatar', 'gender', 'birthCountry', 'nationality', 'age', 'aliases', 'nationality'] }, { 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 type = res.headers.get('content-type'); - const resolvedType = url.expectType?.[type] || type; + const resolvedType = url.expectType?.[type] || type || 'image/jpeg'; return resolvedType.includes(mime); }