Added Virtual Taboo (including OnlyTarts).

This commit is contained in:
DebaucheryLibrarian 2025-02-24 02:53:46 +01:00
parent a114211e87
commit 06f9efa492
4 changed files with 180 additions and 5 deletions

View File

@ -15182,6 +15182,13 @@ const sites = [
tags: ['cheating', 'family'],
parent: 'nubiles',
},
{
slug: 'realitysis',
name: 'Reality Sis',
url: 'https://www.realitysis.com',
tags: ['family'],
parent: 'nubiles',
},
// PASCALS SUBSLUTS
{
slug: 'pascalssubsluts',
@ -20511,30 +20518,31 @@ const sites = [
{
slug: 'virtualtaboo',
name: 'Virtual Taboo',
url: 'https://www.virtualtaboo.com',
url: 'https://virtualtaboo.com',
tags: ['vr'],
parent: 'virtualtaboo',
parameters: {
latest: '/videos',
actor: '/pornstars',
},
},
{
slug: 'onlytarts',
name: 'OnlyTarts',
url: 'https://www.onlytarts.com',
url: 'https://onlytarts.com',
parent: 'virtualtaboo',
},
{
slug: 'oopsfamily',
name: 'Oops Family',
url: 'https://www.oopsfamily.com',
url: 'https://oopsfamily.com',
tags: ['family'],
parent: 'virtualtaboo',
},
{
slug: 'darkroomvr',
name: 'Dark Room VR',
url: 'https://www.darkroomvr.com',
url: 'https://darkroomvr.com',
tags: ['vr'],
parent: 'virtualtaboo',
},

View File

@ -775,7 +775,7 @@ async function scrapeActors(argNames) {
const entitySlugs = sources.flat();
const [entitiesBySlug, existingActorEntries] = await Promise.all([
fetchEntitiesBySlug(entitySlugs, { types: ['channel', 'network', 'info'] }),
fetchEntitiesBySlug(entitySlugs, { types: ['channel', 'network', 'info'], prefer: argv.prefer }),
knex('actors')
.select(knex.raw('actors.id, actors.name, actors.slug, actors.entry_id, actors.entity_id, row_to_json(entities) as entity'))
.whereIn('actors.slug', baseActors.map((baseActor) => baseActor.slug))

View File

@ -353,6 +353,10 @@ const scrapers = {
tushyraw: vixen,
twistys: aylo,
vipsexvault: porndoe,
virtualtaboo,
darkroomvr: virtualtaboo,
onlytarts: virtualtaboo,
oopsfamily: virtualtaboo,
vixen,
vrcosplayx: badoink,
wankzvr,

163
src/scrapers/virtualtaboo.js Executable file
View File

@ -0,0 +1,163 @@
'use strict';
const unprint = require('unprint');
const slugify = require('../utils/slugify');
function scrapeAll(scenes) {
return scenes.map(({ query }) => {
const release = {};
release.url = query.url('a.image-container, a.video-card__title') || query.url(null);
release.entryId = new URL(release.url).pathname.match(/\/videos?\/([\w-]+)/)[1];
release.title = query.content('.video-card__title');
release.duration = query.duration('.video-card__quality');
release.actors = query.exists('.video-card__actors a')
? query.all('.video-card__actors a').map((actorEl) => ({
name: unprint.query.content(actorEl),
url: unprint.query.url(actorEl, null),
}))
: query.content('.video-card__actors')?.split(',').map((actor) => actor.trim());
release.poster = query.img('.image-container img');
release.teaser = query.video('.video-card__trailer');
return release;
});
}
function getPhotos(query) {
const teaserPhotos = query.urls('.video-detail__gallery a[href*="//static"], .gallery-item-container a[href*="//static"]');
const galleryMore = query.number('.video-detail__gallery-item--more, .video-detail__gallery-item-more');
const galleryUrl = /\/(img_)?\d{3}\.jpg/.test(teaserPhotos[0]) && teaserPhotos[0];
// no incremental URL found, return original links
if (!galleryMore || !galleryUrl) {
return teaserPhotos;
}
return Array.from({
length: teaserPhotos.length + galleryMore + 1, // + number seems to be off by one
}, (_value, index) => galleryUrl.replace(/\d+\.jpg/, `${String(index + 1).padStart(3, '0')}.jpg`));
}
function getTrailer({ query, window }) {
if (query.exists('.download-pane__list, .download-list')) {
// Dark Room VR
return query.all('.download-pane__item-container, .download-list__item-container').map((videoEl) => ({
src: unprint.query.url(videoEl, '.download-pane__item, .download-list__item'),
quality: unprint.query.number(videoEl, '.download-pane__item, .download-list__item', { match: /\d+×(\d+)/, matchIndex: 1 }),
vr: true, // only used on VR sites
expectType: {
'application/octet-stream': 'video/mp4',
},
}));
}
try {
const trailerData = window.eval('coreSettings')?.sources?.standard?.h264;
return trailerData
.filter((source) => source.quality !== 'auto')
.map((source) => ({
src: source.fallback, // main url doesn't seem to return plausible video files
quality: Number(source.label.match(/\d+\s*x\s*(\d+)/)?.[1]) || null,
}));
} catch (error) {
console.log(error);
// no data variable
}
return null;
}
function scrapeScene({ query, window }, { url }) {
const release = {};
release.entryId = new URL(url).pathname.match(/\/videos?\/([\w-]+)/)[1];
release.title = query.content('.right-info h1, .video-detail__title');
release.description = query.text('.video-detail__description p, .description p');
release.date = query.date('.video-info__time, .info', 'DD MMMM, YYYY', { match: /\d{1,2} \w+, \d{4}/ });
release.duration = query.duration('.video-info__time, .info');
release.actors = query.all('.video-detail__desktop-sidebar .video-info__text a[href*="/model"], .right-info .info a[href*="/pornstars"]').map((actorEl) => ({
name: unprint.query.content(actorEl),
url: unprint.query.url(actorEl, null),
}));
release.tags = query.contents('.tag-list a, .tags a');
// release.poster = query.sourceSet('.image-container img') || query.background('.xp-poster');
release.poster = query.img(['meta[property="og:image"]', 'meta[property="twitter:image"'], { attribute: 'content' })
|| query.poster('.video-detail__image-container *[poster]');
release.photos = getPhotos(query);
release.trailer = getTrailer({ query, window });
return release;
}
function scrapeProfile({ query }) {
const profile = {};
const bioKeys = query.contents('.pornstar-detail__params--top strong, .actor-detail__param-name');
const bioValues = query.exists('.actor-detail__param-value')
? query.contents('.actor-detail__param-value')
: query.text('.pornstar-detail__params--top', { join: false })?.map((text) => text.split('•')[0].replace(':', '').trim());
const bio = Object.fromEntries(bioKeys.map((key, index) => [slugify(key, '_'), bioValues[index]]));
const tags = query.contents('.actor-detail__tags a').map((tag) => slugify(tag, '_'));
profile.description = query.content('.pornstar-detail__description, .actor-detail__description') || null;
profile.birthPlace = query.content('.pornstar-detail__info span, .actor-detail__info-value')?.split(',')[0].trim();
profile.dateOfBirth = unprint.extractDate(bio.birthday, 'MMM D, YYYY');
profile.measurements = bio.measurements;
profile.height = unprint.extractNumber(bio.height);
profile.weight = unprint.extractNumber(bio.weight);
profile.naturalBoobs = tags.includes('natural_tits') ? true : null; // seemingly no tag for fake tits
profile.hasTattoos = tags.includes('no_tattoos') ? false : null;
profile.avatar = query.img('img.pornstar-detail__picture, .actor-detail__picture img');
return profile;
}
async function fetchLatest(channel, page = 1, { parameters }) {
const url = `${channel.url}${parameters.latest || '/video'}?page=${page}`;
const res = await unprint.get(url, { selectAll: '.video-card__item' });
if (res.ok) {
return scrapeAll(res.context, channel);
}
return res.status;
}
async function fetchProfile({ name: actorName }, { entity, parameters }) {
const url = `${entity.url}${parameters.actor || '/model'}/${slugify(actorName, '-')}`;
const res = await unprint.get(url);
if (res.ok) {
return scrapeProfile(res.context, entity);
}
return res.status;
}
module.exports = {
fetchLatest,
fetchProfile,
scrapeScene: {
scraper: scrapeScene,
parser: {
runScripts: 'dangerously',
},
},
};