'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const argv = require('../src/argv'); const include = require('../src/utils/argv-include')(argv); const slugify = require('../src/utils/slugify'); const scrapers = require('../src/scrapers/scrapers'); const { fetchEntitiesBySlug } = require('../src/entities'); const { resolveLayoutScraper } = require('../src/scrapers/resolve'); const getRecursiveParameters = require('../src/utils/get-recursive-parameters'); const knex = require('../src/knex'); const actors = [ // jules jordan { entity: 'julesjordan', name: 'Vanna Bardot', fields: ['height', 'dateOfBirth', 'measurements', 'description', 'avatar'] }, // gamma { entity: 'wicked', name: 'Abella Danger', fields: ['gender', 'avatar', 'description'] }, { entity: 'xempire', name: 'Abella Danger', fields: ['gender', 'avatar', 'description'] }, // vixen { entity: 'vixen', name: 'Abella Danger', fields: ['gender', 'avatar', 'description'] }, { entity: 'tushy', name: 'Abella Danger', fields: ['gender', 'avatar', 'description'] }, { entity: 'tushyraw', name: 'Abella Danger', fields: ['gender', 'avatar', 'description'] }, { entity: 'blacked', name: 'Abella Danger', fields: ['gender', 'avatar', 'description'] }, { entity: 'blackedraw', name: 'Abella Danger', fields: ['gender', 'avatar', 'description'] }, { entity: 'slayed', name: 'Vanna Bardot', fields: ['gender', 'avatar', 'description'] }, { entity: 'deeper', name: 'Vanna Bardot', fields: ['gender', 'avatar', 'description'] }, { entity: 'milfy', name: 'Clea Gaultier', fields: ['gender', 'avatar', 'description'] }, { entity: 'wifey', name: 'Danielle Renae', fields: ['gender', 'avatar', 'description'] }, // teamskeet { entity: 'teamskeet', name: 'Abella Danger', fields: ['description', 'avatar', 'measurements', 'birthPlace', 'nationality', 'ethnicity', 'height', 'weight', 'hairColor', 'hasPiercings'] }, { entity: 'teamskeet', name: 'Kali Roses', fields: ['description', 'avatar', 'measurements', 'nationality', 'ethnicity', 'hairColor', 'hasPiercings', 'hasTattoos'] }, // tattoos // analvids { entity: 'analvids', name: 'Veronica Leal', fields: ['avatar', 'gender', 'birthCountry', 'nationality', 'age', 'aliases', 'nationality'] }, // mike adriano { entity: 'trueanal', name: 'Brenna McKenna', fields: ['avatar', 'gender', 'description', 'dateOfBirth', 'birthPlace', 'measurements', 'eyes', 'weight', 'height', 'hairColor', 'hasTattoos'] }, { entity: 'analonly', name: 'Lilith Grace', fields: ['avatar', 'gender', 'description', 'dateOfBirth', 'birthPlace', 'measurements', 'eyes', 'weight', 'height', 'hairColor'] }, { entity: 'allanal', name: 'Lexi Lore', fields: ['avatar', 'gender', 'description', 'dateOfBirth', 'birthPlace', 'measurements', 'eyes', 'weight', 'height', 'hairColor'] }, { entity: 'swallowed', name: 'Brooklyn Gray', fields: ['avatar', 'gender', 'description', 'dateOfBirth', 'birthPlace', 'measurements', 'eyes', 'weight', 'height', 'hairColor', 'hasTattoos'] }, { entity: 'nympho', name: 'Gianna Dior', fields: ['avatar', 'gender', 'description', 'dateOfBirth', 'birthPlace', 'measurements', 'eyes', 'weight', 'height', 'hairColor'] }, { entity: 'dirtyauditions', name: 'Nicole Kitt', fields: ['avatar', 'gender', 'description', 'dateOfBirth', 'birthPlace', 'measurements', 'eyes', 'weight', 'height', 'hairColor'] }, // spizoo { entity: 'spizoo', name: 'Charlotte Sins', fields: ['description', 'avatar', 'dateOfBirth', 'ethnicity', 'nationality', 'height', 'measurements', 'hasTattoos', 'hasPiercings', 'hairColor', 'eyes', 'butt', 'pussy'] }, { entity: 'rawattack', name: 'Kitana Montana', fields: ['avatar', 'dateOfBirth', 'nationality', 'measurements', 'eyes', 'height', 'hairColor', 'hasTattoos'] }, // hush / hussiepass { entity: 'hussiepass', name: 'Roxie Sinner', fields: ['avatar', 'description', 'dateOfBirth', 'birthPlace', 'ethnicity', 'measurements', 'foot', 'height', 'weight', 'hasTattoos', 'hasPiercings', 'naturalBoobs', 'socials'] }, { entity: 'eyeontheguy', name: 'Tommy Gunn', fields: ['avatar'] }, { entity: 'interracialpovs', name: 'Nia Nacci', fields: ['avatar', 'aliases', 'dateOfBirth', 'birthPlace', 'ethnicity', 'measurements', 'height', 'weight', 'hasTattoos', 'hasPiercings', 'naturalBoobs', 'socials'] }, { entity: 'povpornstars', name: 'Anna Bell Peaks', fields: ['avatar', 'aliases', 'description', 'dateOfBirth', 'birthPlace', 'ethnicity', 'measurements', 'height', 'weight', 'hasTattoos', 'hasPiercings', 'naturalBoobs', 'socials'] }, { entity: 'seehimfuck', name: 'Sheem The Dream', fields: ['avatar', 'description', 'dateOfBirth', 'birthPlace', 'ethnicity', 'height', 'weight', 'hasTattoos', 'hasPiercings', 'penisLength', 'circumcised', 'socials'] }, { entity: 'hushpass', name: 'Dylan Ryder', fields: ['avatar'] }, { entity: 'interracialpass', name: 'Aidra Fox', fields: ['avatar', 'height', 'measurements'] }, // kelly madison / 8K { entity: 'kellymadison', name: 'Ava Addams', fields: ['avatar', 'description', 'age', 'height', 'measurements', 'birthPlace', 'dateOfBirth', 'ethnicity'] }, { entity: '8kmembers', name: 'Angie Lynx', fields: ['age', 'height', 'measurements', 'birthPlace', 'dateOfBirth', 'ethnicity'] }, // aylo { entity: 'brazzers', name: 'Lexi Lore', fields: ['avatar', 'description', 'gender', 'height', 'weight', 'measurements', 'birthPlace', 'dateOfBirth', 'ethnicity', 'hairColor', 'hasTattoos', 'hasPiercings'] }, { entity: 'digitalplayground', name: 'Elly Clutch', fields: ['avatar', 'description', 'gender', 'height', 'measurements', 'birthPlace', 'dateOfBirth'] }, { entity: 'realitykings', name: 'Abella Danger', fields: ['avatar', 'description', 'gender', 'height', 'measurements', 'birthPlace', 'dateOfBirth', 'weight', 'hairColor', 'ethnicity'] }, { entity: 'fakehub', name: 'Abella Danger', fields: ['avatar', 'description', 'gender', 'height', 'measurements', 'birthPlace', 'dateOfBirth', 'weight', 'hairColor', 'ethnicity'] }, { entity: 'babes', name: 'Alina Lopez', fields: ['avatar', 'description', 'gender', 'height', 'measurements', 'birthPlace', 'dateOfBirth', 'weight', 'hairColor', 'ethnicity', 'hasTattoos', 'hasPiercings'] }, { entity: 'letsdoeit', name: 'Nicole Doshi', fields: ['avatar', 'description', 'gender', 'height', 'measurements', 'birthPlace', 'dateOfBirth'] }, { entity: 'men', name: 'Cade Maddox', fields: ['avatar', 'description', 'gender', 'height', 'ethnicity', 'penisLength', 'dateOfBirth', 'weight', 'hairColor', 'hasTattoos'] }, { entity: 'metrohd', name: 'April Olsen', fields: ['avatar', 'description', 'gender', 'birthPlace', 'height', 'measurements', 'dateOfBirth', 'weight'] }, { entity: 'mofos', name: 'Ariana Starr', fields: ['avatar', 'description', 'gender', 'birthPlace', 'height', 'measurements', 'dateOfBirth'] }, { entity: 'propertysex', name: 'Desiree Dulce', fields: ['avatar', 'description', 'gender', 'birthPlace', 'height', 'measurements', 'dateOfBirth', 'weight', 'hairColor', 'ethnicity', 'hasPiercings'] }, { entity: 'sexyhub', name: 'Angie Lynx', fields: ['avatar', 'description', 'gender', 'birthPlace', 'height', 'measurements', 'dateOfBirth'] }, { entity: 'squirted', name: 'Nicole Doshi', fields: ['avatar', 'description', 'gender', 'birthPlace', 'height', 'measurements', 'dateOfBirth'] }, { entity: 'transangels', name: 'Aubrey Kate', fields: ['avatar', 'description', 'gender', 'birthPlace', 'height', 'measurements', 'dateOfBirth', 'weight', 'hairColor', 'ethnicity', 'hasTattoos'] }, { entity: 'trueamateurs', name: 'Natalie FLowers', fields: ['avatar', 'gender'] }, { 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'] }, // aylo > adult mobile { entity: 'adultmobile', name: 'Scarlett Alexis', fields: ['avatar', 'gender'] }, { entity: 'doghousedigital', name: 'Scarlett Alexis', fields: ['avatar', 'gender'] }, { entity: 'familysinners', name: 'Scarlett Alexis', fields: ['avatar', 'gender'] }, { entity: 'iconmale', name: 'Troy Accola', fields: ['avatar', 'gender', 'hairColor', 'ethnicity', 'hasTattoos', 'hasPiercings'] }, { entity: 'realityjunkies', name: 'Scarlett Alexis', fields: ['avatar', 'gender'] }, { entity: 'sweetheartvideo', name: 'Lexi Lore', fields: ['avatar', 'gender', 'hairColor', 'ethnicity', 'hasTattoos', 'hasPiercings'] }, { entity: 'sweetsinner', name: 'Anna Claire Clouds', fields: ['avatar', 'gender'] }, // bangros { entity: 'bangbros', name: 'Kira Perez', fields: ['avatar', 'gender', 'ethnicity', 'hairColor'] }, ]; const actorScrapers = scrapers.actors; const source = argv.source?.[0] || null; async function validateUrl(url, mime = 'image/') { if (!url) { return false; } const href = url.src || url; try { new URL(href); // eslint-disable-line no-new } catch (_error) { return false; } const res = await fetch(href); const type = res.headers.get('content-type'); const resolvedType = url.expectType?.[type] || type; return resolvedType.includes(mime); } const validators = { age: (value) => !!Number(value), gender: (value) => value && ['female', 'male', 'transsexual', 'trans'].includes(value.toLowerCase()), description: (value) => typeof value === 'string' && value.length > 3, birthPlace: (value) => typeof value === 'string' && value.length > 1, // may return US or USA birthCountry: (value) => typeof value === 'string' && value.length > 1, nationality: (value) => typeof value === 'string' && value.length > 3, height: (value) => !!Number(value) || /\d'\d{1,2}"/.test(value), weight: (value) => !!Number(value), eyes: (value) => typeof value === 'string' && value.length > 3, hairColor: (value) => typeof value === 'string' && value.length > 3, measurements: (value) => /(\d+)([a-z]+)?(?:\s*[-x]\s*(\d+)\s*[-x]\s*(\d+))?/i.test(value), // from actors module dateOfBirth: (value) => value instanceof Date && !Number.isNaN(value.getFullYear()), hasTattoos: (value) => typeof value === 'boolean', hasPiercings: (value) => typeof value === 'boolean', avatar: async (value) => [].concat(value).reduce(async (chain, url) => { const acc = await chain; if (!acc) { return acc; } return validateUrl(url); }, Promise.resolve(true)), socials: async (value) => [].concat(value).reduce(async (chain, url) => { const acc = await chain; if (!acc) { return acc; } return validateUrl(url, 'text/html'); }, Promise.resolve(true)), }; // profiler in this context is shorthand for profile scraper async function init() { const entitiesBySlug = await fetchEntitiesBySlug(Object.keys(actorScrapers), { types: ['channel', 'network', 'info'], prefer: argv.prefer }); Object.entries(actorScrapers).reduce(async (chain, [entitySlug, scraper]) => { await chain; const entity = entitiesBySlug[entitySlug] || null; const fetchProfile = resolveLayoutScraper(entity, scraper)?.fetchProfile; const tests = actors.filter((actor) => actor.entity === entitySlug); // TODO: remove when all tests are written if (tests.length === 0) { console.log('TODO', entitySlug); return; } if (source && source !== entitySlug) { console.log('____', entitySlug); return; } await test(`${entitySlug} (${entity?.name})`, async () => { await test(`${entitySlug} has scraper`, () => assert.notEqual(fetchProfile, null)); await test(`${entitySlug} has entity`, () => assert.notEqual(entity, null)); await test(`${entitySlug} has tests`, () => assert.notEqual(tests.length, 0)); await test(`${entitySlug} has valid fields`, async () => Promise.all(tests.map(async (actor) => { const profile = await fetchProfile({ name: actor.name, slug: slugify(actor.name), }, { ...entity, entity, channel: entity, network: entity.parent, parameters: getRecursiveParameters(entity), }, include); console.log(profile); console.log('Untested fields', Object.entries(profile).filter(([field, value]) => !actor.fields.includes(field) && typeof value !== 'undefined' && value !== null).map(([field]) => field).join(', ')); if (!profile) { assert.fail('profile not found'); } await Promise.all(actor.fields.map(async (field) => { assert.ok( validators[field] ? await validators[field](profile[field]) : typeof profile[field] !== 'undefined', `broken field ${field}, got ${profile[field]}`, ); })); }))); }); }, Promise.resolve()); await knex.destroy(); } init();