Compare commits

..

No commits in common. "33bad4466e1636e4a753035cdab012599208ef11" and "958c6d83fafa6602081dfa40b85bc4cb6608f050" have entirely different histories.

16 changed files with 253 additions and 291 deletions

View File

@ -189,8 +189,12 @@ module.exports = {
'hotcrazymess', 'hotcrazymess',
'thatsitcomshow', 'thatsitcomshow',
], ],
[
// Adult DVD Empire
'elegantangel',
'westcoastproductions',
],
'21sextury', '21sextury',
'adultempire',
'julesjordan', 'julesjordan',
'dorcelclub', 'dorcelclub',
'bang', 'bang',

View File

@ -1,65 +0,0 @@
const config = require('config');
exports.up = async (knex) => {
await knex.schema.alterTable('entities', (table) => {
// internal options, as opposed to parameters for scraper options
table.json('options');
});
await knex.schema.alterTable('releases', (table) => {
table.dropForeign('entity_id');
table.foreign('entity_id')
.references('id')
.inTable('entities')
.onDelete('cascade');
});
await knex.schema.alterTable('releases_caps', (table) => {
table.unique(['release_id', 'media_id']);
});
await knex.schema.createTable('movies_tags', (table) => {
table.integer('tag_id')
.references('id')
.inTable('tags');
table.integer('movie_id')
.notNullable()
.references('id')
.inTable('movies')
.onDelete('cascade');
table.text('original_tag');
table.text('source')
.defaultTo('scraper');
table.unique(['tag_id', 'movie_id']);
});
await knex.raw('GRANT ALL ON ALL TABLES IN SCHEMA public TO :visitor;', {
visitor: knex.raw(config.database.query.user),
});
};
exports.down = async (knex) => {
await knex.schema.alterTable('entities', (table) => {
table.dropColumn('options');
});
await knex.schema.alterTable('releases', (table) => {
table.dropForeign('entity_id');
table.foreign('entity_id')
.references('id')
.inTable('entities')
.onDelete('no action');
});
await knex.schema.alterTable('releases_caps', (table) => {
table.dropUnique(['release_id', 'media_id']);
});
await knex.schema.dropTable('movies_tags');
};

29
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "traxxx", "name": "traxxx",
"version": "1.239.0", "version": "1.238.8",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "traxxx", "name": "traxxx",
"version": "1.239.0", "version": "1.238.8",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.458.0", "@aws-sdk/client-s3": "^3.458.0",
@ -47,7 +47,7 @@
"express-session": "^1.17.3", "express-session": "^1.17.3",
"face-api.js": "^0.22.2", "face-api.js": "^0.22.2",
"file-type": "^18.7.0", "file-type": "^18.7.0",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.2",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"graphile-build": "^4.14.0", "graphile-build": "^4.14.0",
"graphile-utils": "^4.14.0", "graphile-utils": "^4.14.0",
@ -88,7 +88,7 @@
"tunnel": "0.0.6", "tunnel": "0.0.6",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"undici": "^5.28.1", "undici": "^5.28.1",
"unprint": "^0.11.8", "unprint": "^0.11.5",
"url-pattern": "^1.0.3", "url-pattern": "^1.0.3",
"v-tooltip": "^2.1.3", "v-tooltip": "^2.1.3",
"video.js": "^8.6.1", "video.js": "^8.6.1",
@ -9851,22 +9851,17 @@
"integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ=="
}, },
"node_modules/fluent-ffmpeg": { "node_modules/fluent-ffmpeg": {
"version": "2.1.3", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz",
"integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==", "integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==",
"dependencies": { "dependencies": {
"async": "^0.2.9", "async": ">=0.2.9",
"which": "^1.1.1" "which": "^1.1.1"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=0.8.0"
} }
}, },
"node_modules/fluent-ffmpeg/node_modules/async": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
},
"node_modules/fluent-ffmpeg/node_modules/which": { "node_modules/fluent-ffmpeg/node_modules/which": {
"version": "1.3.1", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@ -18298,9 +18293,9 @@
} }
}, },
"node_modules/unprint": { "node_modules/unprint": {
"version": "0.11.8", "version": "0.11.5",
"resolved": "https://registry.npmjs.org/unprint/-/unprint-0.11.8.tgz", "resolved": "https://registry.npmjs.org/unprint/-/unprint-0.11.5.tgz",
"integrity": "sha512-UCtfdbbHSNS/F0hlFwMa+ZmUqkVdp7V3SZVJjcMNnb0GUKm/7VWjhdvzHe+dIejhRdJykHfXWkI/BCbKwl51Vg==", "integrity": "sha512-tLhiFGeSU40GN12625+9oqmNGDFSToMPME60pB+DSGT9wd9fJM0L/lyZMQeNFmWMSThwa/id/FHAOnN7cE1aOw==",
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",
"bottleneck": "^2.19.5", "bottleneck": "^2.19.5",

View File

@ -1,6 +1,6 @@
{ {
"name": "traxxx", "name": "traxxx",
"version": "1.239.0", "version": "1.238.8",
"description": "All the latest porn releases in one place", "description": "All the latest porn releases in one place",
"main": "src/app.js", "main": "src/app.js",
"scripts": { "scripts": {
@ -106,7 +106,7 @@
"express-session": "^1.17.3", "express-session": "^1.17.3",
"face-api.js": "^0.22.2", "face-api.js": "^0.22.2",
"file-type": "^18.7.0", "file-type": "^18.7.0",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.2",
"fs-extra": "^11.1.1", "fs-extra": "^11.1.1",
"graphile-build": "^4.14.0", "graphile-build": "^4.14.0",
"graphile-utils": "^4.14.0", "graphile-utils": "^4.14.0",
@ -147,7 +147,7 @@
"tunnel": "0.0.6", "tunnel": "0.0.6",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"undici": "^5.28.1", "undici": "^5.28.1",
"unprint": "^0.11.8", "unprint": "^0.11.5",
"url-pattern": "^1.0.3", "url-pattern": "^1.0.3",
"v-tooltip": "^2.1.3", "v-tooltip": "^2.1.3",
"video.js": "^8.6.1", "video.js": "^8.6.1",

View File

@ -1251,10 +1251,6 @@ const tags = [
name: 'voodoo', name: 'voodoo',
slug: 'voodoo', slug: 'voodoo',
}, },
{
name: 'bikini',
slug: 'bikini',
},
]; ];
const aliases = [ const aliases = [
@ -2549,30 +2545,6 @@ const aliases = [
name: 'parasites', name: 'parasites',
for: 'parasite', for: 'parasite',
}, },
{
name: 'threesome - fmm',
for: 'mfm',
},
{
name: '4k ultra hd',
for: '4k',
},
{
name: 'sex toy play',
for: 'toys',
},
{
name: 'cumshots',
for: 'cumshot',
},
{
name: 'bikini babes',
for: 'bikini',
},
{
name: 'threesomes',
for: 'threesome',
},
]; ];
const priorities = [ // higher index is higher priority const priorities = [ // higher index is higher priority

View File

@ -104,12 +104,6 @@ const networks = [
}, },
parent: '21sextury', parent: '21sextury',
}, },
{
slug: 'adultempire',
name: 'Adult Empire',
url: 'https://www.adultempire.com',
type: 'info',
},
{ {
slug: 'adulttime', slug: 'adulttime',
name: 'Adult Time', name: 'Adult Time',

View File

@ -3270,15 +3270,6 @@ const sites = [
slug: 'elegantangel', slug: 'elegantangel',
name: 'Elegant Angel', name: 'Elegant Angel',
url: 'https://www.elegantangel.com', url: 'https://www.elegantangel.com',
options: {
spawn: [
{
parameters: {
latest: 'https://www.elegantangel.com/watch-exclusive-elegant-angel-scenes.html',
},
},
],
},
}, },
// EVIL ANGEL // EVIL ANGEL
{ {
@ -13487,6 +13478,7 @@ const sites = [
tags: ['black-cock'], tags: ['black-cock'],
parameters: { parameters: {
studio: false, studio: false,
layout: 'grid',
}, },
}, },
// WHALE MEMBER // WHALE MEMBER
@ -13721,36 +13713,27 @@ exports.seed = (knex) => Promise.resolve()
.then(async () => { .then(async () => {
await Promise.all(sites.map(async (channel) => { await Promise.all(sites.map(async (channel) => {
if (channel.rename) { if (channel.rename) {
await knex('entities') return knex('entities')
.where({ .where({
type: channel.type || 'channel', type: channel.type || 'channel',
slug: channel.rename, slug: channel.rename,
}) })
.update('slug', channel.slug); .update('slug', channel.slug);
return;
} }
if (channel.delete) { return null;
await knex('entities')
.where({
type: channel.type || 'channel',
slug: channel.slug,
})
.delete();
}
}).filter(Boolean)); }).filter(Boolean));
const networks = await knex('entities') const networks = await knex('entities')
.where('type', 'network') .where('type', 'network')
.orWhereNull('parent_id'); .orWhereNull('parent_id');
const networksMap = networks.filter((network) => !network.delete).reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {}); const networksMap = networks.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
const tags = await knex('tags').select('*').whereNull('alias_for'); const tags = await knex('tags').select('*').whereNull('alias_for');
const tagsMap = tags.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {}); const tagsMap = tags.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
const sitesWithNetworks = sites.filter((site) => !site.delete).map((site) => ({ const sitesWithNetworks = sites.map((site) => ({
slug: site.slug, slug: site.slug,
name: site.name, name: site.name,
type: site.type || 'channel', type: site.type || 'channel',
@ -13758,7 +13741,6 @@ exports.seed = (knex) => Promise.resolve()
description: site.description, description: site.description,
url: site.url, url: site.url,
parameters: site.parameters, parameters: site.parameters,
options: site.options,
parent_id: networksMap[site.parent], parent_id: networksMap[site.parent],
priority: site.priority || 0, priority: site.priority || 0,
independent: !!site.independent, independent: !!site.independent,

View File

@ -410,7 +410,7 @@ async function curateProfile(profile, actor) {
curatedProfile.ethnicity = ethnicities[profile.ethnicity?.trim().toLowerCase()] || null; curatedProfile.ethnicity = ethnicities[profile.ethnicity?.trim().toLowerCase()] || null;
curatedProfile.hairType = profile.hairType?.trim() || null; curatedProfile.hairType = profile.hairType?.trim() || null;
curatedProfile.hairColor = hairColors[(profile.hairColor || profile.hair)?.toLowerCase().replace('hair', '').trim()] || null; curatedProfile.hairColor = hairColors[(profile.hairColor || profile.hair)?.toLowerCase().replace('hair', '').trim()] || null;
curatedProfile.eyes = eyeColors[profile.eyes?.replace(/eyes?/i).trim().toLowerCase()] || null; curatedProfile.eyes = eyeColors[profile.eyes?.trim().toLowerCase()] || null;
curatedProfile.tattoos = profile.tattoos?.trim() || null; curatedProfile.tattoos = profile.tattoos?.trim() || null;
curatedProfile.piercings = profile.piercings?.trim() || null; curatedProfile.piercings = profile.piercings?.trim() || null;
@ -878,7 +878,7 @@ async function scrapeActors(argNames) {
const entitySlugs = sources.flat(); const entitySlugs = sources.flat();
const [entitiesBySlug, existingActorEntries] = await Promise.all([ const [entitiesBySlug, existingActorEntries] = await Promise.all([
fetchEntitiesBySlug(entitySlugs, { types: ['channel', 'network', 'info'] }), fetchEntitiesBySlug(entitySlugs, 'desc'),
knex('actors') knex('actors')
.select(knex.raw('actors.id, actors.name, actors.slug, actors.entry_id, actors.entity_id, row_to_json(entities) as entity')) .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)) .whereIn('actors.slug', baseActors.map((baseActor) => baseActor.slug))

View File

@ -84,7 +84,7 @@ async function fetchScene(scraper, url, entity, baseRelease, options, type = 'sc
} }
if ((type === 'scene' && scraper.scrapeScene) || (type === 'movie' && scraper.scrapeMovie)) { if ((type === 'scene' && scraper.scrapeScene) || (type === 'movie' && scraper.scrapeMovie)) {
if (scraper.useUnprint || (type === 'scene' && scraper.scrapeScene?.unprint) || (type === 'movie' && scraper.scrapeMovie?.unprint)) { if (scraper.useUnprint || scraper.scrapeScene?.unprint || scraper.scrapeMovie?.unprint) {
return fetchUnprintScene(scraper, url, entity, baseRelease, options, type); return fetchUnprintScene(scraper, url, entity, baseRelease, options, type);
} }

View File

@ -55,8 +55,7 @@ function curateEntity(entity, includeParameters = false) {
} }
if (includeParameters) { if (includeParameters) {
curatedEntity.options = entity.options; // global internal options curatedEntity.parameters = entity.parameters;
curatedEntity.parameters = entity.parameters; // scraper-specific parameters
} }
if (entity.children) { if (entity.children) {
@ -67,25 +66,10 @@ function curateEntity(entity, includeParameters = false) {
} }
if (entity.included_children) { if (entity.included_children) {
curatedEntity.includedChildren = entity.included_children.flatMap((child) => { curatedEntity.includedChildren = entity.included_children.map((child) => curateEntity({
const curatedChild = curateEntity({ ...child,
...child, parent: curatedEntity.id ? curatedEntity : null,
parent: curatedEntity.id ? curatedEntity : null, }, includeParameters));
}, includeParameters);
// allow entities to 'spawn' virtual copies of themselves, this is useful for sites that use two separate update pages (i.e. Elegant Angel)
if (child.options?.spawn) {
return [
curatedChild,
...child.options.spawn.map((spawnEntity) => ({
...curatedChild,
...spawnEntity,
})),
];
}
return curatedChild;
});
} }
const scraper = resolveScraper(curatedEntity); const scraper = resolveScraper(curatedEntity);
@ -215,7 +199,7 @@ async function fetchIncludedEntities() {
return curatedNetworks; return curatedNetworks;
} }
async function fetchEntitiesBySlug(entitySlugs, options = { prefer: 'channel' }) { async function fetchEntitiesBySlug(entitySlugs, prefer = 'channel') {
const entities = await knex.raw(` const entities = await knex.raw(`
WITH RECURSIVE entity_tree as ( WITH RECURSIVE entity_tree as (
SELECT to_jsonb(entities) as entity, SELECT to_jsonb(entities) as entity,
@ -224,7 +208,7 @@ async function fetchEntitiesBySlug(entitySlugs, options = { prefer: 'channel' })
FROM entities FROM entities
WHERE (slug = ANY(:entitySlugs) WHERE (slug = ANY(:entitySlugs)
OR url ILIKE ANY(:entityHosts)) OR url ILIKE ANY(:entityHosts))
AND type = ANY(:entityTypes) AND type IN ('channel', 'network')
UNION ALL UNION ALL
@ -252,8 +236,7 @@ async function fetchEntitiesBySlug(entitySlugs, options = { prefer: 'channel' })
`, { `, {
entitySlugs: entitySlugs.filter((slug) => !slug.includes('.')), entitySlugs: entitySlugs.filter((slug) => !slug.includes('.')),
entityHosts: entitySlugs.filter((slug) => slug.includes('.')).map((hostname) => `%${hostname}`), entityHosts: entitySlugs.filter((slug) => slug.includes('.')).map((hostname) => `%${hostname}`),
entityTypes: options.types || ['channel', 'network'], sort: knex.raw(prefer === 'channel' ? 'asc' : 'desc'),
sort: knex.raw(options.prefer === 'channel' ? 'asc' : 'desc'),
}); });
// channel entity will overwrite network entity // channel entity will overwrite network entity
@ -280,7 +263,7 @@ async function fetchReleaseEntities(baseReleases) {
.filter(Boolean), .filter(Boolean),
)); ));
return fetchEntitiesBySlug(entitySlugs, { prefer: argv.prefer || 'network' }); return fetchEntitiesBySlug(entitySlugs, argv.prefer || 'network');
} }
async function fetchEntity(entityId, type) { async function fetchEntity(entityId, type) {

View File

@ -648,12 +648,7 @@ streamQueue.define('fetchStreamSource', async ({ source, tempFileTarget, hashStr
.format('mp4') .format('mp4')
.outputOptions(['-movflags frag_keyframe+empty_moov']) .outputOptions(['-movflags frag_keyframe+empty_moov'])
.on('start', (cmd) => logger.verbose(`Fetching stream from ${source.stream} with "${cmd}"`)) .on('start', (cmd) => logger.verbose(`Fetching stream from ${source.stream} with "${cmd}"`))
.on('error', (error) => { .on('error', (error) => logger.error(`Failed to fetch stream from ${source.stream}: ${error.message}`))
logger.error(`Failed to fetch stream from ${source.stream}: ${error.message}`);
hashStream.end();
tempFileTarget.end();
})
.pipe(); .pipe();
// await pipeline(video, hashStream, tempFileTarget); // await pipeline(video, hashStream, tempFileTarget);

View File

@ -1,61 +1,97 @@
'use strict'; 'use strict';
const unprint = require('unprint'); const qu = require('../utils/qu');
const http = require('../utils/http'); const http = require('../utils/http');
const slugify = require('../utils/slugify'); const slugify = require('../utils/slugify');
const { feetInchesToCm, lbsToKg } = require('../utils/convert'); const { feetInchesToCm, lbsToKg } = require('../utils/convert');
function scrapeAll(scenes, channel, _options) { async function getPhotos(entryId, channel) {
const res = await http.get(`${channel.url}/Membership/GetScreenshots?sceneID=scene_${entryId}`);
if (res.ok) {
return res.body.split(/[\s,]+/).filter(Boolean);
}
return [];
}
function scrapeAllTour(scenes, channel) {
return scenes.map(({ query }) => { return scenes.map(({ query }) => {
const release = {}; const release = {};
release.url = query.url('a.scene-title, a.scene-img', { origin: channel.url }); release.url = query.url('.scene-update-details, .feature-update-details', 'href', { origin: channel.url });
release.entryId = query.attribute('article[data-scene-id]', 'data-scene-id') || new URL(release.url).pathname.match(/^\/(\d+)/)?.[1]; release.entryId = new URL(release.url).pathname.match(/\/(\d+)/)[1];
release.title = query.content('.scene-title')?.trim(); release.title = query.q('.scene-img-wrapper img', 'alt').replace(/\s*image$/i, '');
release.duration = query.duration('.scene-length');
release.actors = query.content('.scene-performer-names')?.split(/[,&]/).map((actor) => actor.trim()); release.date = query.date('.scene-update-stats span, .feature-update-details span', 'MMM DD, YYYY');
release.actors = query.cnt('.scene-update-details h3, .feature-update-details h2')?.split(/\s*\|\s*/).map((actor) => actor.trim());
release.poster = query.sourceSet('.screenshot', 'data-srcset'); const poster = query.img('.scene-img-wrapper img');
release.poster = [
poster.replace(/\/res\/\d+/, '/res/1920'),
poster.replace(/\/res\/\d+/, '/res/1600'),
poster,
];
const sceneId = query.attribute('article[data-scene-id]', 'data-scene-id'); release.trailer = { src: query.video('.scene-img-wrapper source') };
const masterId = query.attribute('article[data-master-id]', 'data-master-id');
if (sceneId && masterId) {
release.teaser = `https://video.adultempire.com/hls/previewscene/${masterId}/${sceneId}/index-f1-v1.m3u8`;
}
return release; return release;
}); });
} }
const photoRegex = /(\/\w\/\d+\/)\d+/; async function scrapeAllGrid(scenes, channel, options) {
return Promise.all(scenes.map(async ({ query, el }) => {
const release = {};
const uri = query.url('.grid-item-title') || query.url('a.animated-screen');
async function scrapeRelease({ query, html, element }, { url, entity, baseRelease, parameters }) { release.entryId = el.id.match(/\d+/)?.[0] || uri.match(/^(\d+)\//)?.[1];
release.title = query.cnt('.grid-item-title');
release.url = qu.prefixUrl(uri, channel.url);
release.poster = query.img('.screenshot');
if (options.includePhotos) {
release.photos = await getPhotos(release.entryId, channel);
}
return release;
}));
}
function scrapeMovieScenes(scenes) {
return scenes.map(({ query }) => {
const release = {};
release.title = query.cnt('.scene-title a');
release.url = query.url('.scene-title a', 'href', { origin: 'https://www.elegantangel.com' });
release.entryId = new URL(release.url).pathname.match(/\/(\d+)/)[1];
release.duration = query.number('.scene-length') * 60;
release.actors = query.cnts('.scene-cast-list a');
release.poster = query.img('a img');
return release;
});
}
async function scrapeRelease({ query, html }, url, channel, baseRelease, options) {
const release = {}; const release = {};
const type = query.exists('.scene-list-header') ? 'movie' : 'scene'; const type = query.exists('.scene-list-header') ? 'movie' : 'scene';
release.entryId = new URL(url).pathname.match(/\/(\d+)/)[1]; release.entryId = new URL(url).pathname.match(/\/(\d+)/)[1];
const title = query.content('.scene-page .description, .video-page .description'); release.title = query.cnt('.scene-page .description, .video-page .description');
if (/^scene \d+$/i.test(title)) {
release.sceneIndex = unprint.extractNumber(title);
} else {
release.title = title;
}
release.date = query.date('.release-date:first-child', 'MMM DD, YYYY', /\w{3} \d{2}, \d{4}/); release.date = query.date('.release-date:first-child', 'MMM DD, YYYY', /\w{3} \d{2}, \d{4}/);
release.duration = query.duration('.release-date:last-child');
release.actors = query.all('.video-performer').map((el) => { release.actors = query.all('.video-performer').map((el) => {
const avatar = unprint.query.img(el, 'img', 'data-bgsrc'); const avatar = qu.query.img(el, 'img', 'data-bgsrc');
return { return {
name: unprint.query.content(el, 'span').trim(), name: qu.query.cnt(el, 'span'),
url: unprint.query.url(el, 'a', { origin: entity.url }), url: qu.query.url(el, 'a', 'href', { origin: channel.url }),
avatar: [ avatar: [
avatar.replace(/\/actor\/\d+/, '/actor/1600'), avatar.replace(/\/actor\/\d+/, '/actor/1600'),
avatar, avatar,
@ -63,8 +99,8 @@ async function scrapeRelease({ query, html, element }, { url, entity, baseReleas
}; };
}); });
release.tags = query.contents('.tags a, .categories a'); release.tags = query.cnts('.tags a, .categories a');
release.studio = parameters?.studio === false ? null : slugify(query.content('.studio span:last-child, .studio a'), ''); release.studio = options?.parameters.studio === false ? null : slugify(query.cnt('.studio span:last-child'), '');
if (type === 'scene') { if (type === 'scene') {
release.director = query.text('.director'); release.director = query.text('.director');
@ -73,44 +109,87 @@ async function scrapeRelease({ query, html, element }, { url, entity, baseReleas
} }
if (type === 'movie') { if (type === 'movie') {
release.director = query.content('.director a'); release.director = query.cnt('.director a');
release.covers = [query.sourceSet('.carousel-item .boxcover-image', 'data-srcset')]; release.covers = query.imgs('.carousel-item > img');
release.scenes = scrapeAll(unprint.initAll(element, '#scenes .grid-item'), entity); release.scenes = scrapeMovieScenes(qu.initAll(query.all('#scenes .grid-item')), channel);
} }
if (query.exists('.video-title .movie-title')) { if (query.exists('.video-title .movie-title')) {
release.movie = { release.movie = {
title: query.content('#viewLargeBoxcover .modal-title a'), title: query.cnt('#viewLargeBoxcover .modal-title a'),
url: query.url('#viewLargeBoxcover .modal-title a', 'href', { origin: entity.url }), url: query.url('#viewLargeBoxcover .modal-title a', 'href', { origin: channel.url }),
entryId: query.url('#viewLargeBoxcover .modal-title a')?.match(/(\d+)\//)[1], entryId: query.url('#viewLargeBoxcover .modal-title a')?.match(/(\d+)\//)[1],
covers: query.imgs('#viewLargeBoxcover #viewLargeBoxcoverCarousel .carousel-item > img'), covers: query.imgs('#viewLargeBoxcover #viewLargeBoxcoverCarousel .carousel-item > img'),
}; };
} }
release.caps = query.imgs('#dv_frames a > img', { attribute: 'data-src' }).map((photo) => [ release.photos = query.imgs('#dv_frames a > img').map((photo) => [
photo.replace(photoRegex, (match, path) => `${path}1920`), photo.replace(/(\/p\/\d+\/)\d+/, (match, path) => `${path}1920`),
photo.replace(photoRegex, (match, path) => `${path}1280`), photo.replace(/(\/p\/\d+\/)\d+/, (match, path) => `${path}1600`),
photo, photo,
]); ]);
const trailerId = html.match(/item: (\d+),/)?.[1]; const trailerId = html.match(/item: (\d+),/)?.[1];
if (trailerId) { if (trailerId) {
release.trailer = `https://trailer.adultempire.com/hls/trailer/${trailerId}/master.m3u8`; const trailerUrl = `https://www.adultempire.com/videoEmbed/${trailerId}?type=preview`;
} const trailerRes = await qu.get(trailerUrl);
if (query.exists('.user-actions .btn-4k')) { if (trailerRes.ok) {
release.qualities = [2160]; const stream = trailerRes.item.query.video();
release.trailer = { stream };
}
} }
return release; return release;
} }
async function scrapeProfile({ query }) { function scrapeMovies(movies, channel) {
return movies.map(({ query }) => {
const release = {};
release.url = query.url('.boxcover', 'href', { origin: channel.url });
release.entryId = new URL(release.url).pathname.match(/\/(\d+)/)[1];
release.title = query.cnt('span');
const cover = query.img('picture img');
release.covers = [
// filename is ignored, back-cover has suffix after media ID
cover.replace('_sq.jpg', '/front.jpg').replace(/\/product\/\d+/, '/product/500'),
cover.replace('_sq.jpg', 'b/back.jpg').replace(/\/product\/\d+/, '/product/500'),
];
return release;
});
}
function scrapeActorScenes(scenes, channel) {
return scenes.map(({ query }) => {
const release = {};
release.url = query.url('a', 'href', { origin: channel.url });
release.entryId = new URL(release.url).pathname.match(/\/(\d+)/)[1];
release.title = query.cnt('.grid-item-title');
const poster = query.img('a img');
release.poster = [
poster.replace(/\/\d+\//, '/1600/'),
poster,
];
return release;
});
}
async function scrapeProfile({ query }, url, channel, include) {
const profile = {}; const profile = {};
const bio = query.contents('#profileModal .well li').reduce((acc, info) => { const bio = query.cnts('.performer-page-header li').reduce((acc, info) => {
const [key, value] = info.split(':'); const [key, value] = info.split(':');
return { return {
@ -119,14 +198,11 @@ async function scrapeProfile({ query }) {
}; };
}, {}); }, {});
const bioText = query.content('#profileModal .well'); const measurements = bio.meas?.match(/(\d+)(\w+)-(\d+)-(\d+)/);
profile.description = query.content('#profileModal .modal-body') if (measurements) {
.slice(bioText.length) [profile.bust, profile.cup, profile.waist, profile.hip] = measurements.slice(1);
.replace(/Biography Text ©Adult DVD Empire/i, '') }
.trim();
profile.measurements = bio.measurements?.replace(/["\s]+/g, '');
profile.hair = bio.hair; profile.hair = bio.hair;
profile.eyes = bio.eyes; profile.eyes = bio.eyes;
@ -135,41 +211,79 @@ async function scrapeProfile({ query }) {
profile.height = feetInchesToCm(bio.height); profile.height = feetInchesToCm(bio.height);
profile.weight = lbsToKg(bio.weight); profile.weight = lbsToKg(bio.weight);
const avatar = query.img('picture img, .performer-image-container img'); profile.avatar = query.img('picture img');
if (avatar) { if (include) {
profile.avatar = [ const actorId = new URL(url).pathname.match(/\/(\d+)/)[1];
avatar const res = await qu.getAll(`${channel.url}/www.elegantangel.com/streaming-video-by-scene.html?cast=${actorId}`, '.grid-item', null, {
.replace('_bust', '_body') rejectUnauthorized: false,
.replace(/\/actor\/\d+\//i, '/actor/1000/'), });
avatar,
]; if (res.ok) {
profile.releases = scrapeActorScenes(res.items, channel);
}
} }
return profile; return profile;
} }
async function fetchLatest(channel, page, options) { async function fetchLatestTour(channel, page = 1) {
// const res = await qu.getAll(`${channel.url}/watch-newest-clips-and-scenes.html?page=${page}&hybridview=member`, '.item-grid-scene .grid-item'); const url = `${channel.url}/tour?page=${page}`;
const res = await unprint.get(options.parameters?.latest const res = await qu.getAll(url, '.scene-update', null, {
? `${options.parameters.latest}?page=${page}&view=grid` // invalid certificate
: `${channel.url}/watch-newest-clips-and-scenes.html?page=${page}&view=grid`, { selectAll: '.item-grid-scene .grid-item' }); rejectUnauthorized: false,
});
if (res.ok) { if (res.ok) {
return scrapeAll(res.context, channel, options); return scrapeAllTour(res.items, channel);
} }
return res.status; return res.status;
} }
async function fetchProfilePage(actorUrl) { async function fetchLatestGrid(channel, page, options) {
const res = await unprint.get(actorUrl, { const res = await qu.getAll(`${channel.url}/watch-newest-clips-and-scenes.html?page=${page}&hybridview=member`, '.item-grid-scene .grid-item');
select: '#content',
if (res.ok) {
return scrapeAllGrid(res.items, channel, options);
}
return res.status;
}
async function fetchMovie(url, channel, baseRelease, options) {
const res = await qu.get(url, null, null, {
// invalid certificate
rejectUnauthorized: false, rejectUnauthorized: false,
}); });
if (res.ok) { if (res.ok) {
return scrapeProfile(res.context); return scrapeRelease(res.item, url, channel, baseRelease, options);
}
return res.status;
}
async function fetchMovies(channel, page = 1) {
const res = await qu.getAll(`https://www.elegantangel.com/streaming-elegant-angel-dvds-on-video.html?page=${page}`, '.grid-item', null, {
// invalid certificate
rejectUnauthorized: false,
});
if (res.ok) {
return scrapeMovies(res.items, channel);
}
return res.status;
}
async function fetchProfilePage(actorUrl, channel, include) {
const res = await qu.get(actorUrl, '.performer-page', null, {
rejectUnauthorized: false,
});
if (res.ok) {
return scrapeProfile(res.item, actorUrl, channel, include);
} }
return res.status; return res.status;
@ -184,15 +298,13 @@ async function fetchProfile(baseActor, channel, include) {
} }
} }
const searchRes = await http.get(`https://www.adultempire.com/search/SearchAutoComplete_Agg_EmpireDTRank?search_type=Pornstars&rows=9&name_startsWith=${slugify(baseActor.name, '+')}`); const searchRes = await http.get(`${channel.url}/search/SearchAutoComplete_Agg_ByMedia?rows=9&name_startsWith=${slugify(baseActor.name, '+')}`);
if (searchRes.ok && searchRes.body.Results) { if (searchRes.ok) {
const actorResult = searchRes.body.Results.find((result) => /performer/i.test(result.BasicResponseGroup?.displaytype) && new RegExp(baseActor.name, 'i').test(result.BasicResponseGroup?.description)); const actorResult = searchRes.body.Results.find((result) => /performer/i.test(result.BasicResponseGroup?.displaytype) && new RegExp(baseActor.name, 'i').test(result.BasicResponseGroup?.description));
if (actorResult) { if (actorResult) {
const url = `https://www.adultempire.com/${actorResult.BasicResponseGroup.id}`; return fetchProfilePage(`${channel.url}${actorResult.BasicResponseGroup.id}`, channel, include);
return fetchProfilePage(url);
} }
return null; return null;
@ -202,15 +314,16 @@ async function fetchProfile(baseActor, channel, include) {
} }
module.exports = { module.exports = {
fetchLatest, fetchLatest: fetchLatestTour,
// fetchMovies, fetchMovies,
fetchMovie,
fetchProfile, fetchProfile,
scrapeScene: { scrapeScene: scrapeRelease,
scraper: scrapeRelease, scrapeMovie: scrapeRelease,
unprint: true, grid: {
}, fetchLatest: fetchLatestGrid,
scrapeMovie: { scrapeScene: scrapeRelease,
scraper: scrapeRelease, fetchMovie,
unprint: true, fetchProfile,
}, },
}; };

View File

@ -3,10 +3,6 @@
const scrapers = require('./scrapers'); const scrapers = require('./scrapers');
function resolveScraper(entity) { function resolveScraper(entity) {
if (entity.parameters?.useScraper && scrapers.releases[entity.parameters.useScraper]) {
return scrapers.releases[entity.parameters.useScraper];
}
if (scrapers.releases[entity.slug]) { if (scrapers.releases[entity.slug]) {
return scrapers.releases[entity.slug]; return scrapers.releases[entity.slug];
} }

View File

@ -177,7 +177,6 @@ const scrapers = {
actors: { actors: {
'18vr': badoink, '18vr': badoink,
'21sextury': gamma, '21sextury': gamma,
adultempire,
allanal: mikeadriano, allanal: mikeadriano,
amateureuro: porndoe, amateureuro: porndoe,
americanpornstar, americanpornstar,
@ -218,6 +217,7 @@ const scrapers = {
dorcelclub: dorcel, dorcelclub: dorcel,
doubleviewcasting: firstanalquest, doubleviewcasting: firstanalquest,
dtfsluts: fullpornnetwork, dtfsluts: fullpornnetwork,
elegantangel: adultempire,
evilangel: gamma, evilangel: gamma,
exploitedcollegegirls: elevatedx, exploitedcollegegirls: elevatedx,
eyeontheguy: hush, eyeontheguy: hush,
@ -323,6 +323,7 @@ const scrapers = {
vixen, vixen,
vrcosplayx: badoink, vrcosplayx: badoink,
wankzvr, wankzvr,
westcoastproductions: adultempire,
wicked: gamma, wicked: gamma,
wildoncam: cherrypimps, wildoncam: cherrypimps,
xempire: gamma, xempire: gamma,

View File

@ -288,28 +288,23 @@ async function associateMovieScenes(movies, movieScenes) {
}, },
}), {}); }), {});
const associations = movieScenes const associations = movieScenes.map((scene) => {
.toSorted((sceneA, sceneB) => { if (!scene.movie) {
return (sceneA.sceneIndex || 1) - (sceneB.sceneIndex || 1);
})
.map((scene) => {
if (!scene.movie) {
return null;
}
const sceneMovie = moviesByEntityIdAndEntryId[scene.entity.id]?.[scene.movie.entryId]
|| moviesByEntityIdAndEntryId[scene.entity.parent?.id]?.[scene.movie.entryId];
if (sceneMovie?.id) {
return {
movie_id: sceneMovie.id,
scene_id: scene.id,
};
}
return null; return null;
}) }
.filter(Boolean);
const sceneMovie = moviesByEntityIdAndEntryId[scene.entity.id]?.[scene.movie.entryId]
|| moviesByEntityIdAndEntryId[scene.entity.parent?.id]?.[scene.movie.entryId];
if (sceneMovie?.id) {
return {
movie_id: sceneMovie.id,
scene_id: scene.id,
};
}
return null;
}).filter(Boolean);
await bulkInsert('movies_scenes', associations, false); await bulkInsert('movies_scenes', associations, false);
} }
@ -359,7 +354,6 @@ async function storeMovies(movies, useBatchId) {
await updateMovieSearch(moviesWithId.map((movie) => movie.id)); await updateMovieSearch(moviesWithId.map((movie) => movie.id));
await associateReleaseMedia(moviesWithId, 'movie'); await associateReleaseMedia(moviesWithId, 'movie');
await associateReleaseTags(moviesWithId, 'movie');
return moviesWithId; return moviesWithId;
} }

View File

@ -298,8 +298,6 @@ async function scrapeNetworkParallel(networkEntity) {
async function fetchUpdates() { async function fetchUpdates() {
const includedNetworks = await fetchIncludedEntities(); const includedNetworks = await fetchIncludedEntities();
// console.log(includedNetworks[0]);
const scrapedNetworks = await Promise.map( const scrapedNetworks = await Promise.map(
includedNetworks, includedNetworks,
async (networkEntity) => (networkEntity.parameters?.sequential async (networkEntity) => (networkEntity.parameters?.sequential