Compare commits
85 Commits
db2e5b2da4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e227a4ea5 | ||
|
|
1c8df6415d | ||
|
|
0beb54312a | ||
|
|
95d68fa966 | ||
|
|
50e7b1a437 | ||
|
|
6cad394e88 | ||
|
|
186f9660c3 | ||
|
|
ef7c24ab47 | ||
|
|
1b6aaafe10 | ||
|
|
31aa1118e7 | ||
|
|
74d03b7483 | ||
|
|
40ea7eb80a | ||
|
|
0d30115ad5 | ||
|
|
0560fac1ff | ||
|
|
108bf3b168 | ||
|
|
155e235246 | ||
|
|
bff665c6ec | ||
|
|
c7111329dc | ||
|
|
d7c1c0ae5c | ||
|
|
ea298d7edb | ||
|
|
99dfcae920 | ||
|
|
24cba1e1fa | ||
|
|
076bdad310 | ||
|
|
d432d291dd | ||
|
|
220f7e787d | ||
|
|
f1caa77e4b | ||
|
|
ff633436cb | ||
|
|
6860072a51 | ||
|
|
2c7b4cfc22 | ||
|
|
7d9e1be8d4 | ||
|
|
00db4b1b5b | ||
|
|
9f1cf1575a | ||
|
|
4f13e4ed28 | ||
|
|
9805aa7b5b | ||
|
|
0cc6ebc305 | ||
|
|
016c24af28 | ||
|
|
2158550091 | ||
|
|
68ddc8cb78 | ||
|
|
bc5693e44a | ||
|
|
7276d90629 | ||
|
|
1a1af95a10 | ||
|
|
bcb7a56588 | ||
|
|
16648d50f6 | ||
|
|
062dc0e75e | ||
|
|
42effd53fc | ||
|
|
3a3403bb1f | ||
|
|
6fb4989256 | ||
|
|
9750ca4b79 | ||
|
|
0500f7eda8 | ||
|
|
19beff7dbc | ||
|
|
dfe1b84992 | ||
|
|
3d3b544cb4 | ||
|
|
65fa6027ee | ||
|
|
b3a0ba72eb | ||
|
|
f3e2143b45 | ||
|
|
d289f95d3d | ||
|
|
d8b41ec9b5 | ||
|
|
05f7d8b814 | ||
|
|
c2fc09fdaa | ||
|
|
8a7210a3b9 | ||
|
|
e029ca7fd0 | ||
|
|
ffcfae69d5 | ||
|
|
dcaee01ce8 | ||
|
|
7561a4577e | ||
|
|
98b735dbae | ||
|
|
d2daed788c | ||
|
|
23257745a7 | ||
|
|
156954553d | ||
|
|
eb20af14a6 | ||
|
|
ae247c7a91 | ||
|
|
d49e6ef488 | ||
|
|
2b20d98ee0 | ||
|
|
b8cf6a3e71 | ||
|
|
af57f412c9 | ||
|
|
3696b81e69 | ||
|
|
5b6fefd43b | ||
|
|
a863ab888d | ||
|
|
209a81ef71 | ||
|
|
bd91dcbc77 | ||
|
|
b89f25405a | ||
|
|
198f08cb3a | ||
|
|
febaac3865 | ||
|
|
f82167656b | ||
|
|
6e20d7d216 | ||
|
|
612a489cdf |
@@ -27,7 +27,7 @@
|
||||
"require-await": "off",
|
||||
"no-param-reassign": ["error", {
|
||||
"props": true,
|
||||
"ignorePropertyModificationsFor": ["state", "acc", "req"]
|
||||
"ignorePropertyModificationsFor": ["state", "acc", "req", "error"]
|
||||
}]
|
||||
},
|
||||
"globals": {
|
||||
|
||||
@@ -188,6 +188,8 @@ module.exports = {
|
||||
'wishescumtrue',
|
||||
// hentaied
|
||||
'somegore',
|
||||
// digital playground
|
||||
'digitalplayground', // no longer updates, produces a bunch of garbage for some reason
|
||||
],
|
||||
networks: [
|
||||
// dummy network for testing
|
||||
@@ -279,7 +281,7 @@ module.exports = {
|
||||
trailerQuality: [540, 720, 960, 480, 1080, 360, 320, 1440, 1600, 1920, 2160, 270, 240, 180],
|
||||
limit: 25, // max number of photos per release
|
||||
attempts: 2,
|
||||
flushOrphaned: true,
|
||||
flushOrphaned: false,
|
||||
flushWindow: 1000,
|
||||
streams: {
|
||||
enabled: true, // fetch streams
|
||||
|
||||
23
migrations/20260301042453_foot_float.js
Normal file
23
migrations/20260301042453_foot_float.js
Normal file
@@ -0,0 +1,23 @@
|
||||
exports.up = async function(knex) {
|
||||
await knex.schema.alterTable('actors', (table) => {
|
||||
table.decimal('foot')
|
||||
.alter();
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('actors_profiles', (table) => {
|
||||
table.decimal('foot')
|
||||
.alter();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.alterTable('actors', (table) => {
|
||||
table.integer('foot')
|
||||
.alter();
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('actors_profiles', (table) => {
|
||||
table.integer('foot')
|
||||
.alter();
|
||||
});
|
||||
};
|
||||
13
migrations/20260302222545_series_alt_descriptions.js
Normal file
13
migrations/20260302222545_series_alt_descriptions.js
Normal file
@@ -0,0 +1,13 @@
|
||||
exports.up = async function(knex) {
|
||||
await knex.schema.alterTable('series', (table) => {
|
||||
table.specificType('alt_descriptions', 'text ARRAY');
|
||||
table.json('attributes');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.alterTable('series', (table) => {
|
||||
table.dropColumn('alt_descriptions');
|
||||
table.dropColumn('attributes');
|
||||
});
|
||||
};
|
||||
21
migrations/20260304020542_scene_actor_tags.js
Normal file
21
migrations/20260304020542_scene_actor_tags.js
Normal file
@@ -0,0 +1,21 @@
|
||||
exports.up = async function(knex) {
|
||||
await knex.schema.alterTable('releases_tags', (table) => {
|
||||
table.integer('actor_id')
|
||||
.references('id')
|
||||
.inTable('actors');
|
||||
|
||||
table.dropUnique(['tag_id', 'release_id']);
|
||||
});
|
||||
|
||||
await knex.raw('CREATE UNIQUE INDEX releases_tags_tag_id_release_id_actor_id ON releases_tags (tag_id, release_id, COALESCE(actor_id, -1))');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.alterTable('releases_tags', (table) => {
|
||||
table.dropColumn('actor_id');
|
||||
|
||||
table.unique(['tag_id', 'release_id']);
|
||||
});
|
||||
|
||||
await knex.raw('DROP INDEX IF EXISTS releases_tags_tag_id_release_id_actor_id');
|
||||
};
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "traxxx",
|
||||
"version": "1.250.4",
|
||||
"version": "1.250.43",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "traxxx",
|
||||
"version": "1.250.4",
|
||||
"version": "1.250.43",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.458.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "traxxx",
|
||||
"version": "1.250.4",
|
||||
"version": "1.250.43",
|
||||
"description": "All the latest porn releases in one place",
|
||||
"main": "src/app.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -815,6 +815,10 @@ const tags = [
|
||||
description: 'Two men fucking one woman, but not eachother. Typically involves a \'spitroast\', where one guy gets a blowjob and the other fucks her pussy or ass.',
|
||||
group: 'group',
|
||||
},
|
||||
{
|
||||
name: 'spitroast',
|
||||
slug: 'spitroast',
|
||||
},
|
||||
{
|
||||
name: 'military',
|
||||
slug: 'military',
|
||||
@@ -1893,6 +1897,14 @@ const aliases = [
|
||||
name: 'double penetration (dp)',
|
||||
for: 'dp',
|
||||
},
|
||||
{
|
||||
name: 'double penetration ass pussy',
|
||||
for: 'dp',
|
||||
},
|
||||
{
|
||||
name: 'double penetration mouth pussy',
|
||||
for: 'spitroast',
|
||||
},
|
||||
{
|
||||
name: 'double penetration - dp',
|
||||
for: 'dp',
|
||||
@@ -2355,11 +2367,6 @@ const aliases = [
|
||||
name: 'spit',
|
||||
for: 'saliva',
|
||||
},
|
||||
{
|
||||
name: 'spitroast',
|
||||
for: 'mfm',
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
name: 'spoon',
|
||||
for: 'spooning',
|
||||
@@ -3019,11 +3026,13 @@ const priorities = [ // higher index is higher priority
|
||||
['facial', 'swallowing', 'creampie', 'anal-creampie', 'oral-creampie', 'cum-in-mouth', 'throatpie'],
|
||||
['lesbian', 'rough', 'milf', 'male-focus', 'bdsm', 'oil'],
|
||||
['threesome', 'mfm', 'mff', 'trainbang', 'pissing'],
|
||||
['anal', 'bukkake'],
|
||||
['anal', 'bukkake', 'spitroast'],
|
||||
['dp', 'dap', 'triple-penetration', 'tap', 'dvp', 'tvp', 'airtight'],
|
||||
['blowbang', 'orgy'],
|
||||
['gangbang'],
|
||||
['gay', 'transsexual', 'bisexual', 'hentai'],
|
||||
['pissing'],
|
||||
['compilation', 'bts'],
|
||||
].reduce((acc, slugs, index) => {
|
||||
slugs.forEach((slug) => { acc[slug] = index; });
|
||||
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
const omit = require('object.omit');
|
||||
|
||||
const upsert = require('../src/utils/upsert');
|
||||
const redis = require('../src/redis');
|
||||
|
||||
const entityPrefixes = {
|
||||
channel: '',
|
||||
network: '_',
|
||||
studio: '*',
|
||||
info: '@',
|
||||
};
|
||||
|
||||
const grandParentNetworks = [
|
||||
{
|
||||
@@ -59,7 +67,6 @@ const parentNetworks = [
|
||||
url: 'https://www.21sextury.com',
|
||||
description: 'Watch all the latest scenes and porn video updates on 21Sextury.com, the best European porn site with the hottest pornstars from all over the world! Watch porn videos from the large network here.',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
mobile: 'https://m.dpfanatics.com/en/video',
|
||||
},
|
||||
parent: 'gamma',
|
||||
@@ -108,7 +115,6 @@ const parentNetworks = [
|
||||
url: 'https://www.asgmax.com',
|
||||
parent: 'gamma',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
scene: 'https://www.asgmax.com/en/video/asgmax',
|
||||
},
|
||||
},
|
||||
@@ -190,7 +196,6 @@ const networks = [
|
||||
url: 'https://www.21sextreme.com',
|
||||
description: 'Welcome to 21Sextreme.com, your portal to fisting porn, old and young lesbians, horny grannies & extreme BDSM featuring the best Euro & American Pornstars',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
mobile: 'https://m.dpfanatics.com/en/video',
|
||||
},
|
||||
parent: '21sextury',
|
||||
@@ -201,7 +206,6 @@ const networks = [
|
||||
url: 'https://www.21naturals.com',
|
||||
description: 'Welcome to 21Naturals.com, the porn network featuring the hottest pornstars from all over the world in all natural porn and erotic sex videos. Watch thousands of girls with natural tits',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
mobile: 'https://m.dpfanatics.com/en/video',
|
||||
},
|
||||
parent: '21sextury',
|
||||
@@ -231,7 +235,6 @@ const networks = [
|
||||
description: 'Adult Time is a premium streaming service for adults! Watch adult movies, series, and channels from the top names in the industry.',
|
||||
parent: 'gamma',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
referer: 'https://freetour.adulttime.com/en/join',
|
||||
// scene: false,
|
||||
},
|
||||
@@ -323,7 +326,6 @@ const networks = [
|
||||
url: 'https://www.blowpass.com',
|
||||
description: 'Welcome to Blowpass.com, your ultimate source for deepthroat porn, MILF and teen blowjob videos, big cumshots and any and everything oral!',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
referer: 'https://www.blowpass.com',
|
||||
},
|
||||
parent: 'gamma',
|
||||
@@ -376,9 +378,6 @@ const networks = [
|
||||
url: 'https://www.evilangel.com',
|
||||
description: 'Welcome to the award winning Evil Angel website, home to the most popular pornstars of today, yesterday and tomorrow in their most extreme and hardcore porn scenes to date. We feature almost 30 years of rough sex videos and hardcore anal porn like you\'ve never seen before, and have won countless AVN and XBiz awards including \'Best Site\' and \'Best Studio\'.',
|
||||
parent: 'gamma',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'exploitedx',
|
||||
@@ -422,7 +421,6 @@ const networks = [
|
||||
description: 'The world famous Dogfart Interracial series. Online since 1996, we have the largest collection of Interracial videos, pictures and content on the web.',
|
||||
parent: 'dfxtra',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
profileReferer: 'https://www.dogfartnetwork.com',
|
||||
},
|
||||
},
|
||||
@@ -436,9 +434,6 @@ const networks = [
|
||||
slug: 'fantasymassage',
|
||||
name: 'Fantasy Massage',
|
||||
url: 'https://www.fantasymassage.com',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
},
|
||||
parent: 'gamma',
|
||||
},
|
||||
{
|
||||
@@ -448,7 +443,6 @@ const networks = [
|
||||
description: 'Watch and download thousands of the best porn videos at FameDigital.com, the largest porn network on the web! The hottest teens, MILFs and more pornstars are all here!',
|
||||
parameters: {
|
||||
mobile: 'https://m.dpfanatics.com/en/video',
|
||||
layout: 'api',
|
||||
},
|
||||
parent: 'gamma',
|
||||
},
|
||||
@@ -468,7 +462,6 @@ const networks = [
|
||||
url: 'https://www.filthykings.com',
|
||||
parent: 'gamma',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
queryChannel: true,
|
||||
scene: 'https://www.filthykings.com/en/video/filthykings',
|
||||
referer: 'https://www.filthykings.com',
|
||||
@@ -509,9 +502,6 @@ const networks = [
|
||||
name: 'Girlsway',
|
||||
url: 'https://www.girlsway.com',
|
||||
description: 'Girlsway.com has the best lesbian porn videos online! The hottest pornstars & first time lesbians in real girl on girl sex, tribbing, squirting & pussy licking action right HERE!',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
},
|
||||
parent: 'gamma',
|
||||
},
|
||||
{
|
||||
@@ -559,7 +549,6 @@ const networks = [
|
||||
slug: 'jayrock',
|
||||
name: 'JayRock Productions',
|
||||
url: 'http://jayrockcontent.com',
|
||||
parent: 'gamma',
|
||||
},
|
||||
{
|
||||
slug: 'julesjordan',
|
||||
@@ -763,7 +752,6 @@ const networks = [
|
||||
description: 'PureTaboo.com is the ultimate site for family taboo porn, featuring submissive teens & virgins in rough sex videos in ultra 4k HD.',
|
||||
parent: 'gamma',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
scene: 'https://www.puretaboo.com/en/video',
|
||||
referer: 'https://www.puretaboo.com',
|
||||
},
|
||||
@@ -861,7 +849,7 @@ const networks = [
|
||||
description: 'Home of the Kim Kardashian Sex Tape, Porn Parodies, and over 30,000 XXX Movies from The World Leader In Adult Entertainment.',
|
||||
parent: 'gamma',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
sceneMovies: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -887,7 +875,6 @@ const networks = [
|
||||
description: 'XEmpire.com brings you today\'s top pornstars in beautifully shot, HD sex scenes across 4 unique porn sites of gonzo porn, interracial, lesbian & erotica!',
|
||||
parent: 'gamma',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
sceneMovies: false,
|
||||
actorScenes: 'https://www.xempire.com/en/videos/xempire/latest/{page}/All-Categories/0{actorPath}',
|
||||
},
|
||||
@@ -897,9 +884,6 @@ const networks = [
|
||||
name: 'Zero Tolerance',
|
||||
alias: ['ztod'],
|
||||
url: 'https://www.zerotolerancefilms.com',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
},
|
||||
parent: 'gamma',
|
||||
},
|
||||
// ASG MAX
|
||||
@@ -929,80 +913,88 @@ const networks = [
|
||||
},
|
||||
];
|
||||
|
||||
exports.seed = (knex) => Promise.resolve()
|
||||
.then(async () => {
|
||||
await Promise.all([].concat(grandParentNetworks, parentNetworks, networks).map(async (network) => {
|
||||
if (network.rename) {
|
||||
return knex('entities')
|
||||
.where({
|
||||
type: network.type || 'network',
|
||||
slug: network.rename,
|
||||
})
|
||||
.update('slug', network.slug);
|
||||
}
|
||||
exports.seed = async (knex) => {
|
||||
await Promise.all([].concat(grandParentNetworks, parentNetworks, networks).map(async (network) => {
|
||||
if (network.rename) {
|
||||
return knex('entities')
|
||||
.where({
|
||||
type: network.type || 'network',
|
||||
slug: network.rename,
|
||||
})
|
||||
.update('slug', network.slug);
|
||||
}
|
||||
|
||||
return null;
|
||||
}).filter(Boolean));
|
||||
return null;
|
||||
}).filter(Boolean));
|
||||
|
||||
const grandParentNetworkEntries = await upsert('entities', grandParentNetworks.map((network) => (omit({ ...network, type: 'network' }, 'rename'))), ['slug', 'type'], knex);
|
||||
const grandParentNetworksBySlug = [].concat(grandParentNetworkEntries.inserted, grandParentNetworkEntries.updated).reduce((acc, network) => ({ ...acc, [network.slug]: network.id }), {});
|
||||
const grandParentNetworkEntries = await upsert('entities', grandParentNetworks.map((network) => (omit({ ...network, type: 'network' }, 'rename'))), ['slug', 'type'], knex);
|
||||
const grandParentNetworksBySlug = [].concat(grandParentNetworkEntries.inserted, grandParentNetworkEntries.updated).reduce((acc, network) => ({ ...acc, [network.slug]: network.id }), {});
|
||||
|
||||
const parentNetworksWithGrandParent = parentNetworks.map((network) => ({
|
||||
slug: network.slug,
|
||||
name: network.name,
|
||||
type: network.type || 'network',
|
||||
alias: network.alias,
|
||||
url: network.url,
|
||||
description: network.description,
|
||||
has_logo: network.hasLogo ?? true,
|
||||
showcased: typeof network.showcased === 'boolean' ? network.showcased : true,
|
||||
parameters: network.parameters,
|
||||
options: network.options,
|
||||
parent_id: grandParentNetworksBySlug[network.parent] || null,
|
||||
}));
|
||||
const parentNetworksWithGrandParent = parentNetworks.map((network) => ({
|
||||
slug: network.slug,
|
||||
name: network.name,
|
||||
type: network.type || 'network',
|
||||
alias: network.alias,
|
||||
url: network.url,
|
||||
description: network.description,
|
||||
has_logo: network.hasLogo ?? true,
|
||||
showcased: typeof network.showcased === 'boolean' ? network.showcased : true,
|
||||
parameters: network.parameters || null,
|
||||
options: network.options,
|
||||
parent_id: grandParentNetworksBySlug[network.parent] || null,
|
||||
}));
|
||||
|
||||
const parentNetworkEntries = await upsert('entities', parentNetworksWithGrandParent, ['slug', 'type'], knex);
|
||||
const parentNetworksBySlug = [].concat(parentNetworkEntries.inserted, parentNetworkEntries.updated).reduce((acc, network) => ({ ...acc, [network.slug]: network.id }), {});
|
||||
const parentNetworkEntries = await upsert('entities', parentNetworksWithGrandParent, ['slug', 'type'], knex);
|
||||
const parentNetworksBySlug = [].concat(parentNetworkEntries.inserted, parentNetworkEntries.updated).reduce((acc, network) => ({ ...acc, [network.slug]: network.id }), {});
|
||||
|
||||
const networksWithParent = networks.map((network) => ({
|
||||
slug: network.slug,
|
||||
name: network.name,
|
||||
type: network.type || 'network',
|
||||
alias: network.alias,
|
||||
url: network.url,
|
||||
description: network.description,
|
||||
has_logo: network.hasLogo ?? true,
|
||||
showcased: typeof network.showcased === 'boolean' ? network.showcased : true,
|
||||
parameters: network.parameters,
|
||||
options: network.options,
|
||||
parent_id: parentNetworksBySlug[network.parent] || grandParentNetworksBySlug[network.parent] || null,
|
||||
}));
|
||||
const networksWithParent = networks.map((network) => ({
|
||||
slug: network.slug,
|
||||
name: network.name,
|
||||
type: network.type || 'network',
|
||||
alias: network.alias,
|
||||
url: network.url,
|
||||
description: network.description,
|
||||
has_logo: network.hasLogo ?? true,
|
||||
showcased: typeof network.showcased === 'boolean' ? network.showcased : true,
|
||||
parameters: network.parameters || null,
|
||||
options: network.options,
|
||||
parent_id: parentNetworksBySlug[network.parent] || grandParentNetworksBySlug[network.parent] || null,
|
||||
}));
|
||||
|
||||
const networkEntries = await upsert('entities', networksWithParent, ['slug', 'type'], knex);
|
||||
const networkEntries = await upsert('entities', networksWithParent, ['slug', 'type'], knex);
|
||||
|
||||
const networkIdsBySlug = [].concat(
|
||||
grandParentNetworkEntries.inserted,
|
||||
grandParentNetworkEntries.updated,
|
||||
parentNetworkEntries.inserted,
|
||||
parentNetworkEntries.updated,
|
||||
networkEntries.inserted,
|
||||
networkEntries.updated,
|
||||
).reduce((acc, network) => ({ ...acc, [network.slug]: network.id }), {});
|
||||
const networkIdsBySlug = [].concat(
|
||||
grandParentNetworkEntries.inserted,
|
||||
grandParentNetworkEntries.updated,
|
||||
parentNetworkEntries.inserted,
|
||||
parentNetworkEntries.updated,
|
||||
networkEntries.inserted,
|
||||
networkEntries.updated,
|
||||
).reduce((acc, network) => ({ ...acc, [network.slug]: network.id }), {});
|
||||
|
||||
const tagSlugs = networks.map((network) => network.tags).flat().filter(Boolean);
|
||||
const tagSlugs = networks.map((network) => network.tags).flat().filter(Boolean);
|
||||
|
||||
const tagEntries = await knex('tags').whereIn('slug', tagSlugs);
|
||||
const tagIdsBySlug = tagEntries.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.id }), {});
|
||||
const tagEntries = await knex('tags').whereIn('slug', tagSlugs);
|
||||
const tagIdsBySlug = tagEntries.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.id }), {});
|
||||
|
||||
const tagAssociations = networks
|
||||
.map((network) => (network.tags
|
||||
? network.tags.map((tagSlug) => ({
|
||||
entity_id: networkIdsBySlug[network.slug],
|
||||
tag_id: tagIdsBySlug[tagSlug],
|
||||
inherit: true,
|
||||
}))
|
||||
: []))
|
||||
.flat();
|
||||
const tagAssociations = networks
|
||||
.map((network) => (network.tags
|
||||
? network.tags.map((tagSlug) => ({
|
||||
entity_id: networkIdsBySlug[network.slug],
|
||||
tag_id: tagIdsBySlug[tagSlug],
|
||||
inherit: true,
|
||||
}))
|
||||
: []))
|
||||
.flat();
|
||||
|
||||
await upsert('entities_tags', tagAssociations, ['entity_id', 'tag_id'], knex);
|
||||
});
|
||||
await upsert('entities_tags', tagAssociations, ['entity_id', 'tag_id'], knex);
|
||||
|
||||
const entities = await knex('entities').select('id', 'slug', 'type');
|
||||
|
||||
await redis.connect();
|
||||
|
||||
await redis.del('traxxx:entities:id_by_slug');
|
||||
await redis.hSet('traxxx:entities:id_by_slug', entities.map((entity) => [`${entityPrefixes[entity.type]}${entity.slug}`, entity.id]));
|
||||
|
||||
await redis.disconnect();
|
||||
};
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
const upsert = require('../src/utils/upsert');
|
||||
const redis = require('../src/redis');
|
||||
|
||||
const entityPrefixes = {
|
||||
channel: '',
|
||||
network: '_',
|
||||
studio: '*',
|
||||
info: '@',
|
||||
};
|
||||
|
||||
/* eslint-disable max-len */
|
||||
const sites = [
|
||||
@@ -579,7 +587,6 @@ const sites = [
|
||||
tags: ['gay'],
|
||||
independent: true,
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
scene: 'https://www.chaosmen.com/en/video/chaosmen',
|
||||
},
|
||||
},
|
||||
@@ -601,7 +608,6 @@ const sites = [
|
||||
parent: 'gamma',
|
||||
independent: true,
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
scene: 'https://www.tabooheat.com/en/video/tabooheat',
|
||||
},
|
||||
},
|
||||
@@ -611,6 +617,9 @@ const sites = [
|
||||
slug: 'asgmaxoriginals',
|
||||
url: 'https://www.asgmax.com/en/channel/asgmaxoriginals',
|
||||
parent: 'asgmax',
|
||||
parameters: {
|
||||
queryChannel: 'asgmaxoriginals',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ASG Max Films',
|
||||
@@ -618,6 +627,9 @@ const sites = [
|
||||
url: 'https://www.asgmax.com/en/channel/asgmaxfilms',
|
||||
parent: 'asgmax',
|
||||
hasLogo: false,
|
||||
parameters: {
|
||||
queryChannel: 'asgmaxfilms',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ASG International',
|
||||
@@ -625,6 +637,9 @@ const sites = [
|
||||
url: 'https://www.asgmax.com/en/channel/asginternational',
|
||||
parent: 'asgmax',
|
||||
hasLogo: false,
|
||||
parameters: {
|
||||
queryChannel: 'asginternational',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ASG Massage',
|
||||
@@ -633,6 +648,9 @@ const sites = [
|
||||
parent: 'asgmax',
|
||||
tags: ['massage'],
|
||||
hasLogo: false,
|
||||
parameters: {
|
||||
queryChannel: 'asgmassage',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ASG Auditions',
|
||||
@@ -641,6 +659,9 @@ const sites = [
|
||||
parent: 'asgmax',
|
||||
tags: ['audition'],
|
||||
hasLogo: false,
|
||||
parameters: {
|
||||
queryChannel: 'asgauditions',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ASG Free Use',
|
||||
@@ -649,6 +670,9 @@ const sites = [
|
||||
parent: 'asgmax',
|
||||
tags: ['free-use'],
|
||||
hasLogo: false,
|
||||
parameters: {
|
||||
queryChannel: 'asgfreeuse',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Exeter Hill College',
|
||||
@@ -657,6 +681,9 @@ const sites = [
|
||||
parent: 'asgmax',
|
||||
hasLogo: false,
|
||||
tags: ['animated'],
|
||||
parameters: {
|
||||
queryChannel: 'asgexeterhillcollege',
|
||||
},
|
||||
},
|
||||
// ASG MAX INDEPENDENT
|
||||
{
|
||||
@@ -1020,12 +1047,12 @@ const sites = [
|
||||
{
|
||||
name: 'Disruptive Films',
|
||||
slug: 'disruptivefilms',
|
||||
delete: true,
|
||||
url: 'https://www.disruptivefilms.com',
|
||||
parent: 'disruptivefilms',
|
||||
tags: ['gay'],
|
||||
parameters: {
|
||||
queryChannel: 'asgmaxdisruptivefilms',
|
||||
filterChannel: 'asgmaxdisruptivefilms',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1072,6 +1099,17 @@ const sites = [
|
||||
queryChannel: 'asgmaxtruemale',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Deep Inside',
|
||||
slug: 'deepinside',
|
||||
url: 'https://www.asgmax.com/en/channel/asgmaxdeepinside',
|
||||
parent: 'disruptivefilms',
|
||||
tags: ['gay'],
|
||||
hasLogo: false,
|
||||
parameters: {
|
||||
queryChannel: 'asgmaxdeepinside',
|
||||
},
|
||||
},
|
||||
// AMATEUR ALLURE
|
||||
{
|
||||
name: 'Amateur Allure',
|
||||
@@ -1426,7 +1464,6 @@ const sites = [
|
||||
independent: true,
|
||||
parameters: {
|
||||
scene: 'https://www.gangbangcreampie.com/en/video/gangbangcreampie',
|
||||
layout: 'api',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1438,7 +1475,6 @@ const sites = [
|
||||
independent: true,
|
||||
parameters: {
|
||||
scene: 'https://www.gloryholesecrets.com/en/video/gloryholesecrets',
|
||||
layout: 'api',
|
||||
},
|
||||
},
|
||||
/* different layout
|
||||
@@ -1451,7 +1487,6 @@ const sites = [
|
||||
independent: true,
|
||||
parameters: {
|
||||
scene: 'https://www.portagloryhole.com/scenes',
|
||||
layout: 'api',
|
||||
},
|
||||
},
|
||||
*/
|
||||
@@ -2685,9 +2720,6 @@ const sites = [
|
||||
url: 'https://www.biphoria.com',
|
||||
independent: true,
|
||||
tags: ['bisexual'],
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
},
|
||||
parent: 'gamma',
|
||||
},
|
||||
// BLOWPASS
|
||||
@@ -3180,9 +3212,6 @@ const sites = [
|
||||
alias: ['burna'],
|
||||
url: 'https://www.burningangel.com',
|
||||
independent: true,
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
},
|
||||
parent: 'gamma',
|
||||
},
|
||||
// CHERRY PIMPS
|
||||
@@ -3828,7 +3857,6 @@ const sites = [
|
||||
parent: 'gamma',
|
||||
independent: true,
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
deep: 'https://www.diabolic.com/en/video/diabolic',
|
||||
actors: 'https://www.diabolic.com/en/pornstar/view/{slug}/{id}',
|
||||
},
|
||||
@@ -3900,9 +3928,6 @@ const sites = [
|
||||
name: 'DFXtra',
|
||||
url: 'https://www.dfxtra.com',
|
||||
parent: 'dfxtra',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'dfxtraoriginals',
|
||||
@@ -3910,9 +3935,6 @@ const sites = [
|
||||
url: 'https://www.dfxtra.com',
|
||||
parent: 'dfxtra',
|
||||
hasLogo: false,
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'dfxtracompilations',
|
||||
@@ -3920,9 +3942,6 @@ const sites = [
|
||||
url: 'https://www.dfxtra.com',
|
||||
parent: 'dfxtra',
|
||||
hasLogo: false,
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'dfxbigbangz',
|
||||
@@ -3930,9 +3949,6 @@ const sites = [
|
||||
url: 'https://www.dfxtra.com',
|
||||
parent: 'dfxtra',
|
||||
hasLogo: false,
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'dfxsolemates',
|
||||
@@ -3940,9 +3956,6 @@ const sites = [
|
||||
url: 'https://www.dfxtra.com',
|
||||
parent: 'dfxtra',
|
||||
hasLogo: false,
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'cheatingwithmyex',
|
||||
@@ -3950,9 +3963,6 @@ const sites = [
|
||||
url: 'https://www.dfxtra.com',
|
||||
parent: 'dfxtra',
|
||||
hasLogo: false,
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
},
|
||||
},
|
||||
// DFXTRA DOGFART
|
||||
{
|
||||
@@ -5452,6 +5462,12 @@ const sites = [
|
||||
referer: 'https://www.girlsway.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'lesbianfactor',
|
||||
name: 'Lesbian Factor',
|
||||
url: 'https://www.lesbianfactor.com',
|
||||
parent: 'girlsway',
|
||||
},
|
||||
// HITZEFREI
|
||||
{
|
||||
slug: 'unleashed',
|
||||
@@ -5562,6 +5578,16 @@ const sites = [
|
||||
latest: 'https://seehimfuck.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'seehimsolo',
|
||||
name: 'See Him Solo',
|
||||
url: 'https://seehimsolo.com',
|
||||
tags: ['male-focus', 'solo'],
|
||||
parent: 'hussiepass',
|
||||
parameters: {
|
||||
latest: 'https://seehimsolo.com/categories/movies-2/{page}/latest/',
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'interracialpovs',
|
||||
name: 'Interracial POVs',
|
||||
@@ -6079,7 +6105,6 @@ const sites = [
|
||||
url: 'https://cospimps.com',
|
||||
parent: 'jayrock',
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
/* Gamma scenes are out of date
|
||||
referer: 'https://www.21sextury.com',
|
||||
scene: false,
|
||||
@@ -6147,7 +6172,8 @@ const sites = [
|
||||
slug: 'privatecollection',
|
||||
rename: 'karupsprivatecollection',
|
||||
alias: ['kpc'],
|
||||
url: 'https://www.karups.com/site/kpc/', // trailing slash required
|
||||
url: 'https://www.karupspc.com',
|
||||
// url: 'https://www.karups.com/site/kpc/', // trailing slash required
|
||||
parent: 'karups',
|
||||
},
|
||||
{
|
||||
@@ -6155,7 +6181,8 @@ const sites = [
|
||||
slug: 'hometownamateurs',
|
||||
rename: 'karupshometownamateurs',
|
||||
alias: ['kha'],
|
||||
url: 'https://www.karups.com/site/kha/',
|
||||
url: 'https://www.karupsha.com',
|
||||
// url: 'https://www.karups.com/site/kha/',
|
||||
parent: 'karups',
|
||||
},
|
||||
{
|
||||
@@ -6163,7 +6190,8 @@ const sites = [
|
||||
slug: 'olderwomen',
|
||||
rename: 'karupsolderwomen',
|
||||
alias: ['kow'],
|
||||
url: 'https://www.karups.com/site/kow/',
|
||||
url: 'https://www.karupsow.com',
|
||||
// url: 'https://www.karups.com/site/kow/',
|
||||
parent: 'karups',
|
||||
},
|
||||
{
|
||||
@@ -6174,6 +6202,14 @@ const sites = [
|
||||
independent: true,
|
||||
tags: ['gay'],
|
||||
},
|
||||
{
|
||||
name: 'Jawked',
|
||||
slug: 'jawked',
|
||||
url: 'https://www.jawked.com',
|
||||
parent: 'karups',
|
||||
independent: true,
|
||||
tags: ['gay'],
|
||||
},
|
||||
// KELLY MADISON MEDIA / 5K / 8K
|
||||
{
|
||||
slug: 'teenfidelity',
|
||||
@@ -9412,9 +9448,9 @@ const sites = [
|
||||
parent: 'nubiles',
|
||||
},
|
||||
{
|
||||
slug: 'caughtmycoach',
|
||||
name: 'Caught My Coach',
|
||||
url: 'https://caughtmycoach.com',
|
||||
slug: 'shesbreedingmaterial',
|
||||
name: 'She\'s Breeding Material',
|
||||
url: 'https://shesbreedingmaterial.com',
|
||||
parent: 'nubiles',
|
||||
},
|
||||
// PASCALS SUBSLUTS
|
||||
@@ -10537,15 +10573,28 @@ const sites = [
|
||||
siteAsSerie: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Hardwerk',
|
||||
slug: 'hardwerk',
|
||||
url: 'https://hardwerk.com',
|
||||
independent: true,
|
||||
parent: 'radical',
|
||||
parameters: {
|
||||
endpoint: 'jC4SrjH8YVDtRejiA0PMx',
|
||||
videos: 'films',
|
||||
actors: 'performers',
|
||||
},
|
||||
},
|
||||
// REALITY KINGS
|
||||
{
|
||||
name: 'Look At Her Now',
|
||||
url: 'https://www.lookathernow.com',
|
||||
description: 'Look At Her Now brings you best HD reality porn videos every week. Check out these girls before and after they get some rough pounding.',
|
||||
parameters: { native: true },
|
||||
// parameters: { siteId: 300 },
|
||||
slug: 'lookathernow',
|
||||
parent: 'realitykings',
|
||||
parameters: {
|
||||
siteId: 364,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'We Live Together',
|
||||
@@ -15036,12 +15085,12 @@ const sites = [
|
||||
name: 'Vivid Celeb',
|
||||
url: 'https://www.vividceleb.com',
|
||||
parent: 'vivid',
|
||||
parameters: {
|
||||
referer: 'https://www.thebrats.com',
|
||||
deep: 'https://www.thebrats.com/en/video',
|
||||
scene: false,
|
||||
lastNative: new Date('2018-03-25'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'vivid',
|
||||
name: 'Vivid',
|
||||
url: 'https://www.vivid.com/en/videos/sites/vivid',
|
||||
parent: 'vivid',
|
||||
},
|
||||
{
|
||||
slug: 'thebrats',
|
||||
@@ -15058,122 +15107,72 @@ const sites = [
|
||||
{
|
||||
slug: 'nineteen',
|
||||
name: 'Nineteen',
|
||||
url: 'http://www.nineteen.com',
|
||||
url: 'https://www.vivid.com/en/videos/sites/nineteen',
|
||||
parent: 'vivid',
|
||||
parameters: {
|
||||
referer: 'https://www.thebrats.com',
|
||||
deep: 'https://www.thebrats.com/en/video',
|
||||
scene: false,
|
||||
lastNative: new Date('2019-01-23'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'nastystepfamily',
|
||||
name: 'Nasty Step Family',
|
||||
url: 'http://www.nastystepfamily.com',
|
||||
url: 'https://www.vivid.com/en/videos/sites/nastystepfamily',
|
||||
tags: ['family'],
|
||||
parent: 'vivid',
|
||||
parameters: {
|
||||
referer: 'https://www.thebrats.com',
|
||||
deep: 'https://www.thebrats.com/en/video',
|
||||
scene: false,
|
||||
lastNative: new Date('2019-01-29'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'girlswhofuckgirls',
|
||||
name: 'Girls Who Fuck Girls',
|
||||
url: 'http://www.girlswhofuckgirls.com',
|
||||
url: 'https://www.vivid.com/en/videos/sites/girlswhofuckgirls',
|
||||
tags: ['lesbian'],
|
||||
parent: 'vivid',
|
||||
parameters: {
|
||||
referer: 'https://www.thebrats.com',
|
||||
deep: 'https://www.thebrats.com/en/video',
|
||||
scene: false,
|
||||
lastNative: new Date('2019-05-21'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'petited',
|
||||
name: 'Petited',
|
||||
url: 'http://www.petited.com',
|
||||
url: 'https://www.vivid.com/en/videos/sites/petited',
|
||||
parent: 'vivid',
|
||||
parameters: {
|
||||
referer: 'https://www.thebrats.com',
|
||||
deep: 'https://www.thebrats.com/en/video',
|
||||
scene: false,
|
||||
lastNative: new Date('2019-01-28'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'orgytrain',
|
||||
name: 'Orgy Train',
|
||||
url: 'http://www.orgytrain.com',
|
||||
url: 'https://www.vivid.com/en/videos/sites/orgytrain',
|
||||
parent: 'vivid',
|
||||
parameters: {
|
||||
referer: 'https://www.thebrats.com',
|
||||
deep: 'https://www.thebrats.com/en/video',
|
||||
scene: false,
|
||||
lastNative: new Date('2019-01-09'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'momisamilf',
|
||||
name: 'Mom Is A MILF',
|
||||
url: 'http://www.momisamilf.com',
|
||||
url: 'https://www.vivid.com/en/videos/sites/momisamilf',
|
||||
parent: 'vivid',
|
||||
parameters: {
|
||||
referer: 'https://www.thebrats.com',
|
||||
deep: 'https://www.thebrats.com/en/video',
|
||||
scene: false,
|
||||
lastNative: new Date('2019-01-25'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'blackwhitefuckfest',
|
||||
name: 'Black White Fuck Fest',
|
||||
url: 'http://www.blackwhitefuckfest.com',
|
||||
url: 'https://www.vivid.com/en/videos/sites/blackwhitefuckfest',
|
||||
parent: 'vivid',
|
||||
parameters: {
|
||||
referer: 'https://www.thebrats.com',
|
||||
deep: 'https://www.thebrats.com/en/video',
|
||||
scene: false,
|
||||
lastNative: new Date('2019-01-30'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: '65inchhugeasses',
|
||||
name: '65 Inch Huge Asses',
|
||||
url: 'http://www.65inchhugeasses.com',
|
||||
url: 'https://www.vivid.com/en/videos/sites/65inchhugeasses',
|
||||
parent: 'vivid',
|
||||
parameters: {
|
||||
referer: 'https://www.thebrats.com',
|
||||
deep: 'https://www.thebrats.com/en/video',
|
||||
scene: false,
|
||||
lastNative: new Date('2019-05-18'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'brandnewfaces',
|
||||
name: 'Brand New Faces',
|
||||
url: 'http://www.brandnewfaces.com',
|
||||
url: 'https://www.vivid.com/en/videos/sites/brandnewfaces',
|
||||
parent: 'vivid',
|
||||
parameters: {
|
||||
referer: 'https://www.thebrats.com',
|
||||
deep: 'https://www.thebrats.com/en/video',
|
||||
scene: false,
|
||||
lastNative: new Date('2018-02-28'),
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'vividclassic',
|
||||
name: 'Vivid Classic',
|
||||
url: 'http://www.vividclassic.com',
|
||||
url: 'https://www.vivid.com/en/videos/sites/vividclassic',
|
||||
parent: 'vivid',
|
||||
},
|
||||
{
|
||||
slug: 'tsdivas',
|
||||
name: 'TS Divas',
|
||||
url: 'https://www.vivid.com/en/videos/sites/tsdivas',
|
||||
tags: ['transsexual'],
|
||||
hasLogo: false,
|
||||
parent: 'vivid',
|
||||
parameters: {
|
||||
referer: 'https://www.thebrats.com',
|
||||
deep: 'https://www.thebrats.com/en/video',
|
||||
scene: false,
|
||||
lastNative: new Date('2016-06-29'),
|
||||
},
|
||||
},
|
||||
// VIXEN
|
||||
{
|
||||
@@ -15477,9 +15476,6 @@ const sites = [
|
||||
url: 'https://www.wicked.com',
|
||||
description: 'Welcome to the new Wicked.com! Watch over 25 years of Wicked Pictures\' brand of award-winning porn for couples and women in 4k HD movies & xxx videos',
|
||||
independent: true,
|
||||
parameters: {
|
||||
layout: 'api',
|
||||
},
|
||||
parent: 'gamma',
|
||||
},
|
||||
// XEMPIRE
|
||||
@@ -15575,195 +15571,86 @@ sites.reduce((acc, site) => {
|
||||
}, new Set());
|
||||
|
||||
/* eslint-disable max-len */
|
||||
exports.seed = (knex) => Promise.resolve()
|
||||
.then(async () => {
|
||||
await Promise.all(sites.map(async (channel) => {
|
||||
if (channel.rename) {
|
||||
await knex('entities')
|
||||
.where({
|
||||
type: channel.type || 'channel',
|
||||
slug: channel.rename,
|
||||
})
|
||||
.update('slug', channel.slug);
|
||||
exports.seed = async (knex) => {
|
||||
await Promise.all(sites.map(async (channel) => {
|
||||
if (channel.rename) {
|
||||
await knex('entities')
|
||||
.where({
|
||||
type: channel.type || 'channel',
|
||||
slug: channel.rename,
|
||||
})
|
||||
.update('slug', channel.slug);
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
if (channel.delete) {
|
||||
await knex('entities')
|
||||
.where({
|
||||
type: channel.type || 'channel',
|
||||
slug: channel.slug,
|
||||
})
|
||||
.delete();
|
||||
}
|
||||
}).filter(Boolean));
|
||||
|
||||
const networks = await knex('entities')
|
||||
.where('type', 'network')
|
||||
.orWhereNull('parent_id');
|
||||
|
||||
const networksMap = networks.filter((network) => !network.delete).reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
|
||||
|
||||
const tags = await knex('tags').select('*').whereNull('alias_for');
|
||||
const tagsMap = tags.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
|
||||
|
||||
const sitesWithNetworks = sites.filter((site) => !site.delete).map((site) => ({
|
||||
slug: site.slug,
|
||||
name: site.name,
|
||||
name_stylized: site.style,
|
||||
type: site.type || 'channel',
|
||||
alias: site.alias,
|
||||
description: site.description,
|
||||
url: site.url,
|
||||
parameters: site.parameters || null,
|
||||
options: site.options,
|
||||
parent_id: networksMap[site.parent] || null,
|
||||
priority: site.priority || 0,
|
||||
independent: !!site.independent,
|
||||
visible: site.visible,
|
||||
showcased: site.showcased,
|
||||
has_logo: site.hasLogo === undefined ? true : site.hasLogo,
|
||||
}));
|
||||
|
||||
const { inserted, updated } = await upsert('entities', sitesWithNetworks, ['slug', 'type'], knex);
|
||||
const sitesMap = [].concat(inserted, updated).reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
|
||||
|
||||
const tagAssociations = sites.map((site) => (site.tags && !site.delete
|
||||
? site.tags.map((tagSlug) => {
|
||||
const tag = tagsMap[tagSlug];
|
||||
|
||||
if (!tag) {
|
||||
console.warn(`Tag ${tagSlug} for ${site.slug} does not exist`);
|
||||
}
|
||||
|
||||
if (channel.delete) {
|
||||
await knex('entities')
|
||||
.where({
|
||||
type: channel.type || 'channel',
|
||||
slug: channel.slug,
|
||||
})
|
||||
.delete();
|
||||
}
|
||||
}).filter(Boolean));
|
||||
return {
|
||||
entity_id: sitesMap[site.slug],
|
||||
tag_id: tagsMap[tagSlug],
|
||||
inherit: true,
|
||||
};
|
||||
})
|
||||
: []
|
||||
)).flat();
|
||||
|
||||
const networks = await knex('entities')
|
||||
.where('type', 'network')
|
||||
.orWhereNull('parent_id');
|
||||
await upsert('entities_tags', tagAssociations, ['entity_id', 'tag_id'], knex);
|
||||
|
||||
const networksMap = networks.filter((network) => !network.delete).reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
|
||||
const entities = await knex('entities').select('id', 'slug', 'type');
|
||||
|
||||
const tags = await knex('tags').select('*').whereNull('alias_for');
|
||||
const tagsMap = tags.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
|
||||
await redis.connect();
|
||||
|
||||
const sitesWithNetworks = sites.filter((site) => !site.delete).map((site) => ({
|
||||
slug: site.slug,
|
||||
name: site.name,
|
||||
name_stylized: site.style,
|
||||
type: site.type || 'channel',
|
||||
alias: site.alias,
|
||||
description: site.description,
|
||||
url: site.url,
|
||||
parameters: site.parameters,
|
||||
options: site.options,
|
||||
parent_id: networksMap[site.parent],
|
||||
priority: site.priority || 0,
|
||||
independent: !!site.independent,
|
||||
visible: site.visible,
|
||||
showcased: site.showcased,
|
||||
has_logo: site.hasLogo === undefined ? true : site.hasLogo,
|
||||
}));
|
||||
await redis.del('traxxx:entities:id_by_slug');
|
||||
await redis.hSet('traxxx:entities:id_by_slug', entities.map((entity) => [`${entityPrefixes[entity.type]}${entity.slug}`, entity.id]));
|
||||
|
||||
const { inserted, updated } = await upsert('entities', sitesWithNetworks, ['slug', 'type'], knex);
|
||||
const sitesMap = [].concat(inserted, updated).reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
|
||||
|
||||
const tagAssociations = sites.map((site) => (site.tags && !site.delete
|
||||
? site.tags.map((tagSlug) => {
|
||||
const tag = tagsMap[tagSlug];
|
||||
|
||||
if (!tag) {
|
||||
console.warn(`Tag ${tagSlug} for ${site.slug} does not exist`);
|
||||
}
|
||||
|
||||
return {
|
||||
entity_id: sitesMap[site.slug],
|
||||
tag_id: tagsMap[tagSlug],
|
||||
inherit: true,
|
||||
};
|
||||
})
|
||||
: []
|
||||
)).flat();
|
||||
|
||||
return upsert('entities_tags', tagAssociations, ['entity_id', 'tag_id'], knex);
|
||||
});
|
||||
await redis.disconnect();
|
||||
};
|
||||
|
||||
exports.sites = sites;
|
||||
|
||||
/*
|
||||
'X-Art' => 'xart',
|
||||
'met-art' => 'metart',
|
||||
'18og' => '18OnlyGirls',
|
||||
'a1o1' => 'Asian1on1',
|
||||
'add' => 'ManualAddActors',
|
||||
'analb' => 'AnalBeauty',
|
||||
'bgonzo' => 'BangGonzo',
|
||||
'btlbd' => 'BigTitsLikeBigDicks',
|
||||
'bjf' => 'BlowjobFridays',
|
||||
'cws' => 'CzechWifeSwap',
|
||||
'Daughter' => 'DaughterSwap',
|
||||
'Daughters' => 'DaughterSwap',
|
||||
'dc' => 'DorcelVision',
|
||||
'dpg' => 'DigitalPlayground',
|
||||
'dsw' => 'DaughterSwap',
|
||||
'faq' => 'FirstAnalQuest',
|
||||
'ft' => 'FastTimes',
|
||||
'fittingroom' => 'Fitting-Room',
|
||||
'gbcp' => 'GangbangCreampie',
|
||||
'hart' => 'Hegre',
|
||||
'hegre-art' => 'Hegre',
|
||||
'kha' => 'KarupsHA',
|
||||
'kow' => 'KarupsOW',
|
||||
'kpc' => 'KarupsPC',
|
||||
'la' => 'LatinAdultery',
|
||||
'lcd' => 'LittleCaprice',
|
||||
'lhf' => 'LoveHerFeet',
|
||||
'littlecapricedreams' => 'Little Caprice Dreams',
|
||||
'maj' => 'ManoJob',
|
||||
'mfl' => 'Mofos',
|
||||
'mj' => 'ManoJob',
|
||||
'mpov' => 'MrPOV',
|
||||
'naughtyamericavr' => 'NaughtyAmerica',
|
||||
'news' => 'NewSensations',
|
||||
'ps' => 'PropertySex',
|
||||
'sart' => 'SexArt',
|
||||
'sbj' => 'StreetBlowjobs',
|
||||
'sislove' => 'SisLovesMe',
|
||||
'tds' => 'TheDickSuckers',
|
||||
'these' => 'TheStripperExperience',
|
||||
'tlc' => 'TeensLoveCream',
|
||||
'tle' => 'TheLifeErotic',
|
||||
'tog' => 'TonightsGirlfriend',
|
||||
'wowg' => 'WowGirls',
|
||||
'wy' => 'WebYoung',
|
||||
'itc' => 'InTheCrack',
|
||||
"abbw" => "AbbyWinters",
|
||||
"abme" => "AbuseMe",
|
||||
"ana" => "AnalAngels",
|
||||
"atke" => "ATKExotics",
|
||||
"atkg" => "ATKGalleria",
|
||||
"atkgfs" => "ATKGirlfriends",
|
||||
"atkh" => "ATKHairy",
|
||||
"aktp" => "ATKPetites",
|
||||
"ba" => "Beauty-Angels",
|
||||
"bna" => "BrandNew",
|
||||
"bam" => "BruceAndMorgan",
|
||||
"bcast" => "BrutalCastings",
|
||||
"bd" => "BrutalDildos",
|
||||
"bpu" => "BrutalPickups",
|
||||
"cza" => "CzhecAmateurs",
|
||||
"czbb" => "CzechBangBus",
|
||||
"czb" => "CzechBitch",
|
||||
"cc" => "CzechCasting",
|
||||
"czc" => "CzechCouples",
|
||||
"czestro" => "CzechEstrogenolit",
|
||||
"czf" => "CzechFantasy",
|
||||
"czgb" => "CzechGangBang",
|
||||
"cgfs" => "CzechGFS",
|
||||
"czharem" => "CzechHarem",
|
||||
"czm" => "CzechMassage",
|
||||
"czo" => "CzechOrgasm",
|
||||
"czps" => "CzechPawnShop",
|
||||
"css" => "CzechStreets",
|
||||
"cztaxi" => "CzechTaxi",
|
||||
"czt" => "CzechTwins",
|
||||
"dts" => "DeepThroatSirens",
|
||||
"doan" => "DiaryOfANanny",
|
||||
"ds" => "DungeonSex",
|
||||
"ffr" => "FacialsForever",
|
||||
"ff" => "FilthyFamily",
|
||||
"fbbg" => "FirstBGG",
|
||||
"fs" => "FuckStudies",
|
||||
"tfcp" => "FullyClothedPissing",
|
||||
"gdp" => "GirlsDoPorn",
|
||||
"Harmony" => "HarmonyVision",
|
||||
"hletee" => "HelplessTeens",
|
||||
"jlmf" => "JessieLoadsMonsterFacials",
|
||||
"lang" => "LANewGirl",
|
||||
"mmp" => "MMPNetwork",
|
||||
"mbc" => "MyBabysittersClub",
|
||||
"nvg" => "NetVideoGirls",
|
||||
"oo" => "Only-Opaques",
|
||||
"os" => "Only-Secretaries",
|
||||
"oss" => "OnlySilAndSatin",
|
||||
"psus" => "PascalsSubSluts",
|
||||
"psp" => "PorsntarsPunishment",
|
||||
"pdmqfo" => "QuestForOrgasm",
|
||||
"sed" => "SexualDisgrace",
|
||||
"sislov" => "SisLovesMe",
|
||||
"tslw" => "SlimeWave",
|
||||
"stre" => "StrictRestraint",
|
||||
"t18" => "Taboo18",
|
||||
"tsma" => "TeenSexMania",
|
||||
"tsm" => "TeenSexMovs",
|
||||
"ttw" => "TeensInTheWoods",
|
||||
"tgw" => "ThaiGirlsWild",
|
||||
"taob" => "TheArtOfBlowJob",
|
||||
"trwo" => "TheRealWorkout",
|
||||
"tt" => "TryTeens",
|
||||
"vp" => "VIPissy",
|
||||
"wrh" => "WeAreHairy",
|
||||
"yt" => "YoungThroats",
|
||||
];
|
||||
*/
|
||||
|
||||
@@ -208,6 +208,11 @@ const affiliates = [
|
||||
url: 'https://www.g2buddy.com/disruptivefilms/go.php?pr=9&su=2&si=119&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
{
|
||||
channel: 'sodomysquad',
|
||||
url: 'https://www.g2buddy.com/sodomysquad/go.php?pr=9&su=2&si=137&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
// gamma > ags max > next door studios
|
||||
// excluded affiliate links that link back to main site and don't seem to track properly
|
||||
{
|
||||
@@ -494,6 +499,92 @@ const affiliates = [
|
||||
scene: false, // redirects to Adult Time
|
||||
},
|
||||
},
|
||||
// gamma > vivid
|
||||
{
|
||||
network: 'vivid',
|
||||
url: 'https://www.g2fame.com/vivid/go.php?pr=8&su=2&si=330&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
parameters: {
|
||||
scene: false, // redirects to homepage
|
||||
},
|
||||
},
|
||||
{
|
||||
channel: 'wheretheboysarent',
|
||||
url: 'https://www.g2fame.com/wheretheboysarent/go.php?pr=8&su=2&si=368&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
{
|
||||
channel: 'thebrats',
|
||||
url: 'https://www.g2fame.com/thebrats/go.php?pr=8&su=2&si=369&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
// gamma > zero tolerance
|
||||
{
|
||||
network: 'zerotolerance',
|
||||
url: 'https://www.g2fame.com/zerotolerancefilms/go.php?pr=8&su=2&si=507&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
{
|
||||
channel: 'zerotolerancefilms',
|
||||
url: 'https://www.g2fame.com/zerotolerancefilms/go.php?pr=8&su=2&si=507&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
{
|
||||
channel: '3rddegreefilms',
|
||||
url: 'https://www.g2fame.com/3rddegreefilms/go.php?pr=8&su=2&si=537&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
{
|
||||
channel: 'addicted2girls',
|
||||
url: 'https://www.g2fame.com/addicted2girls/go.php?pr=8&su=2&si=477&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
{
|
||||
channel: 'genderxfilms',
|
||||
url: 'https://www.g2fame.com/genderxfilms/go.php?pr=8&su=2&si=397&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
{
|
||||
channel: 'gangbangcreampie',
|
||||
url: 'https://www.g2fame.com/gangbangcreampie/go.php?pr=8&su=2&si=656&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
{
|
||||
channel: 'gloryholesecrets',
|
||||
url: 'https://www.g2fame.com/gloryholesecrets/go.php?pr=8&su=2&si=655&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
{
|
||||
channel: 'tabooheat',
|
||||
url: 'https://www.g2fame.com/tabooheat/go.php?pr=8&su=2&si=552&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
{
|
||||
channel: 'wicked',
|
||||
url: 'https://www.g2fame.com/wicked/go.php?pr=8&su=2&si=371&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
// gamma > independent channels
|
||||
{
|
||||
channel: 'biphoria',
|
||||
url: 'https://www.g2fame.com/biphoria/go.php?pr=8&su=2&si=418&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
{
|
||||
channel: 'burningangel',
|
||||
url: 'https://www.g2fame.com/burningangel/go.php?pr=8&su=2&si=174&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
{
|
||||
channel: 'chaosmen',
|
||||
url: 'https://www.g2fame.com/chaosmen/go.php?pr=8&su=2&si=608&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
{
|
||||
channel: 'diabolic',
|
||||
url: 'https://www.g2fame.com/diabolic/go.php?pr=8&su=2&si=523&ad=277470&pa=index&ar=&buffer=',
|
||||
comment: 'per signup',
|
||||
},
|
||||
// kelly madison / 8k
|
||||
{
|
||||
network: 'kellymadison',
|
||||
@@ -628,6 +719,11 @@ const affiliates = [
|
||||
url: 'https://register.join-toughlovex.com/track/MzAwMDA5NzkuMy43Ni4xOTcuMC4wLjAuMC4w',
|
||||
comment: 'rev share',
|
||||
},
|
||||
{
|
||||
channel: 'hardwerk',
|
||||
url: 'https://register.hardwerk.com/track/MzAwMDA5NzkuMy4xNTEuMzM5LjAuMC4wLjAuMA',
|
||||
comment: 'rev share',
|
||||
},
|
||||
// radical > topwebmodels
|
||||
{
|
||||
network: 'topwebmodels',
|
||||
@@ -854,6 +950,77 @@ const affiliates = [
|
||||
query: 'ref=4c331ef6',
|
||||
},
|
||||
},
|
||||
// POV Porn Cash / HussiePass
|
||||
{
|
||||
network: 'hussiepass',
|
||||
url: 'https://secure.hussiepass.com/track/MTk0NS4xLjUuNy4wLjAuMC4wLjA',
|
||||
comment: '50% revshare',
|
||||
parameters: {
|
||||
// hussiepass website does not show network scenes
|
||||
channelScenes: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
channel: 'povpornstars',
|
||||
url: 'https://join.povpornstars.com/track/MTk0NS4xLjMuNS4wLjAuMC4wLjA',
|
||||
comment: '50% revshare',
|
||||
},
|
||||
{
|
||||
channel: 'interracialpovs',
|
||||
url: 'https://join.interracialpovs.com/track/MTk0NS4xLjYuOC4wLjAuMC4wLjA',
|
||||
comment: '50% revshare',
|
||||
},
|
||||
{
|
||||
channel: 'ravebunnys',
|
||||
url: 'https://secure.ravebunnys.com/track/MTk0NS4xLjExLjI5LjAuMC4wLjAuMA',
|
||||
comment: '50% revshare',
|
||||
},
|
||||
{
|
||||
channel: 'hotandtatted',
|
||||
url: 'https://join.hotandtatted.com/track/MTk0NS4xLjEwLjEyLjAuMC4wLjAuMA',
|
||||
comment: '50% revshare',
|
||||
},
|
||||
{
|
||||
channel: 'seehimfuck',
|
||||
url: 'https://join.seehimfuck.com/track/MTk0NS4xLjcuOS4wLjAuMC4wLjA',
|
||||
comment: '50% revshare',
|
||||
},
|
||||
{
|
||||
channel: 'seehimsolo',
|
||||
url: 'https://join.seehimsolo.com/track/MTk0NS4xLjguMTAuMC4wLjAuMC4w',
|
||||
comment: '50% revshare',
|
||||
},
|
||||
// karups
|
||||
{
|
||||
network: 'karups',
|
||||
url: 'https://secure.karups.com/track/MjAwMTAwMS4xLjEuMS4wLjAuMC4wLjA',
|
||||
comment: 'revshare',
|
||||
},
|
||||
{
|
||||
channel: 'hometownamateurs',
|
||||
url: 'https://secure.karupsha.com/track/MjAwMTAwMS4xLjMuMy4wLjAuMC4wLjA',
|
||||
comment: 'revshare',
|
||||
},
|
||||
{
|
||||
channel: 'olderwomen',
|
||||
url: 'https://secure.karupsow.com/track/MjAwMTAwMS4xLjQuNC4wLjAuMC4wLjA',
|
||||
comment: 'revshare',
|
||||
},
|
||||
{
|
||||
channel: 'privatecollection',
|
||||
url: 'https://secure.karupspc.com/track/MjAwMTAwMS4xLjIuMi4wLjAuMC4wLjA',
|
||||
comment: 'revshare',
|
||||
},
|
||||
{
|
||||
channel: 'boyfun',
|
||||
url: 'https://secure.boyfun.com/track/MjAwMTAwMS4xLjUuNS4wLjAuMC4wLjA',
|
||||
comment: 'revshare',
|
||||
},
|
||||
{
|
||||
channel: 'jawked',
|
||||
url: 'https://secure.jawked.com/track/MjAwMTAwMS4xLjExLjExLjAuMC4wLjAuMA',
|
||||
comment: 'revshare',
|
||||
},
|
||||
// etc
|
||||
{
|
||||
network: 'bang',
|
||||
|
||||
@@ -17,11 +17,12 @@ const domPurify = DOMPurify(window);
|
||||
// const logger = require('./logger')(__filename);
|
||||
const knex = require('./knex');
|
||||
const redis = require('./redis');
|
||||
const scrapers = require('./scrapers/scrapers').actors;
|
||||
const actorScrapers = require('./scrapers/scrapers').actors;
|
||||
|
||||
const argv = require('./argv');
|
||||
const include = require('./utils/argv-include')(argv);
|
||||
const bulkInsert = require('./utils/bulk-insert');
|
||||
const batchInsert = require('./utils/batch-insert');
|
||||
const chunk = require('./utils/chunk');
|
||||
const logger = require('./logger')(__filename);
|
||||
|
||||
@@ -46,6 +47,7 @@ const commonContext = {
|
||||
slugify,
|
||||
omit,
|
||||
unprint,
|
||||
batchInsert,
|
||||
};
|
||||
|
||||
const hairColors = {
|
||||
@@ -349,6 +351,7 @@ function curateProfileEntry(profile) {
|
||||
tattoos: profile.tattoos,
|
||||
blood_type: profile.bloodType,
|
||||
avatar_media_id: profile.avatarMediaId || null,
|
||||
updated_at: knex.raw('DEFAULT'), // default should be NOW(), this will update the column
|
||||
};
|
||||
|
||||
return curatedProfileEntry;
|
||||
@@ -438,35 +441,35 @@ async function curateProfile(profile, actor) {
|
||||
|| null;
|
||||
|
||||
curatedProfile.dateOfDeath = Number.isNaN(Number(profile.dateOfDeath)) ? null : profile.dateOfDeath;
|
||||
curatedProfile.age = Number(profile.age) || null;
|
||||
curatedProfile.age = Math.round(profile.age) || null;
|
||||
|
||||
curatedProfile.height = Number(profile.height) || profile.height?.match?.(/\d+/)?.[0] || null;
|
||||
curatedProfile.weight = Number(profile.weight) || profile.weight?.match?.(/\d+/)?.[0] || null;
|
||||
curatedProfile.shoeSize = Number(profile.shoeSize) || profile.shoeSize?.match?.(/\d+/)?.[0] || null;
|
||||
curatedProfile.height = Math.round(profile.height || profile.height?.match?.(/\d+/)?.[0]) || null;
|
||||
curatedProfile.weight = Math.round(profile.weight || profile.weight?.match?.(/\d+/)?.[0]) || null;
|
||||
|
||||
// separate measurement values
|
||||
curatedProfile.cup = profile.cup || (typeof profile.bust === 'string' && profile.bust?.match?.(/[a-zA-Z]+/)?.[0]) || null;
|
||||
curatedProfile.bust = Number(profile.bust) || profile.bust?.match?.(/\d+/)?.[0] || null;
|
||||
curatedProfile.waist = Number(profile.waist) || profile.waist?.match?.(/\d+/)?.[0] || null;
|
||||
curatedProfile.hip = Number(profile.hip) || profile.hip?.match?.(/\d+/)?.[0] || null;
|
||||
curatedProfile.bust = Math.round(profile.bust || profile.bust?.match?.(/\d+/)?.[0]) || null;
|
||||
curatedProfile.waist = Math.round(profile.waist || profile.waist?.match?.(/\d+/)?.[0]) || null;
|
||||
curatedProfile.hip = Math.round(profile.hip || profile.hip?.match?.(/\d+/)?.[0]) || null;
|
||||
|
||||
curatedProfile.leg = Number(profile.leg) || profile.leg?.match?.(/\d+/)?.[0] || null;
|
||||
curatedProfile.thigh = Number(profile.thigh) || profile.thigh?.match?.(/\d+/)?.[0] || null;
|
||||
curatedProfile.foot = Number(profile.foot) || profile.foot?.match?.(/\d+/)?.[0] || null;
|
||||
curatedProfile.leg = Math.round(profile.leg || profile.leg?.match?.(/\d+/)?.[0]) || null;
|
||||
curatedProfile.thigh = Math.round(profile.thigh || profile.thigh?.match?.(/\d+/)?.[0]) || null;
|
||||
curatedProfile.foot = Number(profile.foot || profile.foot?.match?.(/\d+/)?.[0]) || null;
|
||||
curatedProfile.shoeSize = Number(profile.shoeSize || profile.shoeSize?.match?.(/\d+/)?.[0]) || null;
|
||||
|
||||
// combined measurement value
|
||||
// ExCoGi uses x, Jules Jordan has spaces between the dashes, SpermMenia/Cum Buffet sometimes misses cup
|
||||
const measurements = profile.measurements?.match(/(\d+)([a-z]+)?(?:\s*[-x]\s*(\d+)\s*[-x]\s*(\d+))?/i);
|
||||
|
||||
if (measurements) {
|
||||
curatedProfile.bust = Number(measurements[1]) || null;
|
||||
curatedProfile.bust = Math.round(measurements[1]) || null;
|
||||
curatedProfile.cup = measurements[2] || null;
|
||||
curatedProfile.waist = Number(measurements[3]) || null;
|
||||
curatedProfile.hip = Number(measurements[4]) || null;
|
||||
curatedProfile.waist = Math.round(measurements[3]) || null;
|
||||
curatedProfile.hip = Math.round(measurements[4]) || null;
|
||||
}
|
||||
|
||||
curatedProfile.penisLength = Number(profile.penisLength) || profile.penisLength?.match?.(/\d+/)?.[0] || null;
|
||||
curatedProfile.penisGirth = Number(profile.penisGirth) || profile.penisGirth?.match?.(/\d+/)?.[0] || null;
|
||||
curatedProfile.penisLength = Math.round(profile.penisLength || profile.penisLength?.match?.(/\d+/)?.[0]) || null;
|
||||
curatedProfile.penisGirth = Math.round(profile.penisGirth || profile.penisGirth?.match?.(/\d+/)?.[0]) || null;
|
||||
|
||||
curatedProfile.isCircumcised = getBoolean(profile.isCircumcised);
|
||||
curatedProfile.naturalBoobs = getBoolean(profile.naturalBoobs);
|
||||
@@ -544,7 +547,7 @@ async function curateProfile(profile, actor) {
|
||||
|
||||
async function insertProfiles(newProfiles) {
|
||||
if (newProfiles.length > 0) {
|
||||
const entries = await bulkInsert('actors_profiles', newProfiles);
|
||||
const entries = await batchInsert('actors_profiles', newProfiles);
|
||||
|
||||
logger.info(`Saved ${newProfiles.length} actor profiles`);
|
||||
|
||||
@@ -606,10 +609,7 @@ async function upsertProfiles(profiles) {
|
||||
}));
|
||||
|
||||
if (avatars.length > 0) {
|
||||
await knex('actors_avatars')
|
||||
.insert(avatars)
|
||||
.onConflict()
|
||||
.ignore();
|
||||
await batchInsert('actors_avatars', avatars, { conflict: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -624,7 +624,7 @@ async function scrapeProfiles(actor, sources, entitiesBySlug, existingProfilesBy
|
||||
try {
|
||||
const entity = entitiesBySlug[scraperSlug] || null;
|
||||
|
||||
const scraper = scrapers[scraperSlug];
|
||||
const scraper = actorScrapers[scraperSlug];
|
||||
const layoutScraper = resolveLayoutScraper(entity, scraper);
|
||||
|
||||
if (!layoutScraper?.fetchProfile) {
|
||||
@@ -759,7 +759,8 @@ function curateSocials(socials, platformsByHostname) {
|
||||
|
||||
async function associateSocials(profiles) {
|
||||
const { platformsByHostname } = await actorsCommon;
|
||||
const profileEntries = await knex('actors_profiles').whereIn(['actor_id', 'entity_id'], profiles.map((profile) => [profile.actorId, profile.entity.id]));
|
||||
const profileEntryChunks = await Promise.all(chunk(profiles).map((profilesChunk) => knex('actors_profiles').whereIn(['actor_id', 'entity_id'], profilesChunk.map((profile) => [profile.actorId, profile.entity.id]))));
|
||||
const profileEntries = profileEntryChunks.flat();
|
||||
|
||||
const profileEntriesByActorIdAndEntityId = profileEntries.reduce((acc, profileEntry) => {
|
||||
if (!acc[profileEntry.actor_id]) {
|
||||
@@ -784,16 +785,14 @@ async function associateSocials(profiles) {
|
||||
return;
|
||||
}
|
||||
|
||||
await knex('actors_socials')
|
||||
.insert(curateSocials(profile.social, platformsByHostname).map((social) => ({
|
||||
platform: social.platform,
|
||||
handle: social.handle,
|
||||
url: social.url,
|
||||
actor_id: profile.actorId,
|
||||
// profile_id: profileId,
|
||||
})))
|
||||
.onConflict()
|
||||
.ignore();
|
||||
await batchInsert('actors_socials', curateSocials(profile.social, platformsByHostname).map((social) => ({
|
||||
platform: social.platform,
|
||||
handle: social.handle,
|
||||
url: social.url,
|
||||
actor_id: profile.actorId,
|
||||
})), {
|
||||
conflict: false,
|
||||
});
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
@@ -832,7 +831,7 @@ async function scrapeActors(argNames) {
|
||||
|
||||
logger.info(`Scraping profiles for ${actorNames.length} actors`);
|
||||
|
||||
const sources = argv.profileSources || config.profiles || Object.keys(scrapers.actors);
|
||||
const sources = argv.profileSources || config.profiles || Object.keys(actorScrapers);
|
||||
const entitySlugs = sources.flat();
|
||||
|
||||
const [entitiesBySlug, existingActorEntries] = await Promise.all([
|
||||
|
||||
22
src/knex.js
22
src/knex.js
@@ -3,7 +3,7 @@
|
||||
const config = require('config');
|
||||
const knex = require('knex');
|
||||
|
||||
module.exports = knex({
|
||||
const knexInstance = knex({
|
||||
client: 'pg',
|
||||
connection: config.database.owner,
|
||||
pool: config.database.pool,
|
||||
@@ -11,3 +11,23 @@ module.exports = knex({
|
||||
asyncStackTraces: process.env.NODE_ENV === 'development',
|
||||
// debug: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
knexInstance.on('query', function onQuery(query) {
|
||||
const bindingCount = query.bindings?.length ?? 0;
|
||||
|
||||
if (bindingCount > 50000) {
|
||||
const error = new Error(`[knex] Dangerous query: ${bindingCount} bindings detected: ${query.sql?.slice(0, 200)}${query.sql?.length > 200 ? '...' : ''}`);
|
||||
|
||||
Error.captureStackTrace(error, onQuery);
|
||||
// console.error(error);
|
||||
|
||||
throw error; // optionally hard-fail so you get a real stack trace
|
||||
}
|
||||
});
|
||||
|
||||
knexInstance.on('query-error', (error, query) => {
|
||||
error.knexSql = `${query.sql?.slice(0, 200)}${query.sql?.length > 200 ? '...' : ''}`;
|
||||
error.knexBindingCount = query.bindings?.length;
|
||||
});
|
||||
|
||||
module.exports = knexInstance;
|
||||
|
||||
@@ -23,7 +23,7 @@ const logger = require('./logger')(__filename);
|
||||
const argv = require('./argv');
|
||||
const knex = require('./knex');
|
||||
const http = require('./utils/http');
|
||||
const bulkInsert = require('./utils/bulk-insert');
|
||||
const batchInsert = require('./utils/batch-insert');
|
||||
const chunk = require('./utils/chunk');
|
||||
const { get } = require('./utils/qu');
|
||||
const { fetchEntityReleaseIds } = require('./entity-releases');
|
||||
@@ -647,6 +647,7 @@ async function fetchHttpSource(source, tempFileTarget, hashStream) {
|
||||
const res = await http.get(source.src, {
|
||||
limits: 'media',
|
||||
headers: {
|
||||
host: new URL(source.src).hostname,
|
||||
...(source.referer && { referer: source.referer }),
|
||||
...(source.host && { host: source.host }),
|
||||
},
|
||||
@@ -923,7 +924,7 @@ async function storeMedias(baseMedias, options) {
|
||||
const newMediaEntries = newMediaWithEntries.filter((media) => media.newEntry).map((media) => media.entry);
|
||||
|
||||
try {
|
||||
await bulkInsert('media', newMediaEntries, false);
|
||||
await batchInsert('media', newMediaEntries, { confict: false });
|
||||
|
||||
return [...newMediaWithEntries, ...existingHashMedias];
|
||||
} catch (error) {
|
||||
@@ -992,11 +993,11 @@ async function associateReleaseMedia(releases, type = 'release') {
|
||||
.filter(Boolean);
|
||||
|
||||
if (associations.length > 0) {
|
||||
await bulkInsert(`${type}s_${role}`, associations, false);
|
||||
await batchInsert(`${type}s_${role}`, associations, { conflict: false });
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.entries) {
|
||||
logger.error(util.inspect(error.entries, null, null, { color: true }));
|
||||
logger.error(util.inspect(error.entries.slice(0, 2), null, null, { color: true }), `${Math.min(error.entries.length, 2)} of ${error.length}`);
|
||||
}
|
||||
|
||||
logger.error(`Failed to store ${type} ${role}: ${error.message} (${error.detail || 'no detail'})`);
|
||||
|
||||
@@ -5,7 +5,7 @@ const angelogodshackoriginal = require('./angelogodshackoriginal');
|
||||
// const americanpornstar = require('./americanpornstar'); // offline
|
||||
const aziani = require('./aziani');
|
||||
const badoink = require('./badoink');
|
||||
// const bamvisions = require('./bamvisions');
|
||||
const bamvisions = require('./bamvisions');
|
||||
const bang = require('./bang');
|
||||
const bradmontana = require('./bradmontana');
|
||||
const cherrypimps = require('./cherrypimps');
|
||||
@@ -140,6 +140,7 @@ module.exports = {
|
||||
purgatoryx: radical,
|
||||
topwebmodels: radical,
|
||||
lucidflix: radical,
|
||||
hardwerk: radical,
|
||||
// hush / hussiepass
|
||||
eyeontheguy: hush,
|
||||
hushpass: hush,
|
||||
@@ -215,7 +216,7 @@ module.exports = {
|
||||
angelogodshackoriginal,
|
||||
babevr: badoink,
|
||||
badoinkvr: badoink,
|
||||
// bamvisions, // DNS error, site offline?
|
||||
bamvisions,
|
||||
bang,
|
||||
meidenvanholland: bluedonkeymedia, // Vurig Vlaanderen uses same database
|
||||
boobpedia,
|
||||
|
||||
@@ -57,7 +57,7 @@ function getCovers(images, target = 'cover') {
|
||||
}
|
||||
|
||||
function getVideos(data) {
|
||||
const teaserSources = data.videos.mediabook?.files;
|
||||
const teaserSources = data.videos?.mediabook?.files;
|
||||
const trailerSources = data.children.find((child) => child.type === 'trailer')?.videos.full?.files;
|
||||
|
||||
const teaser = teaserSources && Object.values(teaserSources).map((source) => ({
|
||||
@@ -84,7 +84,7 @@ function scrapeLatestX(data, site, filterChannel, options) {
|
||||
|
||||
release.url = `${basepath}/${data.id}/${slugify(release.title)}`; // spartanId doesn't work in URLs
|
||||
release.date = new Date(data.dateReleased);
|
||||
release.duration = data.videos.mediabook?.length > 1 ? data.videos.mediabook.length : null;
|
||||
release.duration = data.videos?.mediabook?.length > 1 ? data.videos.mediabook.length : null;
|
||||
|
||||
release.actors = data.actors.map((actor) => ({ name: actor.name, gender: actor.gender }));
|
||||
release.tags = data.tags.map((tag) => tag.name);
|
||||
@@ -127,6 +127,10 @@ async function scrapeLatest(items, site, filterChannel, options) {
|
||||
}
|
||||
|
||||
function scrapeRelease(data, url, channel, networkName, options) {
|
||||
if (Array.isArray(data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const release = {};
|
||||
|
||||
const { title, description } = data;
|
||||
@@ -136,7 +140,7 @@ function scrapeRelease(data, url, channel, networkName, options) {
|
||||
release.description = description;
|
||||
|
||||
release.date = new Date(data.dateReleased);
|
||||
release.duration = data.videos.mediabook?.length > 1 ? data.videos.mediabook.length : null;
|
||||
release.duration = data.videos?.mediabook?.length > 1 ? data.videos.mediabook.length : null;
|
||||
|
||||
release.actors = data.actors.map((actor) => ({ name: actor.name, gender: actor.gender }));
|
||||
release.tags = data.tags.map((tag) => tag.name);
|
||||
@@ -144,7 +148,6 @@ function scrapeRelease(data, url, channel, networkName, options) {
|
||||
[release.poster, ...release.photos] = getThumbs(data).map((src) => ({
|
||||
src,
|
||||
referer: url,
|
||||
host: 'mediavault-private-fl.project1content.com',
|
||||
}));
|
||||
|
||||
const { teaser, trailer } = getVideos(data);
|
||||
@@ -270,7 +273,7 @@ async function fetchLatest(site, page = 1, options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { instanceToken } = options.beforeNetwork?.instanceToken
|
||||
const { instanceToken } = options.beforeNetwork?.instanceToken && !(options.parameters?.native || options.parameters?.childSession || options.parameters?.parentSession === false)
|
||||
? options.beforeNetwork
|
||||
: await getSession(site, options.parameters, url);
|
||||
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
fetchLatest,
|
||||
fetchApiLatest,
|
||||
fetchUpcoming,
|
||||
fetchApiUpcoming,
|
||||
fetchScene,
|
||||
fetchProfile,
|
||||
fetchApiProfile,
|
||||
scrapeAll,
|
||||
} = require('./gamma');
|
||||
|
||||
const { get } = require('../utils/qu');
|
||||
const slugify = require('../utils/slugify');
|
||||
|
||||
function extractLowArtActors(release) {
|
||||
const actors = release.title
|
||||
.replace(/solo/i, '')
|
||||
.split(/,|\band\b/ig)
|
||||
.map((actor) => actor.trim());
|
||||
|
||||
return {
|
||||
...release,
|
||||
actors,
|
||||
};
|
||||
}
|
||||
|
||||
async function networkFetchLatest(site, page = 1) {
|
||||
if (site.parameters?.api) return fetchApiLatest(site, page, false);
|
||||
|
||||
const releases = await fetchLatest(site, page);
|
||||
|
||||
if (site.slug === 'lowartfilms') {
|
||||
return releases.map((release) => extractLowArtActors(release));
|
||||
}
|
||||
|
||||
return releases;
|
||||
}
|
||||
|
||||
async function networkFetchScene(url, site) {
|
||||
const release = await fetchScene(url, site);
|
||||
|
||||
if (site.slug === 'lowartfilms') {
|
||||
return extractLowArtActors(release);
|
||||
}
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
async function networkFetchUpcoming(site, page = 1) {
|
||||
if (site.parameters?.api) return fetchApiUpcoming(site, page, true);
|
||||
|
||||
return fetchUpcoming(site, page);
|
||||
}
|
||||
|
||||
function getActorReleasesUrl(actorPath, page = 1) {
|
||||
return `https://www.peternorth.com/en/videos/All-Categories/0${actorPath}/All-Dvds/0/latest/${page}`;
|
||||
}
|
||||
|
||||
function scrapeClassicProfile({ qu, html }, site) {
|
||||
const profile = {};
|
||||
|
||||
profile.avatar = qu.img('.actorPicture');
|
||||
profile.releases = scrapeAll(html, null, site.url, false);
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
async function fetchClassicProfile(actorName, { site }) {
|
||||
const actorSlug = slugify(actorName);
|
||||
|
||||
const url = `${site.url}/en/pornstars`;
|
||||
const pornstarsRes = await get(url);
|
||||
|
||||
if (!pornstarsRes.ok) return null;
|
||||
|
||||
const actorPath = pornstarsRes.item.qa('option[value*="/pornstar"]')
|
||||
.find((el) => slugify(el.textContent) === actorSlug)
|
||||
?.value;
|
||||
|
||||
if (actorPath) {
|
||||
const actorUrl = `${site.url}${actorPath}`;
|
||||
const res = await get(actorUrl);
|
||||
|
||||
if (res.ok) {
|
||||
return scrapeClassicProfile(res.item, site);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function networkFetchProfile({ name: actorName }, context, include) {
|
||||
const profile = await ((context.site.parameters?.api && fetchApiProfile(actorName, context, include))
|
||||
|| (context.site.parameters?.classic && include.scenes && fetchClassicProfile(actorName, context, include)) // classic profiles only have scenes, no bio
|
||||
|| fetchProfile({ name: actorName }, context, true, getActorReleasesUrl, include));
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchLatest: networkFetchLatest,
|
||||
fetchProfile: networkFetchProfile,
|
||||
fetchScene: networkFetchScene,
|
||||
fetchUpcoming: networkFetchUpcoming,
|
||||
};
|
||||
@@ -1,217 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const Promise = require('bluebird');
|
||||
const util = require('util');
|
||||
const { JSDOM } = require('jsdom');
|
||||
const moment = require('moment');
|
||||
const format = require('template-format');
|
||||
const unprint = require('unprint');
|
||||
|
||||
const logger = require('../logger')(__filename);
|
||||
const qu = require('../utils/qu');
|
||||
const http = require('../utils/http');
|
||||
const slugify = require('../utils/slugify');
|
||||
|
||||
function getApiUrl(appId, apiKey) {
|
||||
const userAgent = 'Algolia for vanilla JavaScript (lite) 3.27.0;instantsearch.js 2.7.4;JS Helper 2.26.0';
|
||||
|
||||
const apiUrl = `https://${appId.toLowerCase()}-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=${userAgent}&x-algolia-application-id=${appId}&x-algolia-api-key=${apiKey}`;
|
||||
|
||||
return {
|
||||
appId,
|
||||
apiKey,
|
||||
userAgent,
|
||||
apiUrl,
|
||||
};
|
||||
}
|
||||
|
||||
function getAvatarFallbacks(avatar) {
|
||||
if (!avatar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
avatar.replace(/\d+x\d+/, '500x750'),
|
||||
avatar.replace(/\d+x\d+/, '240x360'),
|
||||
avatar.replace(/\d+x\d+/, '200x300'),
|
||||
avatar,
|
||||
];
|
||||
}
|
||||
|
||||
async function fetchApiCredentials(referer, site) {
|
||||
if (site?.parameters?.appId && site?.parameters?.apiKey) {
|
||||
return getApiUrl(site.parameters.appId, site.parameters.apiKey);
|
||||
}
|
||||
|
||||
const res = await http.get(referer);
|
||||
const body = res.body.toString();
|
||||
|
||||
const apiLine = body.split('\n').find((bodyLine) => bodyLine.match('apiKey'));
|
||||
|
||||
if (!apiLine) {
|
||||
throw new Error(`No Gamma API key found for ${referer}`);
|
||||
}
|
||||
|
||||
const apiSerial = apiLine.slice(apiLine.indexOf('{'), apiLine.indexOf('};') + 1);
|
||||
const apiData = JSON.parse(apiSerial);
|
||||
|
||||
const { applicationID: appId, apiKey } = apiData.api.algolia;
|
||||
|
||||
return getApiUrl(appId, apiKey);
|
||||
}
|
||||
|
||||
function getAlbumUrl(albumPath, site) {
|
||||
if (site.parameters?.photos) {
|
||||
return /^http/.test(site.parameters.photos)
|
||||
? `${site.parameters.photos}/${albumPath.split('/').slice(-2).join('/')}`
|
||||
: `${site.url}${site.parameters.photos}/${albumPath.split('/').slice(-2).join('/')}`;
|
||||
}
|
||||
|
||||
if (site.url && site.parameters?.photos !== false) {
|
||||
return `${site.url}${albumPath}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchPhotos(url) {
|
||||
const res = await qu.get(url);
|
||||
|
||||
return res.item;
|
||||
}
|
||||
|
||||
function scrapePhotos({ query }, includeThumbnails = true) {
|
||||
return query.all('.preview .imgLink, .pgFooterThumb a').map((linkEl) => {
|
||||
const url = linkEl.href;
|
||||
|
||||
if (/\/join|\/createaccount/.test(url)) {
|
||||
// URL links to join page instead of full photo, extract thumbnail
|
||||
// /createaccount is used by e.g. Tricky Spa native site
|
||||
const src = query.img(linkEl);
|
||||
|
||||
if (/previews\//.test(src)) {
|
||||
// resource often serves full photo at a modifier URL anyway, add as primary source
|
||||
const highRes = src
|
||||
.replace('previews/', '')
|
||||
.replace('_tb.jpg', '.jpg');
|
||||
|
||||
// keep original thumbnail as fallback in case full photo is not available
|
||||
return [highRes, src];
|
||||
}
|
||||
|
||||
if (!includeThumbnails) return null;
|
||||
|
||||
return src;
|
||||
}
|
||||
|
||||
// URL links to full photo
|
||||
return url;
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
async function getPhotos(albumPath, site, includeThumbnails = true) {
|
||||
const albumUrl = getAlbumUrl(albumPath, site);
|
||||
|
||||
if (!albumUrl) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const item = await fetchPhotos(albumUrl);
|
||||
const photos = scrapePhotos(item, includeThumbnails);
|
||||
|
||||
const lastPage = item.query.url('.Gamma_Paginator a.last')?.match(/\d+$/)[0];
|
||||
|
||||
if (lastPage) {
|
||||
const otherPages = Array.from({ length: Number(lastPage) }, (_value, index) => index + 1).slice(1);
|
||||
|
||||
const otherPhotos = await Promise.map(otherPages, async (page) => {
|
||||
const pageItem = await fetchPhotos(`${albumUrl}/${page}`);
|
||||
|
||||
return scrapePhotos(pageItem, includeThumbnails);
|
||||
}, {
|
||||
concurrency: 2,
|
||||
});
|
||||
|
||||
return photos.concat(otherPhotos.flat());
|
||||
}
|
||||
|
||||
return photos;
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to fetch ${site.name} photos from ${albumUrl}: ${error.message}`);
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getFullPhotos(entryId, site, parameters) {
|
||||
const res = await http.get(`${parameters.album || site.url}/media/signPhotoset/${entryId}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
});
|
||||
|
||||
if (res.ok && typeof res.body === 'object') { // gives 200 OK even when redirected to signup page
|
||||
return Object.values(res.body);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function getThumbs(entryId, site, parameters) {
|
||||
const referer = parameters?.referer || `${parameters?.networkReferer ? site.parent.url : site.url}/en/videos`;
|
||||
const { apiUrl } = await fetchApiCredentials(referer, site);
|
||||
|
||||
const res = await http.post(apiUrl, {
|
||||
requests: [
|
||||
{
|
||||
indexName: 'all_photosets',
|
||||
params: `query=&page=0&facets=[]&tagFilters=&facetFilters=[["set_id:${entryId}"]]`,
|
||||
},
|
||||
],
|
||||
}, {
|
||||
headers: {
|
||||
Referer: referer,
|
||||
},
|
||||
}, {
|
||||
encodeJSON: true,
|
||||
});
|
||||
|
||||
if (res.ok && res.body.results?.[0]?.hits[0]?.set_pictures) {
|
||||
return res.body.results[0].hits[0].set_pictures.map((img) => img.thumb_path && ([
|
||||
`https://images-fame.gammacdn.com/photo_set${img.thumb_path}`,
|
||||
`https://images01-fame.gammacdn.com/photo_set${img.thumb_path}`,
|
||||
`https://images02-fame.gammacdn.com/photo_set${img.thumb_path}`,
|
||||
`https://images03-fame.gammacdn.com/photo_set${img.thumb_path}`,
|
||||
`https://images04-fame.gammacdn.com/photo_set${img.thumb_path}`,
|
||||
`https://images-evilangel.gammacdn.com/photo_set${img.thumb_path}`,
|
||||
`https://transform.gammacdn.com/photo_set${img.thumb_path}`,
|
||||
])).filter(Boolean);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function getPhotosApi(entryId, site, parameters) {
|
||||
const [photos, thumbs] = await Promise.all([
|
||||
getFullPhotos(entryId, site, parameters).catch(() => { logger.error(`Gamma scraper failed to fetch photos for ${entryId}`); return []; }),
|
||||
getThumbs(entryId, site, parameters).catch(() => { logger.error(`Gamma scraper failed to fetch photos for ${entryId}`); return []; }),
|
||||
]);
|
||||
|
||||
return photos.concat(thumbs.slice(photos.length));
|
||||
}
|
||||
|
||||
function getImageSources(source) {
|
||||
return [
|
||||
`https://images-fame.gammacdn.com/movies${source}`,
|
||||
`https://images01-fame.gammacdn.com/movies${source}`,
|
||||
`https://images02-fame.gammacdn.com/movies${source}`,
|
||||
`https://images03-fame.gammacdn.com/movies${source}`,
|
||||
`https://images04-fame.gammacdn.com/movies${source}`,
|
||||
`https://images-evilangel.gammacdn.com/movies${source}`,
|
||||
`https://transform.gammacdn.com/movies${source}`,
|
||||
];
|
||||
}
|
||||
|
||||
function curateTitle(title, channel) {
|
||||
// some videos are redundantly prefixed with the name of the site, i.e. Bubblegum Dungeon, Forbidden Seductions and Lady Gonzo
|
||||
return title.replace(new RegExp(`^\\s*${channel.name}\\s*[:|-]\\s`, 'i'), '');
|
||||
@@ -227,6 +21,10 @@ async function scrapeApiReleases(json, site, options) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (options.parameters?.filterChannel && scene.mainChannel?.id !== options.parameters.filterChannel) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const release = {
|
||||
entryId: scene.clip_id,
|
||||
description: scene.description,
|
||||
@@ -241,10 +39,11 @@ async function scrapeApiReleases(json, site, options) {
|
||||
if (typeof options.parameters?.scene === 'string') {
|
||||
release.url = `${options.parameters.scene}${release.path}`;
|
||||
} else if (site.url && options.parameters?.scene !== false) {
|
||||
release.url = `${site.url}/en/video${release.path}`;
|
||||
const siteSlug = new URL(site.url).pathname.match(/\/sites\/(\w+)/)?.[1];
|
||||
release.url = unprint.prefixUrl(`/en/video${siteSlug ? `/${siteSlug}` : ''}${release.path}`, site.origin);
|
||||
}
|
||||
|
||||
release.date = moment.utc(scene.release_date, 'YYYY-MM-DD').toDate();
|
||||
release.date = unprint.extractDate(scene.release_date, 'YYYY-MM-DD');
|
||||
release.director = scene.directors[0]?.name || null;
|
||||
|
||||
release.actors = scene.actors.map((actor) => ({
|
||||
@@ -299,8 +98,41 @@ async function scrapeApiReleases(json, site, options) {
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchLatestApi(site, page = 1, options, _preData, upcoming = false) {
|
||||
const referer = options.parameters?.referer || `${options.parameters?.networkReferer ? site.parent.url : site.url}/en/videos`;
|
||||
function getApiUrl(appId, apiKey) {
|
||||
const userAgent = 'Algolia for vanilla JavaScript (lite) 3.27.0;instantsearch.js 2.7.4;JS Helper 2.26.0';
|
||||
|
||||
const apiUrl = `https://${appId.toLowerCase()}-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=${userAgent}&x-algolia-application-id=${appId}&x-algolia-api-key=${apiKey}`;
|
||||
|
||||
return {
|
||||
appId,
|
||||
apiKey,
|
||||
userAgent,
|
||||
apiUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchApiCredentials(referer, site) {
|
||||
if (site?.parameters?.appId && site?.parameters?.apiKey) {
|
||||
return getApiUrl(site.parameters.appId, site.parameters.apiKey);
|
||||
}
|
||||
|
||||
const res = await unprint.get(referer);
|
||||
const apiLine = res.body.split('\n').find((bodyLine) => bodyLine.match('apiKey'));
|
||||
|
||||
if (!apiLine) {
|
||||
throw new Error(`No Gamma API key found for ${referer}`);
|
||||
}
|
||||
|
||||
const apiSerial = apiLine.slice(apiLine.indexOf('{'), apiLine.indexOf('};') + 1);
|
||||
const apiData = JSON.parse(apiSerial);
|
||||
|
||||
const { applicationID: appId, apiKey } = apiData.api.algolia;
|
||||
|
||||
return getApiUrl(appId, apiKey);
|
||||
}
|
||||
|
||||
async function fetchLatest(site, page = 1, options, _preData, upcoming = false) {
|
||||
const referer = options.parameters?.referer || `${options.parameters?.networkReferer ? site.parent.origin : site.origin}/en/videos`;
|
||||
const { apiUrl } = await fetchApiCredentials(referer, site);
|
||||
const slug = options.parameters.querySlug || site.slug;
|
||||
|
||||
@@ -308,7 +140,7 @@ async function fetchLatestApi(site, page = 1, options, _preData, upcoming = fals
|
||||
? `&filters=channels.id:${options.parameters.queryChannel === true ? slug : options.parameters.queryChannel}`
|
||||
: `&filters=availableOnSite:${slug}`}`;
|
||||
|
||||
const res = await http.post(apiUrl, {
|
||||
const res = await unprint.post(apiUrl, {
|
||||
requests: [
|
||||
{
|
||||
indexName: 'all_scenes',
|
||||
@@ -319,213 +151,97 @@ async function fetchLatestApi(site, page = 1, options, _preData, upcoming = fals
|
||||
headers: {
|
||||
Referer: referer,
|
||||
},
|
||||
}, {
|
||||
encodeJSON: true,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.body.results?.[0]?.hits) {
|
||||
return scrapeApiReleases(res.body.results[0].hits, site, options);
|
||||
if (res.ok && res.data.results?.[0]?.hits) {
|
||||
return scrapeApiReleases(res.data.results[0].hits, site, options);
|
||||
}
|
||||
|
||||
return res.status;
|
||||
}
|
||||
|
||||
function scrapeAll(scenes, site, networkUrl, hasTeaser = true) {
|
||||
return scenes.map(({ query, el }) => {
|
||||
const release = {};
|
||||
|
||||
release.url = query.url('.sceneTitle a, .tlcTitle a', 'href', { origin: networkUrl ? site.parent.url : site.url });
|
||||
|
||||
release.title = query.cnt('.sceneTitle a', 'tlcTitle a', 'title');
|
||||
release.entryId = el.dataset.itemid;
|
||||
|
||||
release.date = query.date('.sceneDate, .tlcSpecsDate .tlcDetailsValue', ['MM-DD-YYYY', 'YYYY-MM-DD']);
|
||||
release.actors = query.cnts('.sceneActors a, .tlcActors a', ' title');
|
||||
|
||||
[release.likes, release.dislikes] = query.all('.value').map((likeEl) => query.number(likeEl));
|
||||
|
||||
release.poster = query.img('.imgLink img, .tlcImageItem', 'data-original') || query.img('.imgLink img, .tlcImageItem');
|
||||
|
||||
if (hasTeaser) {
|
||||
release.teaser = [
|
||||
{ src: `https://videothumb.gammacdn.com/600x339/${release.entryId}.mp4` },
|
||||
{ src: `https://videothumb.gammacdn.com/307x224/${release.entryId}.mp4` },
|
||||
];
|
||||
}
|
||||
|
||||
release.channel = query.el('.fromSite a', 'title')?.replace('.com', '');
|
||||
|
||||
return release;
|
||||
});
|
||||
}
|
||||
|
||||
function getLatestUrl(site, page) {
|
||||
if (site.parameters?.latest) {
|
||||
if (/^http/.test(site.parameters.latest)) {
|
||||
return /%d/.test(site.parameters.latest)
|
||||
? util.format(site.parameters.latest, page)
|
||||
: `${site.parameters.latest}${page}`;
|
||||
}
|
||||
|
||||
return /%d/.test(site.parameters.latest)
|
||||
? util.format(`${site.url}${site.parameters.latest}`, page)
|
||||
: `${site.url}${site.parameters.latest}${page}`;
|
||||
}
|
||||
|
||||
return `${site.url}/en/videos/AllCategories/0/${page}`;
|
||||
}
|
||||
|
||||
function getUpcomingUrl(site) {
|
||||
if (site.parameters?.upcoming) {
|
||||
return /^http/.test(site.parameters.upcoming)
|
||||
? `${site.parameters.upcoming}`
|
||||
: `${site.url}${site.parameters.upcoming}`;
|
||||
}
|
||||
|
||||
return `${site.url}/en/videos/AllCategories/0/1/upcoming`;
|
||||
}
|
||||
|
||||
async function fetchLatest(site, page = 1) {
|
||||
const url = getLatestUrl(site, page);
|
||||
const res = await qu.getAll(url, 'li[data-itemtype=scene], div[data-itemtype*=scene]');
|
||||
|
||||
if (res.ok) {
|
||||
return scrapeAll(res.items, site);
|
||||
}
|
||||
|
||||
return res.status;
|
||||
}
|
||||
|
||||
async function fetchUpcoming(site) {
|
||||
const url = getUpcomingUrl(site);
|
||||
const res = await qu.getAll(url, 'li[data-itemtype=scene], div[data-itemtype*=scene]');
|
||||
|
||||
if (res.ok) {
|
||||
return scrapeAll(res.items, site, null, false);
|
||||
}
|
||||
|
||||
return res.status;
|
||||
}
|
||||
|
||||
async function scrapeScene({ query }, url, channel, baseRelease, mobileItem, options) {
|
||||
const release = { query }; // used by XEmpire scraper to resolve channel-specific details
|
||||
|
||||
const json = query.html('script[type="application/ld+json"]');
|
||||
const videoJson = query.htmls('script').find((script) => /ScenePlayerOptions/i.test(script));
|
||||
|
||||
const [data, data2] = json ? JSON.parse(json) : [];
|
||||
const videoData = videoJson && JSON.parse(videoJson.slice(videoJson.indexOf('{'), videoJson.indexOf('};') + 1));
|
||||
|
||||
release.entryId = (baseRelease?.path || new URL(url).pathname).match(/\/(\d{2,})(\/|$)/)?.[1];
|
||||
release.title = videoData?.playerOptions?.sceneInfos.sceneTitle || data?.name;
|
||||
release.description = data?.description;
|
||||
|
||||
release.date = query.date('.updatedDate', ['MM-DD-YYYY', 'YYYY-MM-DD'])
|
||||
|| qu.extractDate(data?.dateCreated, 'YYYY-MM-DD')
|
||||
|| videoData?.playerOptions?.sceneInfos.sceneReleaseDate;
|
||||
|
||||
release.actors = (data?.actor || data2?.actor)?.map((actor) => ({
|
||||
name: actor.name,
|
||||
gender: actor.gender,
|
||||
})) || [];
|
||||
|
||||
release.duration = qu.durationToSeconds(data.duration);
|
||||
release.director = data?.director?.[0]?.name || data2?.director?.[0]?.name;
|
||||
|
||||
release.tags = data?.keywords?.split(', ') || data2?.keywords?.split(', ') || [];
|
||||
release.stars = (data.aggregateRating.ratingValue / data.aggregateRating.bestRating) * 5 || null;
|
||||
|
||||
release.channel = slugify(data?.productionCompany?.name
|
||||
|| query.el('.studioLink a, .siteLink a', 'title')
|
||||
|| query.cnt('.siteNameSpan')?.toLowerCase().replace('.com', '')
|
||||
|| query.meta('meta[name="twitter:domain"]')?.replace('.com', ''), '');
|
||||
|
||||
if (videoData?.picPreview && new URL(videoData.picPreview).pathname.length > 1) {
|
||||
// sometimes links to just https://images02-fame.gammacdn.com/
|
||||
const poster = new URL(videoData.picPreview);
|
||||
|
||||
release.poster = [
|
||||
videoData.picPreview, // prefer original URL with width and height parameters, without may give a square crop on e.g. XEmpire
|
||||
`${poster.origin}${poster.pathname}`,
|
||||
];
|
||||
}
|
||||
|
||||
const photoLink = query.url('.picturesItem a');
|
||||
const mobilePhotos = mobileItem?.query.imgs('.preview-displayer a img') || [];
|
||||
|
||||
if (photoLink && options.includePhotos) {
|
||||
const photos = await getPhotos(photoLink, channel, mobilePhotos.length < 3); // only get thumbnails when less than 3 mobile photos are available
|
||||
|
||||
if (photos.length < 7) {
|
||||
release.photos = [...photos, ...mobilePhotos]; // probably only teaser photos available, supplement with mobile album
|
||||
} else {
|
||||
release.photos = photos;
|
||||
}
|
||||
} else {
|
||||
release.photos = mobilePhotos;
|
||||
}
|
||||
|
||||
const trailer = videoData && `${videoData.playerOptions.host}${videoData.url}`;
|
||||
|
||||
if (trailer) {
|
||||
release.trailer = [
|
||||
{
|
||||
src: trailer.replace('hd', 'sm'),
|
||||
quality: 240,
|
||||
},
|
||||
{
|
||||
src: trailer.replace('hd', 'med'),
|
||||
quality: 360,
|
||||
},
|
||||
{
|
||||
src: trailer.replace('hd', 'big'),
|
||||
quality: 480,
|
||||
},
|
||||
{
|
||||
// probably 540p
|
||||
src: trailer,
|
||||
quality: parseInt(videoData.sizeOnLoad, 10),
|
||||
},
|
||||
{
|
||||
src: trailer.replace('hd', '720p'),
|
||||
quality: 720,
|
||||
},
|
||||
{
|
||||
src: trailer.replace('hd', '1080p'),
|
||||
quality: 1080,
|
||||
},
|
||||
{
|
||||
src: trailer.replace('hd', '4k'),
|
||||
quality: 2160,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const movieUrl = query.url('.dvdLink', 'href', { origin: channel.url });
|
||||
|
||||
if (movieUrl) {
|
||||
release.movie = {
|
||||
url: movieUrl,
|
||||
title: query.el('.dvdLink', 'title'),
|
||||
entryId: movieUrl.match(/\/(\d+)(\/|$)/)?.[1],
|
||||
covers: [qu.imgs('.dvdLink img')],
|
||||
};
|
||||
}
|
||||
|
||||
return release;
|
||||
async function fetchUpcoming(site, page = 1, options, preData) {
|
||||
return fetchLatest(site, page, options, preData, true);
|
||||
}
|
||||
|
||||
const qualityMap = {
|
||||
'4k': 2160,
|
||||
};
|
||||
|
||||
async function scrapeReleaseApi(data, site, options, movieScenes) {
|
||||
async function getFullPhotos(entryId, site, parameters) {
|
||||
const res = await unprint.get(`${parameters.album || site.url}/media/signPhotoset/${entryId}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
});
|
||||
|
||||
if (res.ok && typeof res.data === 'object') { // gives 200 OK even when redirected to signup page
|
||||
return Object.values(res.data);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function getThumbs(entryId, site, parameters) {
|
||||
const referer = parameters?.referer || `${parameters?.networkReferer ? site.parent.origin : site.origin}/en/videos`;
|
||||
const { apiUrl } = await fetchApiCredentials(referer, site);
|
||||
|
||||
const res = await unprint.post(apiUrl, {
|
||||
requests: [
|
||||
{
|
||||
indexName: 'all_photosets',
|
||||
params: `query=&page=0&facets=[]&tagFilters=&facetFilters=[["set_id:${entryId}"]]`,
|
||||
},
|
||||
],
|
||||
}, {
|
||||
headers: {
|
||||
Referer: referer,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.ok && res.data.results?.[0]?.hits[0]?.set_pictures) {
|
||||
return res.data.results[0].hits[0].set_pictures.map((img) => img.thumb_path && [
|
||||
'https://images-fame.gammacdn.com',
|
||||
'https://images01-fame.gammacdn.com',
|
||||
'https://images02-fame.gammacdn.com',
|
||||
'https://images03-fame.gammacdn.com',
|
||||
'https://images04-fame.gammacdn.com',
|
||||
'https://images-evilangel.gammacdn.com',
|
||||
'https://transform.gammacdn.com',
|
||||
].map((origin) => unprint.prefixUrl(`/photo_set${img.thumb_path}`, origin))).filter(Boolean);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getImageSources(source) {
|
||||
return [
|
||||
'https://images-fame.gammacdn.com',
|
||||
'https://images01-fame.gammacdn.com',
|
||||
'https://images02-fame.gammacdn.com',
|
||||
'https://images03-fame.gammacdn.com',
|
||||
'https://images04-fame.gammacdn.com',
|
||||
'https://images-evilangel.gammacdn.com',
|
||||
'https://transform.gammacdn.com',
|
||||
].map((origin) => unprint.prefixUrl(`/movies${source}`, origin));
|
||||
}
|
||||
|
||||
async function getPhotosApi(entryId, site, parameters) {
|
||||
const [photos, thumbs] = await Promise.all([
|
||||
getFullPhotos(entryId, site, parameters).catch(() => { logger.error(`Gamma scraper failed to fetch photos for ${entryId}`); return []; }),
|
||||
getThumbs(entryId, site, parameters).catch(() => { logger.error(`Gamma scraper failed to fetch photos for ${entryId}`); return []; }),
|
||||
]);
|
||||
|
||||
return photos.concat(thumbs.slice(photos.length));
|
||||
}
|
||||
|
||||
async function scrapeScene(data, site, options, movieScenes) {
|
||||
const release = {};
|
||||
|
||||
release.entryId = data.clip_id || data.movie_id;
|
||||
release.title = curateTitle(data.title, site);
|
||||
release.duration = data.length;
|
||||
release.date = (data.date && new Date(data.date * 1000)) || qu.parseDate(data.release_date || data.last_modified, 'YYYY-MM-DD');
|
||||
release.date = (data.date && new Date(data.date * 1000)) || unprint.extractDate(data.release_date || data.last_modified, 'YYYY-MM-DD');
|
||||
release.director = data.directors[0]?.name || null;
|
||||
|
||||
release.actors = data.actors.map((actor) => ({
|
||||
@@ -534,7 +250,7 @@ async function scrapeReleaseApi(data, site, options, movieScenes) {
|
||||
gender: actor.gender,
|
||||
url: options.parameters?.actors
|
||||
? format(options.parameters.actors, { id: actor.actor_id, slug: actor.url_name })
|
||||
: qu.prefixUrl(`/en/pornstar/${actor.url_name}/${data.actor_id}`, site.url),
|
||||
: unprint.prefixUrl(`/en/pornstar/${actor.url_name}/${data.actor_id}`, site.url),
|
||||
}));
|
||||
|
||||
release.tags = data.categories.map((category) => category.name);
|
||||
@@ -571,12 +287,12 @@ async function scrapeReleaseApi(data, site, options, movieScenes) {
|
||||
release.movie = {
|
||||
entryId: data.movie_id,
|
||||
title: data.movie_title,
|
||||
url: qu.prefixUrl(`${data.url_movie_title}/${data.movie_id}`, options.parameters.movie ? options.parameters.movie : `${site.url}/en/movie`),
|
||||
url: unprint.prefixUrl(`${data.url_movie_title}/${data.movie_id}`, options.parameters.movie ? options.parameters.movie : `${site.url}/en/movie`),
|
||||
};
|
||||
}
|
||||
|
||||
if (movieScenes?.length > 0) {
|
||||
release.scenes = await Promise.all(movieScenes.map((movieScene) => scrapeReleaseApi(movieScene, site, options)));
|
||||
release.scenes = await Promise.all(movieScenes.map((movieScene) => scrapeScene(movieScene, site, options)));
|
||||
}
|
||||
|
||||
release.channel = slugify(data.mainChannel?.id || data.sitename, ''); // remove -
|
||||
@@ -585,148 +301,13 @@ async function scrapeReleaseApi(data, site, options, movieScenes) {
|
||||
return release;
|
||||
}
|
||||
|
||||
async function fetchMovieTrailer(release) {
|
||||
if (!release.entryId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = `https://www.evilangel.com/en/dvdtrailer/${release.entryId}`;
|
||||
const res = await qu.get(url);
|
||||
|
||||
if (!res.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trailerHost = res.html.match(/"host":\s*"(.*\.com)"/)?.[1].replace(/\\\//g, '/');
|
||||
const trailerPath = res.html.match(/"url":\s*"(.*\.mp4)"/)?.[1].replace(/\\\//g, '/');
|
||||
|
||||
if (trailerHost && trailerPath) {
|
||||
return qu.prefixUrl(trailerPath, trailerHost);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function scrapeMovie({ query, el }, url, entity, baseRelease, options) {
|
||||
const release = {};
|
||||
|
||||
const { dataLayer } = query.exec('//script[contains(text(), "dataLayer")]', ['dataLayer']);
|
||||
const rawData = dataLayer?.[0]?.dvdDetails;
|
||||
const data = rawData?.dvdId && rawData; // dvdDetails is mostly empty in some cache states
|
||||
|
||||
if (query.exists('.NotFound-Title')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
release.entryId = new URL(url).pathname.match(/\/(\d+)(\/|$)/)?.[1];
|
||||
|
||||
release.covers = [
|
||||
query.img('.frontCoverImg', 'href'),
|
||||
query.img('.backCoverImg', 'href'),
|
||||
];
|
||||
|
||||
release.description = query.cnt('.descriptionText');
|
||||
release.date = qu.extractDate(data?.dvdReleaseDate) || query.date('.updatedOn', 'YYYY-MM-DD');
|
||||
release.title = data?.dvdName || query.cnt('.dvdTitle');
|
||||
release.director = query.el('.directedBy a', 'title');
|
||||
|
||||
release.actors = data?.dvdActors.map((actor) => ({ name: actor.actorName, entryId: actor.actorId }))
|
||||
|| query.all('.actorCarousel a[href*="/pornstar"]').map((actorEl) => ({
|
||||
entryId: query.url(actorEl, null).match(/\/(\d+)/)?.[1],
|
||||
name: query.cnt(actorEl, 'span'),
|
||||
href: query.url(actorEl, null, 'href', { origin: entity.url }),
|
||||
avatar: getAvatarFallbacks(query.img(actorEl)),
|
||||
}));
|
||||
|
||||
release.tags = query.cnts('.dvdCol a');
|
||||
release.scenes = scrapeAll(qu.initAll(el, 'div[data-itemtype*=scene], li[data-itemtype*=scene]'), entity, entity.url);
|
||||
|
||||
if (options.includeTrailers) {
|
||||
release.trailer = await fetchMovieTrailer(release);
|
||||
}
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
function scrapeActorSearch(html, url, actorName) {
|
||||
const { document } = new JSDOM(html).window;
|
||||
const actorLink = document.querySelector(`a[title="${actorName}" i]`);
|
||||
|
||||
return actorLink ? actorLink.href : null;
|
||||
}
|
||||
|
||||
async function fetchActorReleases(profileUrl, getActorReleasesUrl, page = 1, accReleases = [], context) {
|
||||
const { origin, pathname } = new URL(profileUrl);
|
||||
const profilePath = `/${pathname.split('/').slice(-2).join('/')}`;
|
||||
|
||||
const url = (context.parameters.actorScenes && format(context.parameters.actorScenes, { path: profilePath, page }))
|
||||
|| getActorReleasesUrl?.(profilePath, page);
|
||||
|
||||
if (!url) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const res = await qu.get(url);
|
||||
|
||||
if (!res.ok) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const releases = scrapeAll(res.item.html, null, origin);
|
||||
const nextPage = res.item.query.url('.Gamma_Paginator a.next');
|
||||
|
||||
if (nextPage) {
|
||||
return fetchActorReleases(profileUrl, getActorReleasesUrl, page + 1, accReleases.concat(releases), context);
|
||||
}
|
||||
|
||||
return accReleases.concat(releases);
|
||||
}
|
||||
|
||||
async function scrapeProfile({ query }, url, actorName, _siteSlug, getActorReleasesUrl, withReleases, context) {
|
||||
const avatar = query.img('img.actorPicture');
|
||||
const hair = query.cnt('.actorProfile .attribute_hair_color');
|
||||
const height = query.cnt('.actorProfile .attribute_height');
|
||||
const weight = query.cnt('.actorProfile .attribute_weight');
|
||||
const alias = query.cnt('.actorProfile .attribute_alternate_names');
|
||||
const nationality = query.cnt('.actorProfile .attribute_home');
|
||||
|
||||
const profile = {
|
||||
name: actorName,
|
||||
};
|
||||
|
||||
if (avatar) {
|
||||
// larger sizes usually available, provide fallbacks
|
||||
const avatars = getAvatarFallbacks(avatar);
|
||||
|
||||
profile.avatar = avatars;
|
||||
}
|
||||
|
||||
profile.description = query.cnt('.actorBio p:not(.bioTitle)');
|
||||
|
||||
if (hair) profile.hairColor = hair.split(':')[1].trim();
|
||||
if (height) profile.height = Number(height.match(/\d+/)[0]);
|
||||
if (weight) profile.weight = Number(weight.match(/\d+/)[0]);
|
||||
if (alias) profile.aliases = alias.split(':')[1].trim().split(', ');
|
||||
if (nationality) profile.nationality = nationality.split(':')[1].trim();
|
||||
|
||||
if ((getActorReleasesUrl || context.parameters.actorScenes) && withReleases) {
|
||||
profile.releases = await fetchActorReleases(url, getActorReleasesUrl, 1, [], context);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
async function fetchUpcomingApi(site, page = 1, options, preData) {
|
||||
return fetchLatestApi(site, page, options, preData, true);
|
||||
}
|
||||
|
||||
async function fetchSceneApi(url, site, baseRelease, options) {
|
||||
const referer = options.parameters?.referer || `${site.parameters?.networkReferer ? site.parent.url : site.url}/en/videos`;
|
||||
async function fetchScene(url, site, baseRelease, options) {
|
||||
const referer = options.parameters?.referer || `${site.parameters?.networkReferer ? site.parent.origin : site.origin}/en/videos`;
|
||||
const { apiUrl } = await fetchApiCredentials(referer, site);
|
||||
|
||||
const entryId = (baseRelease?.path || new URL(url).pathname).match(/\/(\d{2,})(\/|$)/)?.[1];
|
||||
|
||||
const res = await http.post(apiUrl, {
|
||||
const res = await unprint.post(apiUrl, {
|
||||
requests: [
|
||||
{
|
||||
indexName: 'all_scenes',
|
||||
@@ -741,28 +322,26 @@ async function fetchSceneApi(url, site, baseRelease, options) {
|
||||
headers: {
|
||||
Referer: referer,
|
||||
},
|
||||
}, {
|
||||
encodeJSON: true,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.body.results?.[0]?.hits.length > 0) {
|
||||
return scrapeReleaseApi(res.body.results[0].hits[0], site, options);
|
||||
if (res.ok && res.data.results?.[0]?.hits.length > 0) {
|
||||
return scrapeScene(res.data.results[0].hits[0], site, options);
|
||||
}
|
||||
|
||||
if (res.status === 200) {
|
||||
if (res.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return res.status;
|
||||
}
|
||||
|
||||
async function fetchMovieApi(url, site, baseRelease, options) {
|
||||
async function fetchMovie(url, site, baseRelease, options) {
|
||||
const referer = options.parameters?.referer || `${site.parameters?.networkReferer ? site.parent.url : site.url}/en/movies`;
|
||||
const { apiUrl } = await fetchApiCredentials(referer, site);
|
||||
|
||||
const entryId = (baseRelease?.path || new URL(url).pathname).match(/\/(\d{2,})(\/|$)/)?.[1];
|
||||
|
||||
const res = await http.post(apiUrl, {
|
||||
const res = await unprint.post(apiUrl, {
|
||||
requests: [
|
||||
{
|
||||
indexName: 'all_movies',
|
||||
@@ -783,12 +362,10 @@ async function fetchMovieApi(url, site, baseRelease, options) {
|
||||
headers: {
|
||||
Referer: referer,
|
||||
},
|
||||
}, {
|
||||
encodeJSON: true,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.body.results?.[0]?.hits.length > 0) {
|
||||
return scrapeReleaseApi(res.body.results[0].hits[0], site, options, res.body.results[1]?.hits);
|
||||
if (res.ok && res.data.results?.[0]?.hits.length > 0) {
|
||||
return scrapeScene(res.data.results[0].hits[0], site, options, res.data.results[1]?.hits);
|
||||
}
|
||||
|
||||
if (res.status === 200) {
|
||||
@@ -798,62 +375,8 @@ async function fetchMovieApi(url, site, baseRelease, options) {
|
||||
return res.status;
|
||||
}
|
||||
|
||||
function getDeepUrl(url, site, baseRelease, mobile) {
|
||||
const filter = new Set(['en', 'video', 'scene', site.slug, site.parent.slug]);
|
||||
const pathname = baseRelease?.path || new URL(url).pathname
|
||||
.split('/')
|
||||
.filter((component) => !filter.has(component))
|
||||
.join('/'); // reduce to scene ID and title slug
|
||||
|
||||
const sceneId = baseRelease?.entryId || pathname.match(/\/(\d+)\//)?.[1];
|
||||
|
||||
if (mobile && /%d/.test(mobile)) {
|
||||
return util.format(mobile, sceneId);
|
||||
}
|
||||
|
||||
if (mobile && sceneId) {
|
||||
return `${mobile}${pathname}`;
|
||||
}
|
||||
|
||||
if (site.parameters?.deep) {
|
||||
return `${site.parameters.deep}${pathname}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
async function fetchScene(url, site, baseRelease, options) {
|
||||
if (site.parameters?.deep === false) {
|
||||
return baseRelease;
|
||||
}
|
||||
|
||||
const deepUrl = getDeepUrl(url, site, baseRelease);
|
||||
const mobileUrl = options.includePhotos && getDeepUrl(url, site, baseRelease, site.parameters?.mobile || site.parent?.parameters?.mobile);
|
||||
|
||||
if (deepUrl) {
|
||||
const [res, mobileRes] = await Promise.all([
|
||||
qu.get(deepUrl),
|
||||
mobileUrl && qu.get(mobileUrl, null, {
|
||||
headers: {
|
||||
// don't redirect to main site
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Mobile Safari/537.36',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (res.status === 200) {
|
||||
const mobileItem = mobileRes?.status === 200 ? mobileRes.item : null;
|
||||
const scene = await scrapeScene(res.item, url, site, baseRelease, mobileItem, options);
|
||||
|
||||
return { ...scene, deepUrl };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchActorScenes(actorName, apiUrl, siteSlug) {
|
||||
const res = await http.post(apiUrl, {
|
||||
const res = await unprint.post(apiUrl, {
|
||||
requests: [
|
||||
{
|
||||
indexName: 'all_scenes',
|
||||
@@ -864,47 +387,16 @@ async function fetchActorScenes(actorName, apiUrl, siteSlug) {
|
||||
headers: {
|
||||
Referer: `https://www.${siteSlug}.com/en/videos`,
|
||||
},
|
||||
}, {
|
||||
encodeJSON: true,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.body.results[0].hits.length > 0) {
|
||||
return res.body.results[0].hits;
|
||||
if (res.ok && res.data.results[0].hits.length > 0) {
|
||||
return res.data.results[0].hits;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function fetchProfile({ name: actorName }, context, include, altSearchUrl, getActorReleasesUrl) {
|
||||
const siteSlug = context.entity.slug || context.site?.slug || context.network?.slug;
|
||||
|
||||
const actorSlug = actorName.toLowerCase().replace(/\s+/, '+');
|
||||
const searchUrl = altSearchUrl
|
||||
? `https://www.${siteSlug}.com/en/search/${actorSlug}/1/actor`
|
||||
: `https://www.${siteSlug}.com/en/search/${siteSlug}/actor/${actorSlug}`;
|
||||
const searchRes = await http.get(searchUrl);
|
||||
|
||||
if (searchRes.status !== 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const actorUrl = scrapeActorSearch(searchRes.body.toString(), searchUrl, actorName);
|
||||
|
||||
if (actorUrl) {
|
||||
const url = `https://${siteSlug}.com${actorUrl}`;
|
||||
const actorRes = await qu.get(url);
|
||||
|
||||
if (actorRes.status !== 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return scrapeProfile(actorRes.item, url, actorName, siteSlug, getActorReleasesUrl, include.scenes, context);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function scrapeApiProfile(data, releases, siteSlug) {
|
||||
function scrapeProfile(data, releases, siteSlug) {
|
||||
const profile = {};
|
||||
|
||||
if (data.male === 1) profile.gender = 'male';
|
||||
@@ -925,7 +417,7 @@ function scrapeApiProfile(data, releases, siteSlug) {
|
||||
return profile;
|
||||
}
|
||||
|
||||
async function fetchApiProfile({ name: actorName }, context, include) {
|
||||
async function fetchProfile({ name: actorName }, context, include) {
|
||||
const siteSlug = context.entity.slug || context.site?.slug || context.network?.slug;
|
||||
const actorSlug = encodeURI(actorName);
|
||||
const referer = context.parameters.profileReferer || `${context.entity.origin}/en/search`;
|
||||
@@ -940,7 +432,7 @@ async function fetchApiProfile({ name: actorName }, context, include) {
|
||||
: []),
|
||||
]).map((site) => `"availableOnSite:${site}"`).join(',');
|
||||
|
||||
const res = await http.post(apiUrl, {
|
||||
const res = await unprint.post(apiUrl, {
|
||||
requests: [
|
||||
{
|
||||
indexName: 'all_actors',
|
||||
@@ -952,17 +444,15 @@ async function fetchApiProfile({ name: actorName }, context, include) {
|
||||
headers: {
|
||||
Referer: referer,
|
||||
},
|
||||
}, {
|
||||
encodeJSON: true,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.body.results[0].hits.length > 0) {
|
||||
const actorData = res.body.results[0].hits.find((actor) => slugify(actor.name) === slugify(actorName));
|
||||
if (res.status === 200 && res.data.results[0].hits.length > 0) {
|
||||
const actorData = res.data.results[0].hits.find((actor) => slugify(actor.name) === slugify(actorName));
|
||||
|
||||
if (actorData) {
|
||||
const actorScenes = include.releases && await fetchActorScenes(actorData.name, apiUrl, siteSlug);
|
||||
|
||||
return scrapeApiProfile(actorData, actorScenes, siteSlug);
|
||||
return scrapeProfile(actorData, actorScenes, siteSlug);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -970,31 +460,9 @@ async function fetchApiProfile({ name: actorName }, context, include) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchApiLatest: fetchLatestApi,
|
||||
fetchApiProfile,
|
||||
fetchApiUpcoming: fetchUpcomingApi,
|
||||
fetchLatest,
|
||||
fetchLatestApi,
|
||||
fetchUpcoming,
|
||||
fetchProfile,
|
||||
fetchScene,
|
||||
fetchSceneApi,
|
||||
fetchUpcoming,
|
||||
fetchUpcomingApi,
|
||||
api: {
|
||||
fetchLatest: fetchLatestApi,
|
||||
fetchUpcoming: fetchUpcomingApi,
|
||||
fetchProfile: fetchApiProfile,
|
||||
// fetchScene,
|
||||
fetchScene: fetchSceneApi,
|
||||
// scrapeMovie,
|
||||
fetchMovie: fetchMovieApi,
|
||||
},
|
||||
getPhotos,
|
||||
scrapeApiProfile,
|
||||
scrapeApiReleases,
|
||||
scrapeProfile,
|
||||
scrapeAll,
|
||||
scrapeMovie,
|
||||
scrapeScene,
|
||||
deprecated: true,
|
||||
fetchMovie,
|
||||
};
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const Promise = require('bluebird');
|
||||
|
||||
const logger = require('../logger');
|
||||
const { fetchApiLatest } = require('./gamma');
|
||||
const qu = require('../utils/qu');
|
||||
const http = require('../utils/http');
|
||||
const slugify = require('../utils/slugify');
|
||||
|
||||
async function fetchActors(entryId, channel, { token, time }) {
|
||||
const url = `${channel.url}/sapi/${token}/${time}/model.getModelContent?_method=model.getModelContent&tz=1&fields[0]=modelId.stageName&fields[1]=_last&fields[2]=modelId.upsellLink&fields[3]=modelId.upsellText&limit=25&transitParameters[contentId]=${entryId}`;
|
||||
const res = await http.get(url);
|
||||
|
||||
if (res.statusCode === 200 && res.body.status === true) {
|
||||
return Object.values(res.body.response.collection).map((actor) => Object.values(actor.modelId.collection)[0].stageName);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function fetchTrailerLocation(entryId, channel) {
|
||||
const url = `${channel.url}/api/download/${entryId}/hd1080/stream`;
|
||||
|
||||
try {
|
||||
const res = await http.get(url, {
|
||||
followRedirects: false,
|
||||
});
|
||||
|
||||
if (res.statusCode === 302) {
|
||||
return res.headers.location;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`${channel.name}: Unable to fetch trailer at '${url}': ${error.message}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function scrapeLatest(items, channel) {
|
||||
return items.map(({ query }) => {
|
||||
const release = {};
|
||||
|
||||
release.url = query.url('h5 a', null, { origin: channel.url });
|
||||
release.entryId = new URL(release.url).pathname.match(/\/(\d+)/)[1];
|
||||
|
||||
release.title = query.cnt('h5 a');
|
||||
|
||||
[release.poster, ...release.photos] = query.imgs('.screenshot').map((src) => [
|
||||
// unnecessarily large
|
||||
// src.replace(/\/\d+/, 3840),
|
||||
// src.replace(/\/\d+/, '/2000'),
|
||||
src.replace(/\/\d+/, '/1500'),
|
||||
src.replace(/\/\d+/, '/1000'),
|
||||
src,
|
||||
]);
|
||||
|
||||
return release;
|
||||
});
|
||||
}
|
||||
|
||||
function scrapeScene({ query, html }, url, channel) {
|
||||
const release = {};
|
||||
|
||||
release.entryId = new URL(url).pathname.match(/\/(\d+)/)[1];
|
||||
|
||||
release.title = query.cnt('h1.description');
|
||||
release.actors = query
|
||||
.all('.video-performer')
|
||||
.map((actorEl) => {
|
||||
const actorUrl = query.url(actorEl, 'a', 'href', { origin: channel.url });
|
||||
const entryId = new URL(url).pathname.match(/\/(\d+)/)?.[1];
|
||||
const avatar = query.img(actorEl, 'img:not([data-bgsrc*="not-available"])', 'data-bgsrc');
|
||||
|
||||
return {
|
||||
name: query.cnt(actorEl, '.video-performer-name'),
|
||||
gender: 'female',
|
||||
avatar: avatar && [
|
||||
avatar.replace(/\/actor\/(\d+)/, '/actor/500'),
|
||||
avatar,
|
||||
],
|
||||
url: actorUrl,
|
||||
entryId,
|
||||
};
|
||||
})
|
||||
.concat({ name: 'Jay Rock', gender: 'male' });
|
||||
|
||||
release.date = query.date('.release-date:first-child', 'MMM DD, YYYY', /\w+ \d{1,2}, \d{4}/);
|
||||
release.duration = query.number('.release-date:last-child') * 60;
|
||||
|
||||
release.studio = query.cnt('.studio span:nth-child(2)');
|
||||
release.director = query.text('.director');
|
||||
|
||||
release.tags = query.cnts('.tags a');
|
||||
|
||||
const poster = html.match(/url\((https.+\.jpg)\)/)?.[1];
|
||||
const photos = query.imgs('#moreScreenshots img');
|
||||
|
||||
[release.poster, ...release.photos] = [poster]
|
||||
.concat(photos)
|
||||
.filter(Boolean)
|
||||
.map((src) => [
|
||||
src.replace(/\/(\d+)\/\d+/, '/$1/1500'),
|
||||
src.replace(/\/(\d+)\/\d+/, '/$1/1000'),
|
||||
src,
|
||||
]);
|
||||
|
||||
const videoId = html.match(/item: (\d+)/)?.[1];
|
||||
|
||||
if (videoId) {
|
||||
release.trailer = { stream: `https://trailer.adultempire.com/hls/trailer/${videoId}/master.m3u8` };
|
||||
}
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
async function scrapeSceneApi(scene, channel, tokens, deep) {
|
||||
const release = {
|
||||
entryId: scene.id,
|
||||
title: scene.title,
|
||||
duration: scene.length,
|
||||
meta: {
|
||||
tokens, // attach tokens to reduce number of requests required for deep fetching
|
||||
},
|
||||
};
|
||||
|
||||
release.url = `${channel.url}/scene/${release.entryId}/${slugify(release.title, { encode: true })}`;
|
||||
release.date = new Date(scene.sites.collection[scene.id].publishDate);
|
||||
release.poster = scene._resources.primary[0].url;
|
||||
|
||||
if (scene.tags) release.tags = Object.values(scene.tags.collection).map((tag) => tag.alias);
|
||||
if (scene._resources.base) release.photos = scene._resources.base.map((resource) => resource.url);
|
||||
|
||||
if (deep) {
|
||||
// don't make external requests during update scraping, as this would happen for every scene on the page
|
||||
const [actors, trailer] = await Promise.all([
|
||||
fetchActors(release.entryId, channel, tokens),
|
||||
fetchTrailerLocation(release.entryId, channel),
|
||||
]);
|
||||
|
||||
release.actors = actors;
|
||||
|
||||
if (trailer) {
|
||||
release.trailer = { src: trailer, quality: 1080 };
|
||||
}
|
||||
}
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
function scrapeLatestApi(scenes, site, tokens) {
|
||||
return Promise.map(scenes, async (scene) => scrapeSceneApi(scene, site, tokens, false), { concurrency: 10 });
|
||||
}
|
||||
|
||||
async function fetchToken(channel) {
|
||||
const res = await http.get(channel.url);
|
||||
const html = res.body.toString();
|
||||
|
||||
const time = html.match(/"aet":\d+/)[0].split(':')[1];
|
||||
const ah = html.match(/"ah":"[\w-]+"/)[0].split(':')[1].slice(1, -1);
|
||||
const token = ah.split('').reverse().join('');
|
||||
|
||||
return { time, token };
|
||||
}
|
||||
|
||||
async function fetchLatestApi(channel, page = 1) {
|
||||
const { time, token } = await fetchToken(channel);
|
||||
|
||||
// transParameters[v1] includes _resources, [v2] includes photos, [preset] is mandatory
|
||||
const url = `${channel.url}/sapi/${token}/${time}/content.load?limit=50&offset=${(page - 1) * 50}&transitParameters[v1]=OhUOlmasXD&transitParameters[v2]=OhUOlmasXD&transitParameters[preset]=videos`;
|
||||
const res = await http.get(url);
|
||||
|
||||
if (res.ok && res.body.status) {
|
||||
return scrapeLatestApi(res.body.response.collection, channel, { time, token });
|
||||
}
|
||||
|
||||
return res.ok ? res.body.status : res.status;
|
||||
}
|
||||
|
||||
async function fetchLatest(channel, page = 1, options, preData) {
|
||||
if (channel.parameters?.useGamma) {
|
||||
return fetchApiLatest(channel, page, preData, options, false);
|
||||
}
|
||||
|
||||
const res = await qu.getAll(`https://jayspov.net/jays-pov-updates.html?view=list&page=${page}`, '.item-grid-list-view > .grid-item');
|
||||
|
||||
if (res.ok) {
|
||||
return scrapeLatest(res.items, channel);
|
||||
}
|
||||
|
||||
return res.status;
|
||||
}
|
||||
|
||||
async function fetchSceneApi(url, channel, baseRelease) {
|
||||
const { time, token } = baseRelease?.meta.tokens || await fetchToken(channel); // use attached tokens when deep fetching
|
||||
const { pathname } = new URL(url);
|
||||
const entryId = pathname.split('/')[2];
|
||||
|
||||
const apiUrl = `${channel.url}/sapi/${token}/${time}/content.load?filter[id][fields][0]=id&filter[id][values][0]=${entryId}&transitParameters[v1]=ykYa8ALmUD&transitParameters[preset]=scene`;
|
||||
const res = await http.get(apiUrl);
|
||||
|
||||
if (res.ok && res.body.status) {
|
||||
return scrapeSceneApi(res.body.response.collection[0], channel, { time, token }, true);
|
||||
}
|
||||
|
||||
return res.ok ? res.body.status : res.status;
|
||||
}
|
||||
|
||||
async function fetchScene(url, channel) {
|
||||
const res = await qu.get(url);
|
||||
|
||||
if (res.ok) {
|
||||
return scrapeScene(res.item, url, channel);
|
||||
}
|
||||
|
||||
return res.status;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchLatest,
|
||||
fetchScene,
|
||||
api: {
|
||||
fetchLatest: fetchLatestApi,
|
||||
fetchScene: fetchSceneApi,
|
||||
},
|
||||
};
|
||||
@@ -36,7 +36,7 @@ function scrapeAll(scenes) {
|
||||
}
|
||||
|
||||
async function fetchLatest(channel, page) {
|
||||
const res = await unprint.get(new URL(`./videos/page${page}.html`, channel.url).href, { // some sites require a trailing slash, join paths properly
|
||||
const res = await unprint.get(new URL(`./videos/page${page}.html`, channel.url).href, { // some sites require a trailing slash, join paths properly; don't use origin in case channel path is used
|
||||
selectAll: '.listing-videos .item',
|
||||
cookies: {
|
||||
warningHidden: 'hide',
|
||||
|
||||
@@ -70,8 +70,7 @@ function scrapeAll(scenes, entity) {
|
||||
async function fetchLatest(site, page = 1) {
|
||||
const url = `${site.url}/video/gallery/${(page - 1) * 12}`; // /0 redirects back to /
|
||||
|
||||
const res = await unprint.get(url, {
|
||||
interface: 'request',
|
||||
const res = await unprint.browser(url, {
|
||||
selectAll: '.content-grid-item',
|
||||
});
|
||||
|
||||
@@ -86,9 +85,8 @@ async function fetchUpcoming(site) {
|
||||
if (site.parameters?.upcoming) {
|
||||
const url = `${site.url}/video/upcoming`;
|
||||
|
||||
const res = await unprint.get(url, {
|
||||
const res = await unprint.browser(url, {
|
||||
selectAll: '.content-grid-item',
|
||||
interface: 'request',
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
@@ -139,9 +137,7 @@ async function scrapeScene({ query }, { url, entity, include }) {
|
||||
}
|
||||
|
||||
async function fetchScene(url, entity, _baseRelease, include) {
|
||||
const res = await unprint.get(url, {
|
||||
interface: 'request',
|
||||
});
|
||||
const res = await unprint.browser(url);
|
||||
|
||||
if (res.ok) {
|
||||
return scrapeScene(res.context, { url, entity, include });
|
||||
@@ -185,9 +181,7 @@ async function findModel(actor, entity) {
|
||||
|
||||
const url = `${origin}/model/alpha/${firstLetter}`;
|
||||
|
||||
const resModels = await unprint.get(url, {
|
||||
interface: 'request',
|
||||
});
|
||||
const resModels = await unprint.browser(url);
|
||||
|
||||
if (!resModels.ok) {
|
||||
return resModels.status;
|
||||
@@ -217,9 +211,7 @@ async function fetchProfile(actor, { entity }) {
|
||||
const model = await findModel(actor, entity);
|
||||
|
||||
if (model) {
|
||||
const resModel = await unprint.get(model.url, {
|
||||
interface: 'request',
|
||||
});
|
||||
const resModel = await unprint.browser(model.url);
|
||||
|
||||
if (resModel.ok) {
|
||||
return scrapeProfile(resModel.context, model.avatar);
|
||||
|
||||
@@ -215,7 +215,7 @@ function scrapeProfile(data, channel, scenes, parameters) {
|
||||
|
||||
async function fetchProfile(actor, { channel, parameters }) {
|
||||
const endpoint = await fetchEndpoint(channel);
|
||||
const res = await http.get(`${channel.url}/_next/data/${endpoint}/models/${actor.slug}.json?slug=${actor.slug}`);
|
||||
const res = await http.get(`${channel.url}/_next/data/${endpoint}/${parameters.actors || 'models'}/${actor.slug}.json?slug=${actor.slug}`);
|
||||
|
||||
if (res.ok && res.body.pageProps?.model) {
|
||||
return scrapeProfile(res.body.pageProps.model, channel, res.body.pageProps.model_contents, parameters);
|
||||
|
||||
@@ -19,7 +19,6 @@ const czechav = require('./czechav');
|
||||
const modelmedia = require('./modelmedia');
|
||||
const dorcel = require('./dorcel');
|
||||
const fabulouscash = require('./fabulouscash');
|
||||
// const famedigital = require('./famedigital');
|
||||
const firstanalquest = require('./firstanalquest');
|
||||
const elevatedx = require('./elevatedx');
|
||||
const exploitedx = require('./exploitedx');
|
||||
@@ -31,7 +30,6 @@ const hush = require('./hush');
|
||||
const innofsin = require('./innofsin');
|
||||
const insex = require('./insex');
|
||||
const inthecrack = require('./inthecrack');
|
||||
const jayrock = require('./jayrock');
|
||||
const jesseloadsmonsterfacials = require('./jesseloadsmonsterfacials');
|
||||
const julesjordan = require('./julesjordan');
|
||||
const karups = require('./karups');
|
||||
@@ -74,7 +72,6 @@ const tokyohot = require('./tokyohot');
|
||||
// const topwebmodels = require('./topwebmodels');
|
||||
const traxxx = require('./traxxx');
|
||||
const virtualtaboo = require('./virtualtaboo');
|
||||
const vivid = require('./vivid');
|
||||
const vixen = require('./vixen');
|
||||
const wankzvr = require('./wankzvr');
|
||||
const whalemember = require('./whalemember');
|
||||
@@ -138,7 +135,6 @@ module.exports = {
|
||||
insex,
|
||||
interracialpass: hush,
|
||||
inthecrack,
|
||||
jayrock,
|
||||
jerkaoke: modelmedia,
|
||||
jesseloadsmonsterfacials,
|
||||
julesjordan,
|
||||
@@ -180,7 +176,6 @@ module.exports = {
|
||||
traxxx,
|
||||
vipsexvault: porndoe,
|
||||
virtualtaboo,
|
||||
vivid,
|
||||
vixen,
|
||||
wankzvr,
|
||||
westcoastproductions: adultempire,
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
const { get, ed } = require('../utils/q');
|
||||
const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma');
|
||||
const http = require('../utils/http');
|
||||
const slugify = require('../utils/slugify');
|
||||
|
||||
function scrapeLatestNative(scenes, site) {
|
||||
return scenes.map((scene) => {
|
||||
const release = {};
|
||||
|
||||
release.entryId = scene.id;
|
||||
release.url = `${site.url}${scene.url}`;
|
||||
|
||||
release.title = scene.name;
|
||||
release.date = ed(scene.release_date, 'YYYY-MM-DD');
|
||||
release.duration = parseInt(scene.runtime, 10) * 60;
|
||||
|
||||
release.actors = scene.cast?.map((actor) => ({
|
||||
name: actor.stagename,
|
||||
gender: actor.gender.toLowerCase(),
|
||||
avatar: actor.placard,
|
||||
})) || [];
|
||||
|
||||
release.stars = Number(scene.rating);
|
||||
release.poster = scene.placard_800 || scene.placard;
|
||||
|
||||
return release;
|
||||
});
|
||||
}
|
||||
|
||||
function scrapeSceneNative({ html, q, qa }, url, _site) {
|
||||
const release = { url };
|
||||
|
||||
release.entryId = new URL(url).pathname.split('/')[2]; // eslint-disable-line prefer-destructuring
|
||||
|
||||
release.title = q('.scene-h2-heading', true);
|
||||
release.description = q('.indie-model-p', true);
|
||||
|
||||
const dateString = qa('h5').find((el) => /Released/.test(el.textContent)).textContent;
|
||||
release.date = ed(dateString, 'MMM DD, YYYY', /\w+ \d{1,2}, \d{4}/);
|
||||
|
||||
const duration = qa('h5').find((el) => /Runtime/.test(el.textContent)).textContent;
|
||||
const [hours, minutes] = duration.match(/\d+/g);
|
||||
|
||||
if (minutes) release.duration = (hours * 3600) + (minutes * 60);
|
||||
else release.duration = hours * 60; // scene shorter that 1hr, hour match are minutes
|
||||
|
||||
release.actors = qa('h4 a[href*="/stars"], h4 a[href*="/celebs"]', true);
|
||||
release.tags = qa('h5 a[href*="/categories"]', true);
|
||||
|
||||
const [poster, trailer] = html.match(/https:\/\/content.vivid.com(.*)(.jpg|.mp4)/g);
|
||||
release.poster = poster;
|
||||
|
||||
if (trailer) {
|
||||
release.trailer = {
|
||||
src: trailer,
|
||||
};
|
||||
}
|
||||
|
||||
const channel = q('h5 a[href*="/sites"]', true);
|
||||
if (channel) release.channel = channel.replace(/\.\w+/, '');
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
async function fetchLatestNative(site, page = 1) {
|
||||
if (site.parameters?.useGamma) {
|
||||
return fetchApiLatest(site, page);
|
||||
}
|
||||
|
||||
const apiUrl = `${site.url}/videos/api/?limit=50&offset=${(page - 1) * 50}&sort=datedesc`;
|
||||
const res = await http.get(apiUrl, {
|
||||
decodeJSON: true,
|
||||
});
|
||||
|
||||
if (res.statusCode === 200 && res.body.code === 200) {
|
||||
return scrapeLatestNative(res.body.responseData, site);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchUpcomingNative(site) {
|
||||
if (site.parameters?.useGamma) {
|
||||
return fetchApiUpcoming(site);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchSceneNative(url, site, release) {
|
||||
if (site.parameters?.useGamma) {
|
||||
return fetchScene(url, site, release);
|
||||
}
|
||||
|
||||
const res = await get(url);
|
||||
|
||||
return res.ok ? scrapeSceneNative(res.item, url, site) : res.status;
|
||||
}
|
||||
|
||||
async function fetchSceneWrapper(url, site, release) {
|
||||
const scene = await fetchScene(url, site, release);
|
||||
|
||||
if (scene.date - new Date(site.parameters?.lastNative) <= 0) {
|
||||
// scene is probably still available on Vivid site, use search API to get URL and original date
|
||||
const searchUrl = `${site.url}/videos/api/?limit=10&sort=datedesc&search=${encodeURI(scene.title)}`;
|
||||
const searchRes = await http.get(searchUrl, {
|
||||
decodeJSON: true,
|
||||
});
|
||||
|
||||
if (searchRes.statusCode === 200 && searchRes.body.code === 200) {
|
||||
const sceneMatch = searchRes.body.responseData.find((item) => slugify(item.name) === slugify(scene.title));
|
||||
|
||||
if (sceneMatch) {
|
||||
return {
|
||||
...scene,
|
||||
url: `${site.url}${sceneMatch.url}`,
|
||||
date: ed(sceneMatch.release_date, 'YYYY-MM-DD'),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scene;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchLatest: fetchApiLatest,
|
||||
fetchProfile: fetchApiProfile,
|
||||
fetchUpcoming: fetchApiUpcoming,
|
||||
fetchScene: fetchSceneWrapper,
|
||||
};
|
||||
@@ -502,6 +502,7 @@ async function fetchScene(url, channel, baseRelease, options) {
|
||||
}
|
||||
|
||||
const res = await unprint.get(url, {
|
||||
interface: 'request',
|
||||
useBrowser: !!options.parameters?.useBrowser,
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ const logger = require('./logger')(__filename);
|
||||
const knex = require('./knex');
|
||||
|
||||
const { fetchEntityReleaseIds } = require('./entities');
|
||||
const { updateSceneSearch } = require('./update-search');
|
||||
|
||||
const slugify = require('./utils/slugify');
|
||||
const batchInsert = require('./utils/batch-insert');
|
||||
@@ -199,6 +200,8 @@ async function reassociateTagEntries(tagEntries, rematch) {
|
||||
tag_id: matchedTags[slugify(tagEntry.original_tag)],
|
||||
})).filter((tagEntry) => tagEntry.tag_id);
|
||||
|
||||
const sceneIds = Array.from(new Set(updatedTagEntries.map((tagEntry) => tagEntry.release_id))).filter(Boolean);
|
||||
|
||||
if (updatedTagEntries.length > 0) {
|
||||
const trx = await knex.transaction();
|
||||
|
||||
@@ -212,10 +215,13 @@ async function reassociateTagEntries(tagEntries, rematch) {
|
||||
})), {
|
||||
conflict: false,
|
||||
transaction: trx,
|
||||
commit: true,
|
||||
});
|
||||
|
||||
await updateSceneSearch(sceneIds);
|
||||
}
|
||||
|
||||
logger.info(`Updated ${updatedTagEntries.length} tags in ${new Set(updatedTagEntries.map((tagEntry) => tagEntry.release_id)).size} scenes`);
|
||||
logger.info(`Updated ${updatedTagEntries.length} tags in ${sceneIds.length} scenes`);
|
||||
}
|
||||
|
||||
async function reassociateReleaseTags(rawSceneIds, rematch) {
|
||||
|
||||
136
src/tools/gamma_banners.js
Normal file
136
src/tools/gamma_banners.js
Normal file
@@ -0,0 +1,136 @@
|
||||
'use strict';
|
||||
|
||||
const unprint = require('unprint');
|
||||
const fs = require('fs');
|
||||
const { Readable } = require('stream');
|
||||
const { pipeline } = require('stream/promises');
|
||||
|
||||
const knex = require('../knex');
|
||||
const argv = require('../argv');
|
||||
const slugify = require('../utils/slugify');
|
||||
|
||||
const apiUrl = 'https://vjoc5ygk89-dsn.algolia.net/1/indexes/*/queries?x-algolia-agent=Algolia%20for%20JavaScript%20(3.33.0)%3B%20Browser%20(lite)%3B%20react%20(16.8.6)%3B%20react-instantsearch%20(5.7.0)%3B%20JS%20Helper%20(2.28.1)&x-algolia-application-id=VJOC5YGK89&x-algolia-api-key=c5546bdfb4d3f31daf49ed3bb1463561';
|
||||
|
||||
async function fetchBanners() {
|
||||
const res = await unprint.post(
|
||||
apiUrl,
|
||||
{
|
||||
requests: [
|
||||
{
|
||||
indexName: 'creatives',
|
||||
params: new URLSearchParams({
|
||||
hitsPerPage: 1000,
|
||||
maxValuesPerFacet: 100,
|
||||
page: 0,
|
||||
filters: '(ProgramType:Legacy OR ProgramType:Internal) AND NOT OverlayActive:false',
|
||||
facets: '["SceneActors","SceneCategories","ProgramName","Size","Niche","MediaExt","SiteTag","OverlayName"]',
|
||||
facetFilters: `[["SiteTag:${argv.site}"],["MediaExt:jpg", "MediaExt:png", "MediaExt:gif"]]`,
|
||||
}).toString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'content-type': 'application/x-www-form-urlencoded',
|
||||
referer: 'https://creatives.gammae.com/',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.ok && res.data.results[0]) {
|
||||
return res.data.results[0].hits;
|
||||
}
|
||||
|
||||
console.error(`Failed API request (${res.status}): ${res.body}`);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function matchTags(rawTags) {
|
||||
if (!rawTags) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const tags = rawTags
|
||||
.map((tag) => tag?.trim().match(/[a-z0-9()]+/ig)?.join(' ').toLowerCase())
|
||||
.filter(Boolean);
|
||||
|
||||
const tagEntries = await knex('tags')
|
||||
.select('tags.slug', 'aliases.slug as alias_slug')
|
||||
.whereIn(knex.raw('lower(tags.name)'), tags)
|
||||
.leftJoin('tags as aliases', 'aliases.id', 'tags.alias_for')
|
||||
.orderByRaw('CASE WHEN tags.alias_for IS NOT NULL THEN aliases.priority ELSE tags.priority END DESC');
|
||||
|
||||
return tagEntries.map((tagEntry) => tagEntry.alias_slug || tagEntry.slug);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
const banners = await fetchBanners();
|
||||
|
||||
if (!banners) {
|
||||
return;
|
||||
}
|
||||
|
||||
await banners.reduce(async (chain, banner) => {
|
||||
await chain;
|
||||
|
||||
const channel = slugify(banner.SiteTag, '');
|
||||
const url = unprint.prefixUrl(banner.MediaLocation || banner.CreativeURL, 'https://cdn.banhq.com');
|
||||
|
||||
if (!url) {
|
||||
console.log('No URL found');
|
||||
console.log(banner);
|
||||
return;
|
||||
}
|
||||
|
||||
const tags = await matchTags([
|
||||
...banner.Tags?.map((tag) => tag.Value) || [],
|
||||
...banner.SceneCategories || [],
|
||||
banner.Niche,
|
||||
].filter(Boolean));
|
||||
|
||||
const fileTags = tags.slice(0, 4).join('_');
|
||||
const fileActors = banner.SceneActors?.slice(0, 2).map((actor) => slugify(actor, '_')).join('_');
|
||||
|
||||
// tags are unreliable and describe entire scene, not banner, don't include by default
|
||||
const segments = [channel, banner.Width, banner.Height, banner.MediaID, argv.actors?.[0] !== false && fileActors].filter(Boolean);
|
||||
const filename = `${segments.join('_')}${argv.tags && argv.tags ? `-${fileTags}` : ''}.${banner.MediaExt || 'jpg'}`;
|
||||
|
||||
const filepath = `/tmp/gamma/${channel}/${filename}`;
|
||||
|
||||
if (argv.inspect) {
|
||||
console.log(banner);
|
||||
}
|
||||
|
||||
if (argv.preview) {
|
||||
console.log(`Preview ${url}: ${filepath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(`/tmp/gamma/${channel}`, { recursive: true });
|
||||
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
|
||||
if (res.ok && res.body) {
|
||||
const writer = fs.createWriteStream(filepath);
|
||||
|
||||
await pipeline(Readable.fromWeb(res.body), writer);
|
||||
|
||||
if (argv.actors) {
|
||||
console.log(`Saved ${url} to ${filepath}`);
|
||||
} else {
|
||||
console.log(`Saved ${url} to ${filepath}, actors ${banner.SceneActors?.join(', ') || ''}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`Failed to fetch ${url} (${res.status})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Failed to fetch ${url}: ${error.message}`);
|
||||
}
|
||||
}, Promise.resolve());
|
||||
|
||||
await knex.destroy();
|
||||
}
|
||||
|
||||
init();
|
||||
15
src/tools/huge-query.js
Normal file
15
src/tools/huge-query.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
const knex = require('../knex');
|
||||
|
||||
async function init() {
|
||||
const data = Array.from({ length: 100_000 }, (value, index) => ({
|
||||
id: `test_affiliate_${index}`,
|
||||
}));
|
||||
|
||||
await knex('affiliates').insert(data);
|
||||
|
||||
console.log('Done!');
|
||||
}
|
||||
|
||||
init();
|
||||
@@ -41,7 +41,7 @@ async function fetchScenes() {
|
||||
studios.name as studio_name,
|
||||
grandparents.id as parent_network_id,
|
||||
COALESCE(JSON_AGG(DISTINCT (actors.id, actors.name)) FILTER (WHERE actors.id IS NOT NULL), '[]') as actors,
|
||||
COALESCE(JSON_AGG(DISTINCT (tags.id, tags.name, tags.priority, tags_aliases.name)) FILTER (WHERE tags.id IS NOT NULL), '[]') as tags,
|
||||
COALESCE(JSON_AGG(DISTINCT (tags.id, tags.name, tags.priority, tags_aliases.name, local_tags.actor_id)) FILTER (WHERE tags.id IS NOT NULL), '[]') as tags,
|
||||
COALESCE(JSON_AGG(DISTINCT (movies.id, movies.title)) FILTER (WHERE movies.id IS NOT NULL), '[]') as movies,
|
||||
COALESCE(JSON_AGG(DISTINCT (series.id, series.title)) FILTER (WHERE series.id IS NOT NULL), '[]') as series,
|
||||
COALESCE(JSON_AGG(DISTINCT (releases_fingerprints.hash)) FILTER (WHERE releases_fingerprints.hash IS NOT NULL), '[]') as fingerprints,
|
||||
@@ -136,6 +136,14 @@ async function init() {
|
||||
dupe_index int
|
||||
)`);
|
||||
|
||||
await utilsApi.sql('drop table if exists scenes_tags');
|
||||
await utilsApi.sql(`create table scenes_tags (
|
||||
id int,
|
||||
scene_id int,
|
||||
tag_id int,
|
||||
actor_id int
|
||||
)`);
|
||||
|
||||
console.log('Recreated scenes table');
|
||||
console.log('Fetching scenes from primary database');
|
||||
|
||||
@@ -143,49 +151,62 @@ async function init() {
|
||||
|
||||
console.log('Fetched scenes from primary database');
|
||||
|
||||
const docs = scenes.map((scene) => {
|
||||
const docs = scenes.flatMap((scene) => {
|
||||
const flatActors = scene.actors.flatMap((actor) => actor.f2.match(/[\w']+/g)); // match word characters to filter out brackets etc.
|
||||
const flatTags = scene.tags.filter((tag) => tag.f3 > 6).flatMap((tag) => (tag.f4 ? `${tag.f2} ${tag.f4}` : tag.f2).match(/[\w']+/g)); // only make top tags searchable to minimize cluttered results
|
||||
const filteredTitle = filterTitle(scene.title, [...flatActors, ...flatTags]);
|
||||
|
||||
return {
|
||||
replace: {
|
||||
index: 'scenes',
|
||||
id: scene.id,
|
||||
doc: {
|
||||
title: scene.title || undefined,
|
||||
title_filtered: filteredTitle || undefined,
|
||||
date: scene.date ? Math.round(scene.date.getTime() / 1000) : undefined,
|
||||
created_at: Math.round(scene.created_at.getTime() / 1000),
|
||||
effective_date: Math.round((scene.date || scene.created_at).getTime() / 1000),
|
||||
is_showcased: scene.showcased,
|
||||
entry_id: scene.entry_id || undefined,
|
||||
shoot_id: scene.shoot_id || undefined,
|
||||
channel_id: scene.channel_id,
|
||||
channel_slug: scene.channel_slug,
|
||||
channel_name: [].concat(scene.channel_name, scene.channel_aliases).join(' '),
|
||||
network_id: scene.network_id || undefined,
|
||||
network_slug: scene.network_slug || undefined,
|
||||
network_name: [].concat(scene.network_name, scene.network_aliases).join(' ') || undefined,
|
||||
studio_id: scene.studio_id || undefined,
|
||||
studio_slug: scene.studio_slug || undefined,
|
||||
studio_name: scene.studio_name || undefined,
|
||||
entity_ids: [scene.channel_id, scene.network_id, scene.parent_network_id, scene.studio_id].filter(Boolean), // manticore does not support OR, this allows IN
|
||||
actor_ids: scene.actors.map((actor) => actor.f1),
|
||||
actors: scene.actors.map((actor) => actor.f2).join(),
|
||||
tag_ids: scene.tags.map((tag) => tag.f1),
|
||||
tags: flatTags.join(' '),
|
||||
movie_ids: scene.movies.map((movie) => movie.f1),
|
||||
movies: scene.movies.map((movie) => movie.f2).join(' '),
|
||||
serie_ids: scene.series.map((serie) => serie.f1),
|
||||
series: scene.series.map((serie) => serie.f2).join(' '),
|
||||
fingerprints: scene.fingerprints.join(' '),
|
||||
meta: scene.date ? format(scene.date, 'y yy M MM MMM MMMM d dd') : undefined,
|
||||
stashed: scene.stashed || 0,
|
||||
dupe_index: scene.dupe_index || 0,
|
||||
return [
|
||||
{
|
||||
replace: {
|
||||
index: 'scenes',
|
||||
id: scene.id,
|
||||
doc: {
|
||||
title: scene.title || undefined,
|
||||
title_filtered: filteredTitle || undefined,
|
||||
date: scene.date ? Math.round(scene.date.getTime() / 1000) : undefined,
|
||||
created_at: Math.round(scene.created_at.getTime() / 1000),
|
||||
effective_date: Math.round((scene.date || scene.created_at).getTime() / 1000),
|
||||
is_showcased: scene.showcased,
|
||||
entry_id: scene.entry_id || undefined,
|
||||
shoot_id: scene.shoot_id || undefined,
|
||||
channel_id: scene.channel_id,
|
||||
channel_slug: scene.channel_slug,
|
||||
channel_name: [].concat(scene.channel_name, scene.channel_aliases).join(' '),
|
||||
network_id: scene.network_id || undefined,
|
||||
network_slug: scene.network_slug || undefined,
|
||||
network_name: [].concat(scene.network_name, scene.network_aliases).join(' ') || undefined,
|
||||
studio_id: scene.studio_id || undefined,
|
||||
studio_slug: scene.studio_slug || undefined,
|
||||
studio_name: scene.studio_name || undefined,
|
||||
entity_ids: [scene.channel_id, scene.network_id, scene.parent_network_id, scene.studio_id].filter(Boolean), // manticore does not support OR, this allows IN
|
||||
actor_ids: scene.actors.map((actor) => actor.f1),
|
||||
actors: scene.actors.map((actor) => actor.f2).join(),
|
||||
tag_ids: scene.tags.map((tag) => tag.f1),
|
||||
tags: flatTags.join(' '),
|
||||
movie_ids: scene.movies.map((movie) => movie.f1),
|
||||
movies: scene.movies.map((movie) => movie.f2).join(' '),
|
||||
serie_ids: scene.series.map((serie) => serie.f1),
|
||||
series: scene.series.map((serie) => serie.f2).join(' '),
|
||||
fingerprints: scene.fingerprints.join(' '),
|
||||
meta: scene.date ? format(scene.date, 'y yy M MM MMM MMMM d dd') : undefined,
|
||||
stashed: scene.stashed || 0,
|
||||
dupe_index: scene.dupe_index || 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
...scene.tags.map((tag) => ({
|
||||
replace: {
|
||||
index: 'scenes_tags',
|
||||
// id: scene.id,
|
||||
doc: {
|
||||
scene_id: scene.id,
|
||||
tag_id: tag.f1,
|
||||
actor_id: tag.f5,
|
||||
},
|
||||
},
|
||||
})),
|
||||
];
|
||||
});
|
||||
|
||||
// const accData = chunk(docs, 10000).reduce(async (chain, docsChunk, index, array) => {
|
||||
|
||||
88
src/tools/manticore-stashes.js
Normal file
88
src/tools/manticore-stashes.js
Normal file
@@ -0,0 +1,88 @@
|
||||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const manticore = require('manticoresearch');
|
||||
|
||||
const knex = require('../knex');
|
||||
const chunk = require('../utils/chunk');
|
||||
|
||||
const mantiClient = new manticore.ApiClient();
|
||||
|
||||
mantiClient.basePath = `http://${config.database.manticore.host}:${config.database.manticore.httpPort}`;
|
||||
|
||||
const utilsApi = new manticore.UtilsApi(mantiClient);
|
||||
const indexApi = new manticore.IndexApi(mantiClient);
|
||||
|
||||
async function syncStashes(domain = 'scene') {
|
||||
await utilsApi.sql(`truncate table ${domain}s_stashed`);
|
||||
|
||||
const stashes = await knex(`stashes_${domain}s`)
|
||||
.select(
|
||||
`stashes_${domain}s.id as stashed_id`,
|
||||
`stashes_${domain}s.${domain}_id`,
|
||||
'stashes.id as stash_id',
|
||||
'stashes.user_id as user_id',
|
||||
`stashes_${domain}s.created_at as created_at`,
|
||||
)
|
||||
.leftJoin('stashes', 'stashes.id', `stashes_${domain}s.stash_id`);
|
||||
|
||||
await chunk(stashes, 1000).reduce(async (chain, stashChunk, index) => {
|
||||
await chain;
|
||||
|
||||
const stashDocs = stashChunk.map((stash) => ({
|
||||
replace: {
|
||||
index: `${domain}s_stashed`,
|
||||
id: stash.stashed_id,
|
||||
doc: {
|
||||
[`${domain}_id`]: stash[`${domain}_id`],
|
||||
stash_id: stash.stash_id,
|
||||
user_id: stash.user_id,
|
||||
created_at: Math.round(stash.created_at.getTime() / 1000),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
await indexApi.bulk(stashDocs.map((doc) => JSON.stringify(doc)).join('\n'));
|
||||
|
||||
console.log(`Synced ${index * 1000 + stashChunk.length}/${stashes.length} ${domain} stashes`);
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await utilsApi.sql('drop table if exists scenes_stashed');
|
||||
|
||||
await utilsApi.sql(`create table if not exists scenes_stashed (
|
||||
scene_id int,
|
||||
stash_id int,
|
||||
user_id int,
|
||||
created_at timestamp
|
||||
)`);
|
||||
|
||||
await utilsApi.sql('drop table if exists movies_stashed');
|
||||
|
||||
await utilsApi.sql(`create table if not exists movies_stashed (
|
||||
movie_id int,
|
||||
stash_id int,
|
||||
user_id int,
|
||||
created_at timestamp
|
||||
)`);
|
||||
|
||||
await utilsApi.sql('drop table if exists actors_stashed');
|
||||
|
||||
await utilsApi.sql(`create table if not exists actors_stashed (
|
||||
actor_id int,
|
||||
stash_id int,
|
||||
user_id int,
|
||||
created_at timestamp
|
||||
)`);
|
||||
|
||||
await syncStashes('scene');
|
||||
await syncStashes('actor');
|
||||
await syncStashes('movie');
|
||||
|
||||
console.log('Done!');
|
||||
|
||||
knex.destroy();
|
||||
}
|
||||
|
||||
init();
|
||||
@@ -16,14 +16,14 @@ async function updateManticoreStashedScenes(docs) {
|
||||
await chunk(docs, 1000).reduce(async (chain, docsChunk) => {
|
||||
await chain;
|
||||
|
||||
const sceneIds = docsChunk.map((doc) => doc.replace.id);
|
||||
const sceneIds = docsChunk.filter((doc) => !!doc.replace).map((doc) => doc.replace.id);
|
||||
|
||||
const stashes = await knex('stashes_scenes')
|
||||
.select('stashes_scenes.id as stashed_id', 'stashes_scenes.scene_id', 'stashes_scenes.created_at', 'stashes.id as stash_id', 'stashes.user_id as user_id')
|
||||
.leftJoin('stashes', 'stashes.id', 'stashes_scenes.stash_id')
|
||||
.whereIn('scene_id', sceneIds);
|
||||
|
||||
const stashDocs = docsChunk.flatMap((doc) => {
|
||||
const stashDocs = docsChunk.filter((doc) => doc.replace).flatMap((doc) => {
|
||||
const sceneStashes = stashes.filter((stash) => stash.scene_id === doc.replace.id);
|
||||
|
||||
if (sceneStashes.length === 0) {
|
||||
@@ -50,6 +50,25 @@ async function updateManticoreStashedScenes(docs) {
|
||||
if (stashDocs.length > 0) {
|
||||
await indexApi.bulk(stashDocs.map((doc) => JSON.stringify(doc)).join('\n'));
|
||||
}
|
||||
|
||||
const deleteSceneIds = docs.filter((doc) => doc.delete).map((doc) => doc.delete.id);
|
||||
|
||||
if (deleteSceneIds.length > 0) {
|
||||
await indexApi.callDelete({
|
||||
index: 'scenes_stashed',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
in: {
|
||||
scene_id: deleteSceneIds,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
@@ -128,9 +147,20 @@ async function updateManticoreSceneSearch(releaseIds) {
|
||||
studios.showcased
|
||||
`, releaseIds && [releaseIds]);
|
||||
|
||||
// console.log(scenes.rows);
|
||||
const scenesById = Object.fromEntries(scenes.rows.map((scene) => [scene.id, scene]));
|
||||
|
||||
const docs = releaseIds.map((sceneId) => {
|
||||
const scene = scenesById[sceneId];
|
||||
|
||||
if (!scene) {
|
||||
return {
|
||||
delete: {
|
||||
index: 'scenes',
|
||||
id: sceneId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const docs = scenes.rows.map((scene) => {
|
||||
const flatActors = scene.actors.flatMap((actor) => actor.f2.split(' '));
|
||||
const flatTags = scene.tags.filter((tag) => tag.f3 > 6).flatMap((tag) => [tag.f2].concat(tag.f4)).filter(Boolean); // only make top tags searchable to minimize cluttered results
|
||||
const filteredTitle = filterTitle(scene.title, [...flatActors, ...flatTags]);
|
||||
@@ -291,7 +321,20 @@ async function updateManticoreMovieSearch(movieIds) {
|
||||
movies_covers.*
|
||||
`, movieIds && [movieIds]);
|
||||
|
||||
const docs = movies.rows.map((movie) => {
|
||||
const moviesById = Object.fromEntries(movies.rows.map((movie) => [movie.id, movie]));
|
||||
|
||||
const docs = movieIds.map((movieId) => {
|
||||
const movie = moviesById[movieId];
|
||||
|
||||
if (!movie) {
|
||||
return {
|
||||
delete: {
|
||||
index: 'movies',
|
||||
id: movieId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const combinedTags = Object.values(Object.fromEntries(movie.tags.concat(movie.movie_tags).map((tag) => [tag.f1, {
|
||||
id: tag.f1,
|
||||
name: tag.f2,
|
||||
|
||||
@@ -314,8 +314,6 @@ async function scrapeNetworkParallel(networkEntity) {
|
||||
async function fetchUpdates() {
|
||||
const includedNetworks = await fetchIncludedEntities();
|
||||
|
||||
// console.log(includedNetworks[0]);
|
||||
|
||||
const scrapedNetworks = await Promise.map(
|
||||
includedNetworks,
|
||||
async (networkEntity) => (networkEntity.parameters?.sequential
|
||||
|
||||
@@ -4,11 +4,12 @@ const knex = require('../knex');
|
||||
const chunk = require('./chunk');
|
||||
const logger = require('../logger')(__filename);
|
||||
|
||||
const chunkTarget = 50_000; // PostgreSQL allows 65,535 binding parameters, allow for a bit of margin
|
||||
|
||||
// improved version of bulkInsert
|
||||
async function batchInsert(table, items, {
|
||||
conflict = true,
|
||||
update = false,
|
||||
chunkSize = 1000,
|
||||
concurrent = false,
|
||||
transaction,
|
||||
commit = false,
|
||||
@@ -17,6 +18,10 @@ async function batchInsert(table, items, {
|
||||
throw new Error('No table specified for batch insert');
|
||||
}
|
||||
|
||||
if (conflict && update) {
|
||||
throw new Error('Batch insert conflict must specify columns, or update must be disabled');
|
||||
}
|
||||
|
||||
if (!Array.isArray(items)) {
|
||||
throw new Error('Batch insert items are not an array');
|
||||
}
|
||||
@@ -25,8 +30,20 @@ async function batchInsert(table, items, {
|
||||
return [];
|
||||
}
|
||||
|
||||
const chunks = chunk(items, chunkSize);
|
||||
// PostgreSQL's bindings limit applies to individual values, so item size needs to be taken into account
|
||||
const itemSize = items.reduce((acc, item) => Math.max(acc, Object.keys(item).length), 0);
|
||||
|
||||
if (itemSize === 0) {
|
||||
throw new Error('Batch insert items are empty');
|
||||
}
|
||||
|
||||
const chunks = chunk(items, Math.floor(chunkTarget / itemSize));
|
||||
const conflicts = [].concat(conflict).filter((column) => typeof column === 'string'); // conflict might be 'true'
|
||||
|
||||
if (conflicts.length > 0 && !update) {
|
||||
throw new Error('Batch insert conflict columns must be specified together with update');
|
||||
}
|
||||
|
||||
const trx = transaction || await knex.transaction();
|
||||
|
||||
try {
|
||||
@@ -49,12 +66,6 @@ async function batchInsert(table, items, {
|
||||
.onConflict(conflicts)
|
||||
.merge();
|
||||
}
|
||||
|
||||
throw new Error('Batch insert conflict columns must be specified together with update');
|
||||
}
|
||||
|
||||
if (conflict && update) {
|
||||
throw new Error('Batch insert conflict must specify columns, or update must be disabled');
|
||||
}
|
||||
|
||||
// error on any conflict
|
||||
|
||||
@@ -145,7 +145,7 @@ const actors = [
|
||||
// perv city
|
||||
{ entity: 'pervcity', name: 'Brooklyn Gray', fields: ['avatar', 'description', 'dateOfBirth', 'birthPlace', 'ethnicity', 'height', 'weight', 'eyes', 'hairColor'] },
|
||||
{ entity: 'dpdiva', name: 'Liz Jordan', fields: ['avatar', 'description', 'dateOfBirth', 'birthPlace', 'ethnicity', 'height', 'weight', 'eyes', 'hairColor'] },
|
||||
// { entity: 'bamvisions', name: 'Abella Danger', fields: ['avatar', 'height', 'measurements'] }, // site offline as of 2026-02-25
|
||||
{ entity: 'bamvisions', name: 'Abella Danger', fields: ['avatar', 'height', 'measurements'] }, // site offline as of 2026-02-25
|
||||
// radical
|
||||
{ entity: 'bjraw', name: 'Nikki Knightly', fields: ['avatar', 'description', 'gender', 'dateOfBirth', 'birthPlace', 'measurements', 'height', 'weight', 'eyes', 'hairColor'] },
|
||||
{ entity: 'gotfilled', name: 'Alexa Chains', fields: ['avatar', 'description', 'gender', 'dateOfBirth', 'birthPlace', 'measurements', 'height', 'weight', 'eyes', 'hairColor'] },
|
||||
@@ -153,6 +153,7 @@ const actors = [
|
||||
{ entity: 'topwebmodels', name: 'Lexi Belle', fields: ['avatar', 'dateOfBirth', 'birthPlace', 'measurements', 'height', 'weight', 'eyes', 'hairColor'] },
|
||||
{ entity: 'purgatoryx', name: 'Kenzie Reeves', fields: ['avatar', 'description', 'gender', 'dateOfBirth', 'birthPlace', 'measurements', 'height', 'weight', 'eyes', 'hairColor'] },
|
||||
{ entity: 'lucidflix', name: 'Ava Amira', fields: ['avatar', 'description', 'gender'] },
|
||||
{ entity: 'hardwerk', name: 'Luna Silver', fields: ['avatar', 'gender'] },
|
||||
// wankz
|
||||
{ entity: 'wankzvr', name: 'Melody Marks', fields: ['avatar', 'gender', 'description', 'birthPlace', 'height', 'measurements', 'age'] },
|
||||
{ entity: 'milfvr', name: 'Ember Snow', fields: ['avatar', 'gender', 'description', 'measurements', 'birthPlace', 'height', 'age'] },
|
||||
|
||||
Reference in New Issue
Block a user