119 Commits

Author SHA1 Message Date
DebaucheryLibrarian
7e227a4ea5 1.250.43 2026-03-17 23:54:35 +01:00
DebaucheryLibrarian
1c8df6415d Added Jawked to Karups, set up affiliate links. 2026-03-17 23:54:32 +01:00
DebaucheryLibrarian
0beb54312a Updating Manticore scenes after tag reassociation. 2026-03-16 04:58:46 +01:00
DebaucheryLibrarian
95d68fa966 1.250.42 2026-03-16 04:43:04 +01:00
DebaucheryLibrarian
50e7b1a437 Separated spitroast from MFM tag, added Hardwerk DP tags. 2026-03-16 04:43:02 +01:00
DebaucheryLibrarian
6cad394e88 Removed stray console log. 2026-03-16 02:50:31 +01:00
DebaucheryLibrarian
186f9660c3 Increased dangerous query bindings threshold. 2026-03-15 22:30:55 +01:00
DebaucheryLibrarian
ef7c24ab47 1.250.41 2026-03-15 21:33:16 +01:00
DebaucheryLibrarian
1b6aaafe10 Using batch insert module for media, calculating chunk size based on item size. 2026-03-15 21:33:14 +01:00
DebaucheryLibrarian
31aa1118e7 1.250.40 2026-03-15 20:07:53 +01:00
DebaucheryLibrarian
74d03b7483 Using browser for Nubiles, added She's Breeding Material. 2026-03-15 20:07:51 +01:00
DebaucheryLibrarian
40ea7eb80a 1.250.39 2026-03-15 17:42:08 +01:00
DebaucheryLibrarian
0d30115ad5 Refreshing entity slug cache in seeds. Added Hardwerk to Radical. 2026-03-15 17:42:07 +01:00
DebaucheryLibrarian
0560fac1ff 1.250.38 2026-03-13 05:14:45 +01:00
DebaucheryLibrarian
108bf3b168 Integrated manticore stash sync tool. 2026-03-13 05:14:42 +01:00
DebaucheryLibrarian
155e235246 Fixed Aylo specifying wrong host for media. 2026-03-10 05:54:00 +01:00
DebaucheryLibrarian
bff665c6ec 1.250.37 2026-03-10 04:41:36 +01:00
DebaucheryLibrarian
c7111329dc Improved knex error reporting. 2026-03-10 04:41:30 +01:00
DebaucheryLibrarian
d7c1c0ae5c 1.250.36 2026-03-09 05:36:21 +01:00
DebaucheryLibrarian
ea298d7edb Fixed Aylo scraper ignoring session configuration. 2026-03-09 05:36:17 +01:00
DebaucheryLibrarian
99dfcae920 1.250.35 2026-03-08 04:03:57 +01:00
DebaucheryLibrarian
24cba1e1fa Deleting flushed scenes from manticore. 2026-03-08 04:03:55 +01:00
DebaucheryLibrarian
076bdad310 1.250.34 2026-03-06 04:25:55 +01:00
DebaucheryLibrarian
d432d291dd Added See Him Solo to Hussie Pass, added HP affiliates. 2026-03-06 04:25:51 +01:00
DebaucheryLibrarian
220f7e787d 1.250.33 2026-03-05 02:00:53 +01:00
DebaucheryLibrarian
f1caa77e4b Added scene tags table to manticore scenes tool. 2026-03-05 02:00:43 +01:00
DebaucheryLibrarian
ff633436cb 1.250.32 2026-03-04 02:53:21 +01:00
DebaucheryLibrarian
6860072a51 Added database support for actor-specific scene tags. 2026-03-04 02:53:17 +01:00
DebaucheryLibrarian
2c7b4cfc22 1.250.31 2026-03-04 01:57:39 +01:00
DebaucheryLibrarian
7d9e1be8d4 Added Lesbian Factor. 2026-03-04 01:57:33 +01:00
DebaucheryLibrarian
00db4b1b5b 1.250.30 2026-03-03 23:47:50 +01:00
DebaucheryLibrarian
9f1cf1575a Added ASG Max channel parameters. 2026-03-03 23:47:48 +01:00
DebaucheryLibrarian
4f13e4ed28 1.250.29 2026-03-03 23:11:33 +01:00
DebaucheryLibrarian
9805aa7b5b Added Deep Inside to Disruptive Films. Added Sodomy Squad affiliate. 2026-03-03 23:11:32 +01:00
DebaucheryLibrarian
0cc6ebc305 1.250.28 2026-03-03 22:40:52 +01:00
DebaucheryLibrarian
016c24af28 Added channel filter option to Gamma scraper, re-added Disruptive Films channel. 2026-03-03 22:40:49 +01:00
DebaucheryLibrarian
2158550091 1.250.27 2026-03-03 01:21:00 +01:00
DebaucheryLibrarian
68ddc8cb78 Added Wicked affiliate. Improved Gamma banner tool filename composition. 2026-03-03 01:20:58 +01:00
DebaucheryLibrarian
bc5693e44a 1.250.26 2026-03-02 23:55:11 +01:00
DebaucheryLibrarian
7276d90629 Disabled tags by default in Gamma banner tool filenames. Added Gangbang Creampie, Gloryhole Secrets and Taboo Heat affiliates. 2026-03-02 23:55:09 +01:00
DebaucheryLibrarian
1a1af95a10 1.250.25 2026-03-02 22:36:08 +01:00
DebaucheryLibrarian
bcb7a56588 Added alt descriptions and attributes columns to series. 2026-03-02 22:36:05 +01:00
DebaucheryLibrarian
16648d50f6 Re-enabled filename actors and tags in Gamma banner tool, improved disable argument. 2026-03-02 06:24:54 +01:00
DebaucheryLibrarian
062dc0e75e 1.250.24 2026-03-02 06:21:30 +01:00
DebaucheryLibrarian
42effd53fc Added Diabolic affiliate. Disabled filename actors and tags in Gamma banner tool, unreliable. 2026-03-02 06:21:27 +01:00
DebaucheryLibrarian
3a3403bb1f 1.250.23 2026-03-02 06:07:49 +01:00
DebaucheryLibrarian
6fb4989256 Added Chaos Men affiliate. 2026-03-02 06:07:45 +01:00
DebaucheryLibrarian
9750ca4b79 1.250.22 2026-03-02 05:52:48 +01:00
DebaucheryLibrarian
0500f7eda8 Added Burning Angel affiliate. Fixed Gamma banner tool breaking on invalid URL. 2026-03-02 05:52:46 +01:00
DebaucheryLibrarian
19beff7dbc 1.250.21 2026-03-02 05:38:52 +01:00
DebaucheryLibrarian
dfe1b84992 Explicitly unsetting channel parent in seed. 2026-03-02 05:38:50 +01:00
DebaucheryLibrarian
3d3b544cb4 1.250.20 2026-03-02 05:07:26 +01:00
DebaucheryLibrarian
65fa6027ee Prioritized pissing tag. 2026-03-02 05:07:23 +01:00
DebaucheryLibrarian
b3a0ba72eb 1.250.19 2026-03-02 04:01:41 +01:00
DebaucheryLibrarian
f3e2143b45 Fixed wrong date parse function call in Gamma scraper. Added Biphoria affiliate link. 2026-03-02 04:01:39 +01:00
DebaucheryLibrarian
d289f95d3d 1.250.18 2026-03-02 03:46:48 +01:00
DebaucheryLibrarian
d8b41ec9b5 Use request interface for Vixen deep fetch, seemingly less chance of a 403. 2026-03-02 03:46:46 +01:00
DebaucheryLibrarian
05f7d8b814 1.250.17 2026-03-02 03:27:27 +01:00
DebaucheryLibrarian
c2fc09fdaa Removed redundant program filter from Gamma banner tool. 2026-03-02 03:27:24 +01:00
DebaucheryLibrarian
8a7210a3b9 1.250.16 2026-03-02 03:08:28 +01:00
DebaucheryLibrarian
e029ca7fd0 Added Gamma banner downloader. 2026-03-02 03:08:26 +01:00
DebaucheryLibrarian
ffcfae69d5 1.250.15 2026-03-02 03:07:12 +01:00
DebaucheryLibrarian
dcaee01ce8 Using channel origin instead of URL for Gamma referer URL composition. 2026-03-02 03:07:10 +01:00
DebaucheryLibrarian
7561a4577e 1.250.14 2026-03-02 01:41:38 +01:00
DebaucheryLibrarian
98b735dbae Added Vivid and Zero Tolerance affiliate links. Restored BAM Visions profile scraper, site is back online. 2026-03-02 01:41:36 +01:00
DebaucheryLibrarian
d2daed788c 1.250.13 2026-03-02 01:14:27 +01:00
DebaucheryLibrarian
23257745a7 Fixed profile updated_at timestamp not updating. 2026-03-02 01:14:23 +01:00
DebaucheryLibrarian
156954553d 1.250.12 2026-03-02 01:06:31 +01:00
DebaucheryLibrarian
eb20af14a6 Improved Gamma scene URL composition. 2026-03-02 01:06:29 +01:00
DebaucheryLibrarian
ae247c7a91 1.250.11 2026-03-02 00:49:04 +01:00
DebaucheryLibrarian
d49e6ef488 Explicitly unsetting parameters in seed. 2026-03-02 00:49:01 +01:00
DebaucheryLibrarian
2b20d98ee0 Removed stray console log. 2026-03-01 23:53:22 +01:00
DebaucheryLibrarian
b8cf6a3e71 1.250.10 2026-03-01 23:52:46 +01:00
DebaucheryLibrarian
af57f412c9 Refactored Gamma scraper, only using API. 2026-03-01 23:52:41 +01:00
DebaucheryLibrarian
3696b81e69 1.250.9 2026-03-01 20:45:32 +01:00
DebaucheryLibrarian
5b6fefd43b Rounding actor profile values stored as integers to prevent database errors. 2026-03-01 20:45:30 +01:00
DebaucheryLibrarian
a863ab888d 1.250.8 2026-03-01 19:58:37 +01:00
DebaucheryLibrarian
209a81ef71 Removed Vivid wrapper, updated channel URLs. 2026-03-01 19:58:35 +01:00
DebaucheryLibrarian
bd91dcbc77 1.250.7 2026-03-01 04:49:04 +01:00
DebaucheryLibrarian
b89f25405a Using batch insert for various actor scraping inserts. 2026-03-01 04:49:01 +01:00
DebaucheryLibrarian
198f08cb3a Removed stray console log. 2026-03-01 04:28:09 +01:00
DebaucheryLibrarian
febaac3865 1.250.6 2026-03-01 04:27:32 +01:00
DebaucheryLibrarian
f82167656b Changed actor foot column to decimal. 2026-03-01 04:27:29 +01:00
DebaucheryLibrarian
6e20d7d216 1.250.5 2026-02-27 00:55:16 +01:00
DebaucheryLibrarian
612a489cdf Fixed actor scraper list reference. 2026-02-27 00:55:14 +01:00
DebaucheryLibrarian
db2e5b2da4 1.250.4 2026-02-27 00:51:13 +01:00
DebaucheryLibrarian
d81310ed25 Removed outdated profile source list. 2026-02-27 00:51:11 +01:00
DebaucheryLibrarian
ec86aa9286 1.250.3 2026-02-26 00:04:41 +01:00
DebaucheryLibrarian
5d58ddcd49 Disabled BAM Visions profile test while site is offline. 2026-02-26 00:04:39 +01:00
DebaucheryLibrarian
c515c8aeb3 1.250.2 2026-02-26 00:00:41 +01:00
DebaucheryLibrarian
debf92afd7 Changed MetroHD test actor to Vanna Bardot, April Olsen returns implausible weight 64, which seems to be a data error (too low for lbs, too high for kg). 2026-02-26 00:00:37 +01:00
DebaucheryLibrarian
601f930324 1.250.1 2026-02-25 01:09:53 +01:00
DebaucheryLibrarian
e77ced44c7 Added batch insert util to replace bulk insert. Fixed circular dependencies. 2026-02-25 01:09:49 +01:00
DebaucheryLibrarian
9f37f54634 1.250.0 2026-02-24 06:17:41 +01:00
DebaucheryLibrarian
dc7f325d13 Added scene media detach. 2026-02-24 06:17:38 +01:00
DebaucheryLibrarian
35c941488e 1.249.15 2026-02-24 05:50:06 +01:00
DebaucheryLibrarian
fc32843c5a Expanded title query in Hush scraper. 2026-02-24 05:50:04 +01:00
DebaucheryLibrarian
26b31fb10a 1.249.14 2026-02-24 05:39:07 +01:00
DebaucheryLibrarian
9aa6c9c6c5 Added Rave Bunnys and Hot and Tatted to Hussie Pass, improved scraper. Only looking for one valid avatar URL in profile tests. 2026-02-24 05:39:05 +01:00
DebaucheryLibrarian
855a15bc73 1.249.13 2026-02-24 05:08:08 +01:00
DebaucheryLibrarian
3329661135 Added profile referer parameter to Gamma, needed for Dogfart. 2026-02-24 05:07:23 +01:00
DebaucheryLibrarian
791bd6bf27 1.249.12 2026-02-24 04:46:16 +01:00
DebaucheryLibrarian
d6be985c4b Refactored Hush / Hussie Pass with unprint. 2026-02-24 04:46:12 +01:00
DebaucheryLibrarian
7286846308 1.249.11 2026-02-24 03:37:11 +01:00
DebaucheryLibrarian
81dfce8b3d Updated POV Pornstars parameter URLs to https. 2026-02-24 03:37:09 +01:00
DebaucheryLibrarian
aff0e27c55 1.249.10 2026-02-24 03:32:56 +01:00
DebaucheryLibrarian
68fe786cb7 Updated POV Pornstars URL to https. 2026-02-24 03:32:53 +01:00
DebaucheryLibrarian
9a0b0a8989 1.249.9 2026-02-24 03:15:20 +01:00
DebaucheryLibrarian
60b8271e4f Updated unprint to fix response OK. 2026-02-24 03:15:18 +01:00
DebaucheryLibrarian
a52042b56c 1.249.8 2026-02-24 02:32:42 +01:00
DebaucheryLibrarian
7a3dac865e Updated unprint for browser context close fix. 2026-02-24 02:32:40 +01:00
DebaucheryLibrarian
74e0fb721d 1.249.7 2026-02-24 02:12:19 +01:00
DebaucheryLibrarian
ba366df7a5 Added entity resolution prefer to entity options. 2026-02-24 02:12:16 +01:00
DebaucheryLibrarian
d4e6082d2e 1.249.6 2026-02-24 01:32:43 +01:00
DebaucheryLibrarian
ea325b8ec5 Removed unavailable profile details from Fantasy Massage profile test. 2026-02-24 01:32:41 +01:00
DebaucheryLibrarian
41b1f39752 1.249.5 2026-02-24 01:28:24 +01:00
DebaucheryLibrarian
c75c3e3ed9 Changed profile test to prefer network. Removed stray console from Gamma. 2026-02-24 01:28:22 +01:00
DebaucheryLibrarian
ee495a5cde 1.249.4 2026-02-24 01:18:22 +01:00
DebaucheryLibrarian
b52e871cfe Passing network channels as site scopes in Gamma API profile scraper. 2026-02-24 01:18:20 +01:00
40 changed files with 1689 additions and 2088 deletions

View File

@@ -27,7 +27,7 @@
"require-await": "off",
"no-param-reassign": ["error", {
"props": true,
"ignorePropertyModificationsFor": ["state", "acc", "req"]
"ignorePropertyModificationsFor": ["state", "acc", "req", "error"]
}]
},
"globals": {

View File

@@ -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
@@ -196,144 +198,7 @@ module.exports = {
'forbondage',
],
},
profiles: [
[
'evilangel',
'famedigital',
'devilsfilm',
'roccosiffredi',
],
[
// Gamma; Evil Angel + Devil's Film, Pure Taboo (unavailable), (sometimes) Burning Angel and Wicked have their own assets
'xempire',
'blowpass',
],
[
// MindGeek; Mile High Media has its own assets
'brazzers',
'realitykings',
'mofos',
'digitalplayground',
'twistys',
'babes',
'fakehub',
'sexyhub',
'metrohd',
'iconmale',
'men',
'transangels',
],
'wicked',
'burningangel',
'milehighmedia',
[
'vixen',
'tushy',
'blacked',
'tushyraw',
'blackedraw',
'deeper',
],
[
// Nubiles
'nubiles',
'nubilesporn',
'deeplush',
'brattysis',
'nfbusty',
'anilos',
'hotcrazymess',
'thatsitcomshow',
],
'21sextury',
'dogfartnetwork',
'adultempire',
'julesjordan',
'dorcelclub',
'bang',
'pervcity',
'kink',
'peternorth',
'naughtyamerica',
'cherrypimps',
'pimpxxx',
'18vr',
'babevr',
'badoinkvr',
'realvr',
'vrcosplayx',
'teamskeet',
'mylf',
'spermmania',
[
'letsdoeit',
'mamacitaz',
'forbondage',
'amateureuro',
'vipsexvault',
'transbella',
],
[
'hussiepass',
'hushpass',
'interracialpass',
'interracialpovs',
'povpornstars',
'seehimfuck',
'eyeontheguy',
],
[
// Full Porn Network
'analized',
'hergape',
'jamesdeen',
'dtfsluts',
'analbbc',
'analviolation',
'baddaddypov',
'girlfaction',
'homemadeanalwhores',
'mugfucked',
'onlyprince',
'pervertgallery',
'povperverts',
],
'wankzvr',
'milfvr',
'tranzvr',
'topwebmodels',
'pascalssubsluts',
'kellymadison',
'5kporn',
'private',
'bangbros',
'hitzefrei',
'porncz',
'czechav',
'angelogodshackoriginal',
'littlecapricedreams',
'missyx',
'gangbangcreampie',
'gloryholesecrets',
'aziani',
[
'firstanalquest',
'doubleviewcasting',
],
[
'silverstonedvd',
'silviasaint',
],
[
'analvids',
'pornworld',
],
'pierrewoodman',
'score',
'boobpedia',
'pornhub',
'freeones',
],
profiles: null,
interpolation: {
excludeAvatarCredits: [ // never allow
'Pierre Woodman',
@@ -416,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

View 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();
});
};

View 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');
});
};

View 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');
};

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "traxxx",
"version": "1.249.3",
"version": "1.250.43",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "traxxx",
"version": "1.249.3",
"version": "1.250.43",
"license": "ISC",
"dependencies": {
"@aws-sdk/client-s3": "^3.458.0",
@@ -94,7 +94,7 @@
"tunnel": "0.0.6",
"ua-parser-js": "^1.0.37",
"undici": "^5.28.1",
"unprint": "^0.18.33",
"unprint": "^0.18.35",
"url-pattern": "^1.0.3",
"v-tooltip": "^2.1.3",
"video.js": "^8.6.1",
@@ -20385,9 +20385,9 @@
}
},
"node_modules/unprint": {
"version": "0.18.33",
"resolved": "https://registry.npmjs.org/unprint/-/unprint-0.18.33.tgz",
"integrity": "sha512-SCVWfYCou5aUXxJOFaG2LOmhRB+NO5ZxyO4gnpS2iUCb7vKpEgAqpgux5kL8AX0vg7Uhir8+X/Z4c/6tsLhUVQ==",
"version": "0.18.35",
"resolved": "https://registry.npmjs.org/unprint/-/unprint-0.18.35.tgz",
"integrity": "sha512-oTCBE8pGzfTFlSb0QbYv/ctICTmcU/K81gOPfchn+efLHu48hq1S3582JHvwXAXCjiRKZYatJlEFzUTXVtfuvA==",
"license": "ISC",
"dependencies": {
"bottleneck": "^2.19.5",

View File

@@ -1,6 +1,6 @@
{
"name": "traxxx",
"version": "1.249.3",
"version": "1.250.43",
"description": "All the latest porn releases in one place",
"main": "src/app.js",
"scripts": {
@@ -153,7 +153,7 @@
"tunnel": "0.0.6",
"ua-parser-js": "^1.0.37",
"undici": "^5.28.1",
"unprint": "^0.18.33",
"unprint": "^0.18.35",
"url-pattern": "^1.0.3",
"v-tooltip": "^2.1.3",
"video.js": "^8.6.1",

View File

@@ -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',
@@ -963,6 +967,11 @@ const tags = [
slug: 'pyjamas',
group: 'clothing',
},
{
name: 'rave',
slug: 'rave',
group: 'clothing',
},
{
name: 'redhead',
slug: 'redhead',
@@ -1888,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',
@@ -2350,11 +2367,6 @@ const aliases = [
name: 'spit',
for: 'saliva',
},
{
name: 'spitroast',
for: 'mfm',
secondary: true,
},
{
name: 'spoon',
for: 'spooning',
@@ -3014,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; });

View File

@@ -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 = [
{
@@ -9,6 +17,9 @@ const grandParentNetworks = [
name: 'Gamma Entertainment',
url: 'https://www.gammaentertainment.com',
alias: ['gammaentertainment'],
options: {
preferNetwork: true,
},
},
{
slug: 'hush',
@@ -56,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',
@@ -105,7 +115,6 @@ const parentNetworks = [
url: 'https://www.asgmax.com',
parent: 'gamma',
parameters: {
layout: 'api',
scene: 'https://www.asgmax.com/en/video/asgmax',
},
},
@@ -187,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',
@@ -198,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',
@@ -228,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,
},
@@ -320,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',
@@ -373,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',
@@ -419,7 +421,7 @@ 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',
},
},
{
@@ -432,9 +434,6 @@ const networks = [
slug: 'fantasymassage',
name: 'Fantasy Massage',
url: 'https://www.fantasymassage.com',
parameters: {
layout: 'api',
},
parent: 'gamma',
},
{
@@ -444,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',
},
@@ -464,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',
@@ -505,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',
},
{
@@ -555,7 +549,6 @@ const networks = [
slug: 'jayrock',
name: 'JayRock Productions',
url: 'http://jayrockcontent.com',
parent: 'gamma',
},
{
slug: 'julesjordan',
@@ -759,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',
},
@@ -857,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,
},
},
{
@@ -883,10 +875,8 @@ 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}',
actorAvailableOnSites: ['allblackx', 'darkx', 'eroticax', 'hardx', 'lesbianx', 'xempire', 'xempirepartners'],
},
},
{
@@ -894,9 +884,6 @@ const networks = [
name: 'Zero Tolerance',
alias: ['ztod'],
url: 'https://www.zerotolerancefilms.com',
parameters: {
layout: 'api',
},
parent: 'gamma',
},
// ASG MAX
@@ -926,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();
};

View File

@@ -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',
@@ -5558,6 +5574,19 @@ const sites = [
url: 'https://seehimfuck.com',
tags: ['male-focus'],
parent: 'hussiepass',
parameters: {
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',
@@ -5569,14 +5598,28 @@ const sites = [
{
slug: 'povpornstars',
name: 'POV Pornstars',
url: 'http://www.povpornstars.com',
url: 'https://www.povpornstars.com',
tags: ['pov'],
parent: 'hussiepass',
parameters: {
latest: 'http://www.povpornstars.com/tour/categories/movies_%d_d.html',
profile: 'http://www.povpornstars.com/tour/models/%s.html',
latest: 'https://www.povpornstars.com/tour/categories/movies_{page}_d.html',
profile: 'https://www.povpornstars.com/tour/models/{actor}.html',
},
},
{
slug: 'ravebunnys',
name: 'Rave Bunnys',
url: 'https://ravebunnys.com',
tags: ['rave'],
parent: 'hussiepass',
},
{
slug: 'hotandtatted',
name: 'Hot and Tatted',
url: 'https://hotandtatted.com',
tags: ['tattoos'],
parent: 'hussiepass',
},
// HUSH PASS
{
slug: 'shotherfirst',
@@ -5584,7 +5627,7 @@ const sites = [
url: 'https://shotherfirst.com',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/shot-her-first_%d_d.html',
latest: 'https://hushpass.com/t1/categories/shot-her-first_{page}_d.html',
media: 'https://hushpass.com',
t1: true,
},
@@ -5595,7 +5638,7 @@ const sites = [
url: 'https://whitezilla.com',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/whitezilla_%d_d.html',
latest: 'https://hushpass.com/t1/categories/whitezilla_{page}_d.html',
media: 'https://hushpass.com',
t1: true,
},
@@ -5606,7 +5649,7 @@ const sites = [
url: 'https://frathousefuckfest.com',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/frat-house-fuck-fest_%d_d.html',
latest: 'https://hushpass.com/t1/categories/frat-house-fuck-fest_{page}_d.html',
media: 'https://hushpass.com',
t1: true,
},
@@ -5617,7 +5660,7 @@ const sites = [
url: 'https://freakyfirsttimers.com',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/freaky-first-timers_%d_d.html',
latest: 'https://hushpass.com/t1/categories/freaky-first-timers_{page}_d.html',
media: 'https://hushpass.com',
t1: true,
},
@@ -5628,7 +5671,7 @@ const sites = [
url: 'https://milfinvaders.com',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/milf-invaders_%d_d.html',
latest: 'https://hushpass.com/t1/categories/milf-invaders_{page}_d.html',
media: 'https://hushpass.com',
t1: true,
},
@@ -5639,7 +5682,7 @@ const sites = [
url: 'https://housewivesneedcash.com',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/housewives-need-cash_%d_d.html',
latest: 'https://hushpass.com/t1/categories/housewives-need-cash_{page}_d.html',
media: 'https://hushpass.com',
t1: true,
},
@@ -5650,7 +5693,7 @@ const sites = [
url: 'https://bubblebuttbonanza.com',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/bubble-butt-bonanza_%d_d.html',
latest: 'https://hushpass.com/t1/categories/bubble-butt-bonanza_{page}_d.html',
media: 'https://hushpass.com',
t1: true,
},
@@ -5661,7 +5704,7 @@ const sites = [
url: 'https://suburbansexparty.com',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/suburban-sex-party_%d_d.html',
latest: 'https://hushpass.com/t1/categories/suburban-sex-party_{page}_d.html',
media: 'https://hushpass.com',
t1: true,
},
@@ -5672,7 +5715,7 @@ const sites = [
url: 'https://buttnakedinthestreets.com',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/ButtNakedInStreets_%d_d.html',
latest: 'https://hushpass.com/t1/categories/ButtNakedInStreets_{page}_d.html',
media: 'https://hushpass.com',
match: 'Butt Naked In Streets',
t1: true,
@@ -5684,7 +5727,7 @@ const sites = [
url: 'https://muffbumperpatrol.com',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/muff-bumper-patrol_%d_d.html',
latest: 'https://hushpass.com/t1/categories/muff-bumper-patrol_{page}_d.html',
media: 'https://hushpass.com',
t1: true,
},
@@ -5695,7 +5738,7 @@ const sites = [
url: 'https://biggathananigga.com',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/bigga-than-a-nigga_%d_d.html',
latest: 'https://hushpass.com/t1/categories/bigga-than-a-nigga_{page}_d.html',
media: 'https://hushpass.com',
t1: true,
},
@@ -5706,7 +5749,7 @@ const sites = [
url: 'https://bachelorpartyfuckfest.com',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/bachelor-party-fuck-fest_%d_d.html',
latest: 'https://hushpass.com/t1/categories/bachelor-party-fuck-fest_{page}_d.html',
media: 'https://hushpass.com',
t1: true,
},
@@ -5717,7 +5760,7 @@ const sites = [
url: 'https://teencumdumpsters.com',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/teen-cum-dumpsters_%d_d.html',
latest: 'https://hushpass.com/t1/categories/teen-cum-dumpsters_{page}_d.html',
media: 'https://hushpass.com',
t1: true,
},
@@ -5727,7 +5770,7 @@ const sites = [
name: 'POV Hunnies',
parent: 'hushpass',
parameters: {
latest: 'https://hushpass.com/t1/categories/POVHunnies_%d_d.html',
latest: 'https://hushpass.com/t1/categories/POVHunnies_{page}_d.html',
media: 'https://hushpass.com',
t1: true,
},
@@ -5865,7 +5908,7 @@ const sites = [
tags: ['interracial'],
parent: 'interracialpass',
parameters: {
latest: 'https://www.interracialpass.com/t1/categories/2-big-to-be-true_%d_d.html',
latest: 'https://www.interracialpass.com/t1/categories/2-big-to-be-true_{page}_d.html',
media: 'https://www.interracialpass.com',
t1: true,
},
@@ -5877,7 +5920,7 @@ const sites = [
tags: ['interracial'],
parent: 'interracialpass',
parameters: {
latest: 'https://www.interracialpass.com/t1/categories/abominable-black-man_%d_d.html',
latest: 'https://www.interracialpass.com/t1/categories/abominable-black-man_{page}_d.html',
media: 'https://www.interracialpass.com',
t1: true,
},
@@ -5889,7 +5932,7 @@ const sites = [
parent: 'interracialpass',
hasLogo: false,
parameters: {
latest: 'https://www.interracialpass.com/t1/categories/BootyAnnihilation_%d_d.html',
latest: 'https://www.interracialpass.com/t1/categories/BootyAnnihilation_{page}_d.html',
media: 'https://www.interracialpass.com',
t1: true,
},
@@ -5901,7 +5944,7 @@ const sites = [
tags: ['interracial'],
parent: 'interracialpass',
parameters: {
latest: 'https://www.interracialpass.com/t1/categories/daddys-worst-nightmare_%d_d.html',
latest: 'https://www.interracialpass.com/t1/categories/daddys-worst-nightmare_{page}_d.html',
media: 'https://www.interracialpass.com',
t1: true,
},
@@ -5913,7 +5956,7 @@ const sites = [
tags: ['interracial'],
parent: 'interracialpass',
parameters: {
latest: 'https://www.interracialpass.com/t1/categories/monster-cock-fuck-fest_%d_d.html',
latest: 'https://www.interracialpass.com/t1/categories/monster-cock-fuck-fest_{page}_d.html',
media: 'https://www.interracialpass.com',
t1: true,
},
@@ -5925,7 +5968,7 @@ const sites = [
tags: ['interracial'],
parent: 'interracialpass',
parameters: {
latest: 'https://www.interracialpass.com/t1/categories/my-daughters-fucking-a-black-dude_%d_d.html',
latest: 'https://www.interracialpass.com/t1/categories/my-daughters-fucking-a-black-dude_{page}_d.html',
media: 'https://www.interracialpass.com',
t1: true,
},
@@ -5937,7 +5980,7 @@ const sites = [
tags: ['interracial'],
parent: 'interracialpass',
parameters: {
latest: 'https://www.interracialpass.com/t1/categories/my-moms-fucking-blackzilla_%d_d.html',
latest: 'https://www.interracialpass.com/t1/categories/my-moms-fucking-blackzilla_{page}_d.html',
media: 'https://www.interracialpass.com',
t1: true,
},
@@ -5949,7 +5992,7 @@ const sites = [
tags: ['interracial'],
parent: 'interracialpass',
parameters: {
latest: 'https://www.interracialpass.com/t1/categories/my-wifes-first-monster-cock_%d_d.html',
latest: 'https://www.interracialpass.com/t1/categories/my-wifes-first-monster-cock_{page}_d.html',
media: 'https://www.interracialpass.com',
match: 'My Wifes First Monster Cock',
t1: true,
@@ -6062,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,
@@ -6130,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',
},
{
@@ -6138,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',
},
{
@@ -6146,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',
},
{
@@ -6157,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',
@@ -9395,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
@@ -10520,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',
@@ -15019,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',
@@ -15041,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
{
@@ -15460,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
@@ -15558,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",
];
*/

View File

@@ -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',

View File

@@ -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,11 +831,11 @@ 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([
fetchEntitiesBySlug(entitySlugs, { types: ['channel', 'network', 'info'], prefer: argv.prefer || 'channel' }),
fetchEntitiesBySlug(entitySlugs, { types: ['channel', 'network', 'info'], prefer: argv.prefer || 'options' }),
knex('actors')
.select(knex.raw('actors.id, actors.name, actors.slug, actors.entry_id, actors.entity_id, row_to_json(entities) as entity'))
.whereIn('actors.slug', baseActors.map((baseActor) => baseActor.slug))

View File

@@ -24,7 +24,7 @@ const { updateSceneSearch, updateMovieSearch } = require('./update-search');
const { scrapeActors, deleteActors, flushActors, flushProfiles, interpolateProfiles } = require('./actors');
const { flushEntities } = require('./entities');
const { deleteScenes, deleteMovies, flushScenes, flushMovies, flushBatches } = require('./releases');
const { flushOrphanedMedia } = require('./media');
const { flushOrphanedMedia, detachReleaseMedia, detachEntityReleaseMedia } = require('./media');
const { reassociateEntityReleaseTags, reassociateReleaseTags, reassociateOriginalTags } = require('./tags');
const getFileEntries = require('./utils/file-entries');
@@ -160,7 +160,8 @@ async function init() {
}
if (argv.flushNetworks || argv.flushChannels) {
await flushEntities(argv.flushNetworks, argv.flushChannels);
// inject flushOrphanedMedia to prevent circular dependency with entity media flush
await flushEntities(argv.flushNetworks, argv.flushChannels, flushOrphanedMedia);
}
if (argv.flushBatches) {
@@ -203,6 +204,14 @@ async function init() {
await flushOrphanedMedia();
}
if (argv.detachReleaseMedia) {
await detachReleaseMedia(argv.detachReleaseMedia);
}
if (argv.detachNetworkMedia || argv.detachChannelMedia) {
await detachEntityReleaseMedia(argv.detachNetworkMedia, argv.detachChannelMedia);
}
if (argv.request) {
const res = await http[argv.requestMethod](argv.request);

View File

@@ -349,7 +349,32 @@ const { argv } = yargs
describe: 'Remove files from storage when flushing media.',
type: 'boolean',
alias: 'flush-files',
default: true,
})
.option('detach-channel-media', {
describe: 'Remove media files from channel scenes.',
type: 'array',
})
.option('detach-network-media', {
describe: 'Remove media files from network scenes.',
type: 'array',
})
.option('detach-release-media', {
describe: 'Remove media files from network scenes.',
type: 'array',
alias: ['detach-scene-media'],
})
.option('detach-media-domains', {
describe: 'Only detach these types of media.',
type: 'array',
default: [
'posters',
'photos',
'caps',
'trailers',
'teasers',
'covers',
],
alias: ['detach-media'],
})
.option('flush-channels', {
describe: 'Delete all scenes and movies from channels.',

View File

@@ -7,8 +7,9 @@ const logger = require('./logger')(__filename);
const argv = require('./argv');
const knex = require('./knex');
const { deleteScenes, deleteMovies, deleteSeries } = require('./releases');
const { flushOrphanedMedia } = require('./media');
const { resolveScraper, resolveLayoutScraper } = require('./scrapers/resolve');
const { fetchEntityReleaseIds } = require('./entity-releases');
const getRecursiveParameters = require('./utils/get-recursive-parameters');
function getRecursiveParent(entity) {
if (!entity) {
@@ -257,13 +258,18 @@ async function fetchEntitiesBySlug(entitySlugs, options = { prefer: 'channel', a
entitySlugs: entitySlugs.filter((slug) => !slug.includes('.')),
entityHosts: entitySlugs.filter((slug) => slug.includes('.')).map((hostname) => `%${hostname}`),
entityTypes: options.types || ['channel', 'network'],
sort: knex.raw(options.prefer === 'channel' ? 'asc' : 'desc'),
sort: knex.raw(options.prefer === 'channel' || options.prefer === 'options' ? 'asc' : 'desc'),
});
// channel entity will overwrite network entity
// by default channel entity will overwrite network entity
const entitiesBySlug = entities.rows.reduce((accEntities, { entity }) => {
const host = urlToHostname(entity.url);
const curatedEntity = accEntities[entity.slug] || accEntities[host] || curateEntity(entity, true);
const entityOptions = getRecursiveParameters(entity, 'options');
const accEntity = accEntities[entity.slug] || accEntities[host];
const curatedEntity = !accEntity || (options.prefer === 'options' && entity.type === 'network' && entityOptions.preferNetwork)
? curateEntity(entity, true)
: accEntity;
return {
...accEntities,
@@ -368,87 +374,7 @@ async function searchEntities(query, type, limit) {
return curateEntities(entities);
}
async function fetchEntityReleaseIds(networkSlugs = [], channelSlugs = []) {
const entityQuery = knex
.withRecursive('selected_entities', knex.raw(`
SELECT entities.*
FROM entities
WHERE
entities.slug = ANY(:networkSlugs)
AND entities.type = 'network'
OR (entities.slug = ANY(:channelSlugs)
AND entities.type = 'channel')
UNION ALL
SELECT entities.*
FROM entities
INNER JOIN selected_entities ON selected_entities.id = entities.parent_id
`, {
networkSlugs,
channelSlugs,
}));
const sceneIds = await entityQuery
.clone()
.select('releases.id')
.distinct('releases.id')
.from('selected_entities')
.leftJoin('releases', 'releases.entity_id', 'selected_entities.id')
.whereNotNull('releases.id')
.modify((builder) => {
if (argv.flushAfter) {
builder.where('effective_date', '>=', argv.flushAfter);
}
if (argv.flushBefore) {
builder.where('effective_date', '<=', argv.flushBefore);
}
})
.pluck('releases.id');
const movieIds = await entityQuery
.clone()
.select('movies.id')
.distinct('movies.id')
.from('selected_entities')
.leftJoin('movies', 'movies.entity_id', 'selected_entities.id')
.whereNotNull('movies.id')
.modify((builder) => {
if (argv.flushAfter) {
builder.where('effective_date', '>=', argv.flushAfter);
}
if (argv.flushBefore) {
builder.where('effective_date', '<=', argv.flushBefore);
}
})
.pluck('movies.id');
const serieIds = await entityQuery
.clone()
.select('series.id')
.distinct('series.id')
.from('selected_entities')
.leftJoin('series', 'series.entity_id', 'selected_entities.id')
.whereNotNull('series.id')
.modify((builder) => {
if (argv.flushAfter) {
builder.where('date', '>=', argv.flushAfter);
}
if (argv.flushBefore) {
builder.where('date', '<=', argv.flushBefore);
}
})
.pluck('series.id');
return {
sceneIds,
movieIds,
serieIds,
};
}
async function flushEntities(networkSlugs = [], channelSlugs = []) {
async function flushEntities(networkSlugs = [], channelSlugs = [], flushOrphanedMedia) {
const { sceneIds, movieIds, serieIds } = await fetchEntityReleaseIds(networkSlugs, channelSlugs);
const entitySlugs = networkSlugs.concat(channelSlugs).join(', ');

88
src/entity-releases.js Normal file
View File

@@ -0,0 +1,88 @@
'use strict';
const knex = require('./knex');
const argv = require('./argv');
async function fetchEntityReleaseIds(networkSlugs = [], channelSlugs = []) {
const entityQuery = knex
.withRecursive('selected_entities', knex.raw(`
SELECT entities.*
FROM entities
WHERE
entities.slug = ANY(:networkSlugs)
AND entities.type = 'network'
OR (entities.slug = ANY(:channelSlugs)
AND entities.type = 'channel')
UNION ALL
SELECT entities.*
FROM entities
INNER JOIN selected_entities ON selected_entities.id = entities.parent_id
`, {
networkSlugs,
channelSlugs,
}));
const sceneIds = await entityQuery
.clone()
.select('releases.id')
.distinct('releases.id')
.from('selected_entities')
.leftJoin('releases', 'releases.entity_id', 'selected_entities.id')
.whereNotNull('releases.id')
.modify((builder) => {
if (argv.flushAfter) {
builder.where('effective_date', '>=', argv.flushAfter);
}
if (argv.flushBefore) {
builder.where('effective_date', '<=', argv.flushBefore);
}
})
.pluck('releases.id');
const movieIds = await entityQuery
.clone()
.select('movies.id')
.distinct('movies.id')
.from('selected_entities')
.leftJoin('movies', 'movies.entity_id', 'selected_entities.id')
.whereNotNull('movies.id')
.modify((builder) => {
if (argv.flushAfter) {
builder.where('effective_date', '>=', argv.flushAfter);
}
if (argv.flushBefore) {
builder.where('effective_date', '<=', argv.flushBefore);
}
})
.pluck('movies.id');
const serieIds = await entityQuery
.clone()
.select('series.id')
.distinct('series.id')
.from('selected_entities')
.leftJoin('series', 'series.entity_id', 'selected_entities.id')
.whereNotNull('series.id')
.modify((builder) => {
if (argv.flushAfter) {
builder.where('date', '>=', argv.flushAfter);
}
if (argv.flushBefore) {
builder.where('date', '<=', argv.flushBefore);
}
})
.pluck('series.id');
return {
sceneIds,
movieIds,
serieIds,
};
}
module.exports = {
fetchEntityReleaseIds,
};

View File

@@ -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;

View File

@@ -23,9 +23,10 @@ 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');
// const pipeline = util.promisify(stream.pipeline);
const streamQueue = taskQueue();
@@ -646,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 }),
},
@@ -922,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) {
@@ -991,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'})`);
@@ -1159,8 +1161,36 @@ async function flushOrphanedMedia(stage = 1) {
}
}
async function detachReleaseMedia(rawSceneIds) {
const sceneIds = rawSceneIds.map((sceneId) => Number(sceneId)).filter(Boolean);
await argv.detachMediaDomains.reduce(async (chain, domain) => {
await chain;
const mediaEntries = await knex(`releases_${domain}`).whereIn('release_id', sceneIds);
await knex(`releases_${domain}`)
.whereIn('release_id', sceneIds)
.delete();
logger.info(`Removed ${mediaEntries.length} ${domain} from ${new Set(mediaEntries.map((mediaEntry) => mediaEntry.release_id)).size} scenes`);
}, Promise.resolve());
if (argv.flushOrphanedMedia !== false) {
await flushOrphanedMedia();
}
}
async function detachEntityReleaseMedia(networkSlugs = [], channelSlugs = []) {
const { sceneIds } = await fetchEntityReleaseIds(networkSlugs, channelSlugs);
await detachReleaseMedia(sceneIds);
}
module.exports = {
associateAvatars,
associateReleaseMedia,
flushOrphanedMedia,
detachReleaseMedia,
detachEntityReleaseMedia,
};

View File

@@ -140,6 +140,7 @@ module.exports = {
purgatoryx: radical,
topwebmodels: radical,
lucidflix: radical,
hardwerk: radical,
// hush / hussiepass
eyeontheguy: hush,
hushpass: hush,
@@ -148,6 +149,8 @@ module.exports = {
interracialpovs: hush,
povpornstars: hush,
seehimfuck: hush,
ravebunnys: hush,
hotandtatted: hush,
// wankzvr
wankzvr,
tranzvr: wankzvr,

View File

@@ -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);

View File

@@ -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,
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,21 @@
'use strict';
const util = require('util');
const unprint = require('unprint');
const format = require('template-format');
const qu = require('../utils/q');
const slugify = require('../utils/slugify');
const { feetInchesToCm, inchesToCm } = require('../utils/convert');
const tryUrls = require('../utils/try-urls');
const { convert } = require('../utils/convert');
function deriveEntryId(release) {
if (release.date && release.url) {
const slug = new URL(release.url).pathname.match(/\/trailers\/(.*).html/)[1];
return `${slugify(qu.formatDate(release.date, 'YYYY-MM-DD'))}-${slugify(slug)}`;
return `${slugify(unprint.formatDate(release.date, 'YYYY-MM-DD'))}-${slugify(slug)}`;
}
if (release.date && release.title) {
return `${slugify(qu.formatDate(release.date, 'YYYY-MM-DD'))}-${slugify(release.title)}`;
return `${slugify(unprint.formatDate(release.date, 'YYYY-MM-DD'))}-${slugify(release.title)}`;
}
return null;
@@ -22,7 +23,7 @@ function deriveEntryId(release) {
function extractPoster(posterPath, site, baseRelease) {
if (posterPath && !/400.jpg/.test(posterPath)) {
const poster = `${site.parameters?.media || site.url}${posterPath}`;
const poster = unprint.prefixUrl(posterPath, site.parameters?.media || site.url);
const posterSources = [
poster,
// upscaled
@@ -40,38 +41,38 @@ function extractPoster(posterPath, site, baseRelease) {
return [baseRelease?.poster || null, []];
}
function getImageWithFallbacks(q, selector, site, el) {
function getImageWithFallbacks(query, selector, site, el) {
const sources = el
? [
q(el, selector, 'src0_3x'),
q(el, selector, 'src0_2x'),
q(el, selector, 'src0_1x'),
unprint.query.attribute(el, selector, 'src0_3x'),
unprint.query.attribute(el, selector, 'src0_2x'),
unprint.query.attribute(el, selector, 'src0_1x'),
]
: [
q(selector, 'src0_3x'),
q(selector, 'src0_2x'),
q(selector, 'src0_1x'),
query.attribute(selector, 'src0_3x'),
query.attribute(selector, 'src0_2x'),
query.attribute(selector, 'src0_1x'),
];
return sources.filter(Boolean).map((src) => `${site.parameters?.media || site.url}${src}`);
return sources.filter(Boolean).map((src) => unprint.prefixUrl(src, site.parameters?.media || site.url));
}
function scrapeAll(scenes, channel) {
return scenes.map(({ query }) => {
const release = {};
release.title = query.q('h4 a', true);
release.title = query.content('h4 a');
release.url = query.url('a');
release.date = query.date('.date', 'YYYY-MM-DD');
release.duration = query.duration('.time');
const count = query.number('a img', null, 'cnt');
const count = query.number('a img', { attribute: 'cnt' });
[release.poster, ...release.photos] = Array.from({ length: count }, (value, index) => [
query.img('a img', `src${index}_3x`, { origin: channel.url }),
query.img('a img', `src${index}_2x`, { origin: channel.url }),
query.img('a img', `src${index}_1x`, { origin: channel.url }),
[release.poster, ...release.photos] = Array.from({ length: count }, (_value, index) => [
query.img('a img', { attribute: `src${index}_3x`, origin: channel.url }),
query.img('a img', { attribute: `src${index}_2x`, origin: channel.url }),
query.img('a img', { attribute: `src${index}_1x`, origin: channel.url }),
]);
release.stars = query.count('img[src*="star_full"]') + (query.count('img[src*="star_half"]') * 0.5);
@@ -85,18 +86,18 @@ function scrapeAllT1(scenes, site, accNetworkReleases) {
return scenes.map(({ query }) => {
const release = {};
release.title = query.q('h4 a', 'title') || query.q('h4 a', true);
release.title = query.attribute('h4 a', 'title') || query.content('h4 a');
release.url = query.url('h4 a');
release.date = query.date('.more-info-div', 'MMM D, YYYY');
release.duration = query.dur('.more-info-div');
release.duration = query.duration('.more-info-div');
if (/bts|behind the scenes/i.test(release.title)) release.tags = ['behind the scenes'];
const posterPath = query.q('.img-div img', 'src0_1x') || query.img('img.video_placeholder');
const posterPath = query.attribute('.img-div img', 'src0_1x') || query.img('img.video_placeholder');
if (posterPath) {
const poster = /^http/.test(posterPath) ? posterPath : `${site.parameters?.media || site.url}${posterPath}`;
const poster = unprint.prefixUrl(posterPath, site.parameters?.media || site.url);
release.poster = [
poster.replace('-1x', '-3x'),
@@ -117,37 +118,40 @@ function scrapeAllT1(scenes, site, accNetworkReleases) {
}).filter(Boolean);
}
async function fetchLatest(site, page = 1, include, { uniqueReleases = [], duplicateReleases = [] }) {
const url = (site.parameters?.latest && util.format(site.parameters.latest, page))
async function fetchLatest(site, page = 1, _include, { uniqueReleases = [], duplicateReleases = [] }) {
const url = (site.parameters?.latest && format(site.parameters.latest, { page }))
|| (site.parameters?.t1 && `${site.url}/t1/categories/movies_${page}_d.html`)
|| `${site.url}/categories/movies_${page}_d.html`;
const res = await qu.getAll(url, '.modelfeature, .item-video, .updateItem');
const res = await unprint.get(url, { selectAll: '.modelfeature, .item-video, .updateItem' });
if (!res.ok) {
return res.status;
}
if (site.parameters?.t1) {
return scrapeAllT1(res.items, site, [...uniqueReleases, ...duplicateReleases]);
return scrapeAllT1(res.context, site, [...uniqueReleases, ...duplicateReleases]);
}
return scrapeAll(res.items, site, uniqueReleases);
return scrapeAll(res.context, site, uniqueReleases);
}
function scrapeScene({ html, query }, channel, url) {
const release = { url }; // url used for entry ID
release.title = query.cnt('.videoDetails h3');
release.description = query.cnt('.videoDetails p');
release.title = query.content('.videoDetails h3, .videoDetails h1');
release.description = query.content('.videoDetails p');
release.date = query.date('.videoInfo p', ['MM/DD/YYYY', 'YYYY-MM-DD']);
release.duration = Number(query.cnt('.videoInfo p:nth-of-type(2)')?.match(/(\d+) min/i)?.[1]) * 60;
release.duration = Number(query.content('.videoInfo p:nth-of-type(2)')?.match(/(\d+) min/i)?.[1]) * 60;
release.actors = query.cnts('.update_models a');
release.actors = query.all('.update_models a').map((actorEl) => ({
name: unprint.query.content(actorEl),
url: unprint.query.url(actorEl, null, { origin: channel.origin }),
}));
const posterPath = html.match(/poster="([\w-/.]+)"/)?.[1];
const poster = qu.prefixUrl(posterPath, channel.url) || query.img('.update_thumb', 'src0_1x', { origin: channel.url }); // latter used when trailer requires signup
const poster = unprint.prefixUrl(posterPath, channel.url) || query.img('.update_thumb', 'src0_1x', { origin: channel.url }); // latter used when trailer requires signup
[release.poster, ...release.photos] = [poster, ...query.imgs('.item-thumb img', 'src0_1x', { origin: channel.url })]
.map((src) => src && [
@@ -159,10 +163,10 @@ function scrapeScene({ html, query }, channel, url) {
const trailerPath = html.match(/\/trailers?\/.*.mp4/);
if (trailerPath) {
release.trailer = qu.prefixUrl(trailerPath, channel.parameters?.media || channel.url);
release.trailer = unprint.prefixUrl(trailerPath, channel.parameters?.media || channel.url);
}
release.tags = query.cnts('.featuring a[href*="categories/"]');
release.tags = query.contents('.featuring a[href*="categories/"]');
release.stars = query.count('.stars img[src*="star_full"]') + (query.count('.stars img[src*="star_half"]') * 0.5);
release.entryId = deriveEntryId(release);
@@ -173,29 +177,34 @@ function scrapeScene({ html, query }, channel, url) {
function scrapeSceneT1({ html, query }, site, url, baseRelease) {
const release = { url };
release.title = query.q('.trailer-section-head .section-title', true);
release.title = query.content('.trailer-section-head .section-title');
release.description = query.text('.row .update-info-block');
release.date = query.date('.update-info-row', 'MMM D, YYYY', /\w+ \d{1,2}, \d{4}/);
release.duration = query.dur('.update-info-row:nth-child(2)');
release.duration = query.duration('.update-info-row:nth-child(2)');
release.actors = query.all('.models-list-thumbs a').map((el) => ({
name: query.q(el, 'span', true),
avatar: getImageWithFallbacks(query.q, 'img', site, el),
name: unprint.query.content(el, 'span'),
url: unprint.query.url(el, null),
avatar: getImageWithFallbacks(query, 'img', site, el),
}));
release.tags = query.all('.tags a', true);
release.tags = query.contents('.tags a');
// const posterPath = html.match(/poster="(.*\.jpg)/)?.[1];
const posterPath = query.q('.player-thumb img', 'src0_1x');
const posterPath = query.img('.player-thumb img', { attribute: 'src0_1x' });
const trailer = html.match(/<video.*src="(.*\.mp4)/)?.[1];
[release.poster, release.photos] = extractPoster(posterPath, site, baseRelease);
const trailer = html.match(/<video.*src="(.*\.mp4)/)?.[1];
if (trailer && /^http/.test(trailer)) release.trailer = { src: trailer, referer: url };
else if (trailer) release.trailer = { src: `${site.parameters?.media || site.url}${trailer}`, referer: url };
if (trailer) {
release.trailer = {
src: unprint.prefixUrl(trailer, site.parameters?.media || site.url),
referer: url,
};
}
const stars = query.q('.update-rating', true).match(/\d.\d/)?.[0];
if (stars) release.stars = Number(stars);
release.stars = query.number('.update-rating');
if (site.type === 'network') {
const channelRegExp = new RegExp(site.children.map((channel) => channel.parameters?.match || channel.name).join('|'), 'i');
@@ -206,16 +215,99 @@ function scrapeSceneT1({ html, query }, site, url, baseRelease) {
}
}
// release.entryId = q('.player-thumb img', 'id')?.match(/set-target-(\d+)/)[1];
release.entryId = deriveEntryId(release);
return release;
}
function scrapeProfileT1({ el, query }, site) {
const profile = {};
async function fetchScene(url, site, baseRelease) {
const res = await unprint.get(url);
const bio = query.all('.detail-div + .detail-div p, .detail-div p', true).reduce((acc, info) => {
if (!res.ok) {
return res.status;
}
if (site.parameters?.t1) {
return scrapeSceneT1(res.context, site, url, baseRelease);
}
return scrapeScene(res.context, site, url, baseRelease);
}
async function fetchActorScenes({ query }, channel, accScenes = []) {
const scenes = scrapeAll(unprint.initAll(query.all('.item-video')), channel);
const nextPage = query.url('.next a');
if (nextPage) {
const res = await unprint.get(nextPage);
if (res.ok) {
return fetchActorScenes(res.context, channel, scenes.concat(accScenes));
}
}
return accScenes.concat(scenes);
}
async function scrapeProfile({ query }, url, channel, options) {
const profile = { url };
const bio = query.all('.stats li').reduce((acc, bioEl) => {
const key = unprint.query.content(bioEl, 'strong');
const value = unprint.query.url(bioEl, null) || unprint.query.text(bioEl);
return {
...acc,
[slugify(key, '_')]: value,
};
}, {});
if (bio.date_of_birth) profile.dateOfBirth = unprint.extractDate(bio.date_of_birth, 'MMMM D, YYYY');
if (bio.birthplace) profile.birthPlace = bio.birthplace;
if (bio.fun_fact) profile.description = bio.fun_fact;
if (bio.ethnicity) profile.ethnicity = bio.ethnicity;
if (bio.height) profile.height = convert(bio.height, 'cm');
if (bio.weight) profile.weight = convert(bio.weight.match(/(\d+)\s*lb/i)?.[1], 'lb', 'kg');
if (bio.shoe_size && !/unknown/i.test(bio.shoe_size)) profile.foot = unprint.extractNumber(bio.shoe_size);
profile.measurements = bio.measurements;
if (bio.penis_length) profile.penisLength = Number(bio.penis_length.match(/(\d+)\s*cm/i)?.[1] || convert(bio.penis_length.match(/(\d+\.?\d+)\s*in/i)?.[1], 'cm')) || null;
if (bio.penis_girth) profile.penisGirth = Number(bio.penis_girth.match(/(\d+)\s*cm/i)?.[1] || convert(bio.penis_girth.match(/(\d+\.?\d+)\s*in/i)?.[1], 'cm')) || null;
if (bio.circumcised && /yes/i.test(bio.circumcised)) profile.isCircumcised = true;
if (bio.circumcised && /no/i.test(bio.circumcised)) profile.isCircumcised = false;
if (bio.natural_breasts && /yes/i.test(bio.natural_breasts)) profile.naturalBoobs = true;
if (bio.natural_breasts && /no/i.test(bio.natural_breasts)) profile.naturalBoobs = false;
if (bio.tattoos && /(yes)|(some)|(many)/i.test(bio.tattoos)) profile.hasTattoos = true;
if (bio.tattoos && /no/i.test(bio.tattoos)) profile.hasTattoos = false;
if (bio.piercings && /(yes)|(some)|(many)/i.test(bio.piercings)) profile.hasPiercings = true;
if (bio.piercings && /no/i.test(bio.piercings)) profile.hasPiercings = false;
if (bio.aliases) profile.aliases = bio.aliases.split(',').map((alias) => alias.trim()).filter((alias) => !/known/i.test(alias)); // filter out "No known aliases"
profile.socials = [bio.onlyfans, bio.twitter, bio.instagram, bio.domain].filter(Boolean);
profile.avatar = [
query.img('.profile-pic img', { attribute: 'src0_3x', origin: channel.url }),
query.img('.profile-pic img', { attribute: 'src0_2x', origin: channel.url }),
query.img('.profile-pic img', { attribute: 'src0_1x', origin: channel.url }),
];
if (options.includeActorScenes) {
profile.releases = await fetchActorScenes({ query }, channel);
}
return profile;
}
function scrapeProfileT1({ query }, url, site) {
const profile = { url };
const bio = query.contents('.detail-div + .detail-div p, .detail-div p').reduce((acc, info) => {
const [key, value] = info.split(':');
if (!value) return acc;
@@ -233,125 +325,49 @@ function scrapeProfileT1({ el, query }, site) {
const heightMetric = bio.height?.match(/(\d{3})(\b|c)/);
const heightImperial = bio.height?.match(/\d{1}(\.\d)?/g);
if (heightMetric) profile.height = Number(heightMetric[1]);
if (heightImperial) profile.height = feetInchesToCm(Number(heightImperial[0]), Number(heightImperial[1]));
profile.avatar = getImageWithFallbacks(query.q, '.img-div img', site);
if (heightMetric) {
profile.height = Number(heightMetric[1]);
}
const qReleases = qu.initAll(el, '.item-video');
if (heightImperial) {
profile.height = convert(`${heightImperial[0]}' ${heightImperial[1]}"`, 'cm');
}
profile.avatar = getImageWithFallbacks(query, '.img-div img', site);
const qReleases = unprint.initAll(query.all('.item-video'));
profile.releases = scrapeAllT1(qReleases, site);
return profile;
}
async function fetchScene(url, site, baseRelease) {
const res = await qu.get(url);
if (!res.ok) {
return res.status;
}
if (site.parameters?.t1) {
return scrapeSceneT1(res.item, site, url, baseRelease);
}
return scrapeScene(res.item, site, url, baseRelease);
}
async function fetchActorScenes({ query, el }, channel, accScenes = []) {
const scenes = scrapeAll(qu.initAll(el, '.item-video'), channel);
const nextPage = query.url('.next a');
if (nextPage) {
const res = await qu.get(nextPage);
if (res.ok) {
return fetchActorScenes(res.item, channel, scenes.concat(accScenes));
}
}
return accScenes.concat(scenes);
}
async function scrapeProfile({ query, el }, channel, options) {
const profile = {};
const bio = query.all('.stats li').reduce((acc, bioEl) => {
const key = query.cnt(bioEl, 'strong');
const value = query.url(bioEl) || query.text(bioEl);
return {
...acc,
[slugify(key, '_')]: value,
};
}, {});
if (bio.date_of_birth) profile.dateOfBirth = qu.extractDate(bio.date_of_birth, 'MMMM D, YYYY');
if (bio.birthplace) profile.birthPlace = bio.birthplace;
if (bio.fun_fact) profile.description = bio.fun_fact;
if (bio.ethnicity) profile.ethnicity = bio.ethnicity;
if (bio.height) profile.height = Number(bio.height.match(/^\d{2,3}/)?.[0]);
if (bio.weight) profile.weight = Number(bio.weight.match(/^\d{2,3}/)?.[0]);
if (bio.shoe_size) profile.foot = Number(bio.shoe_size);
profile.measurements = bio.measurements;
if (bio.penis_length) profile.penisLength = Number(bio.penis_length.match(/(\d+)\s*cm/i)?.[1] || inchesToCm(bio.penis_length.match(/(\d+\.?\d+)\s*in/i)?.[1])) || null;
if (bio.penis_girth) profile.penisGirth = Number(bio.penis_girth.match(/(\d+)\s*cm/i)?.[1] || inchesToCm(bio.penis_girth.match(/(\d+\.?\d+)\s*in/i)?.[1])) || null;
if (bio.circumcised && /yes/i.test(bio.circumcised)) profile.isCircumcised = true;
if (bio.circumcised && /no/i.test(bio.circumcised)) profile.isCircumcised = false;
if (bio.natural_breasts && /yes/i.test(bio.natural_breasts)) profile.naturalBoobs = true;
if (bio.natural_breasts && /no/i.test(bio.natural_breasts)) profile.naturalBoobs = false;
if (bio.tattoos && /(yes)|(some)|(many)/i.test(bio.tattoos)) profile.hasTattoos = true;
if (bio.tattoos && /no/i.test(bio.tattoos)) profile.hasTattoos = false;
if (bio.piercings && /(yes)|(some)|(many)/i.test(bio.piercings)) profile.hasPiercings = true;
if (bio.piercings && /no/i.test(bio.piercings)) profile.hasPiercings = false;
if (bio.aliases) profile.aliases = bio.aliases.split(',').map((alias) => alias.trim());
profile.socials = [bio.onlyfans, bio.twitter, bio.instagram, bio.domain].filter(Boolean);
profile.avatar = [
query.img('.profile-pic img', 'src0_3x', { origin: channel.url }),
query.img('.profile-pic img', 'src0_2x', { origin: channel.url }),
query.img('.profile-pic img', 'src0_1x', { origin: channel.url }),
];
if (options.includeActorScenes) {
profile.releases = await fetchActorScenes({ query, el }, channel);
}
return profile;
}
async function fetchProfile({ name: actorName }, { channel }, options) {
const actorSlugA = slugify(actorName, '');
async function fetchProfile({ name: actorName, url: actorUrl }, { channel }, options) {
const actorSlugA = slugify(actorName, '', { lower: false });
const actorSlugB = slugify(actorName);
const t1 = channel.parameters?.t1 ? 't1/' : '';
// follow redirects because a lot of profiles redirect from lowercase to uppercase or vice versa
const res1 = channel.parameters?.profile
? await qu.get(util.format(channel.parameters.profile, actorSlugA))
: await qu.get(`${channel.url}/${t1}models/${actorSlugA}.html`, null, null, { followRedirects: true });
const res = (res1.ok && res1)
|| (channel.parameters?.profile && await qu.get(util.format(channel.parameters.profile, actorSlugB)))
|| await qu.get(`${channel.url}/${t1}models/${actorSlugB}.html`, null, null, { followRedirects: true });
const { res, url } = await tryUrls([
actorUrl,
...channel.parameters?.profile ? [
format(channel.parameters.profile, { actor: actorSlugA }),
format(channel.parameters.profile, { actor: actorSlugB }),
] : [
`${channel.url}/${t1}models/${actorSlugA}.html`,
`${channel.url}/${t1}models/${actorSlugB}.html`,
],
], { followRedirects: false });
if (!res.ok) {
return res.status;
}
if (channel.parameters?.t1) {
return scrapeProfileT1(res.item, channel);
return scrapeProfileT1(res.context, url, channel);
}
return scrapeProfile(res.item, channel, options);
return scrapeProfile(res.context, url, channel, options);
}
module.exports = {

View File

@@ -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,
},
};

View File

@@ -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',

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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,
};

View File

@@ -502,6 +502,7 @@ async function fetchScene(url, channel, baseRelease, options) {
}
const res = await unprint.get(url, {
interface: 'request',
useBrowser: !!options.parameters?.useBrowser,
});

View File

@@ -4,9 +4,10 @@ const logger = require('./logger')(__filename);
const knex = require('./knex');
const { fetchEntityReleaseIds } = require('./entities');
const { updateSceneSearch } = require('./update-search');
const slugify = require('./utils/slugify');
const bulkInsert = require('./utils/bulk-insert');
const batchInsert = require('./utils/batch-insert');
function curateTagMedia(media) {
if (!media) {
@@ -161,7 +162,7 @@ async function associateReleaseTags(releases, type = 'release') {
const tagAssociations = buildReleaseTagAssociations(releases, tagIdsBySlug, entityTagIdsByEntityId, type);
await bulkInsert(`${type}s_tags`, tagAssociations, false);
await batchInsert(`${type}s_tags`, tagAssociations, { conflict: false });
}
async function fetchTag(tagId) {
@@ -199,19 +200,28 @@ 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) {
// TODO: prevent wiping tags if insert fails
await knex('releases_tags')
const trx = await knex.transaction();
await trx('releases_tags')
.whereIn('id', updatedTagEntries.map((tagEntry) => tagEntry.id))
.delete();
await bulkInsert('releases_tags', updatedTagEntries.map((tagEntry) => ({
await batchInsert('releases_tags', updatedTagEntries.map((tagEntry) => ({
...tagEntry,
id: undefined,
})), true);
})), {
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) {

64
src/tools/batch-test.js Normal file
View File

@@ -0,0 +1,64 @@
'use strict';
const knex = require('../knex');
const batchInsert = require('../utils/batch-insert');
async function createTestTable() {
const tableExists = await knex.schema.hasTable('batch_test');
if (tableExists) {
// await knex('batch_test').delete();
return;
}
await knex.schema.createTable('batch_test', (table) => {
table.increments('id');
table.string('name')
.unique();
table.integer('age');
table.text('location');
table.datetime('created_at')
.notNullable()
.defaultTo(knex.fn.now());
});
}
async function init() {
await createTestTable();
const transaction = await knex.transaction();
const entries = await batchInsert('batch_test', [
{
name: 'John',
age: 18,
location: 'Home',
},
{
name: 'Jack',
age: 38,
location: 'Work',
},
{
name: 'James',
age: 35,
location: 'Club',
},
], {
conflict: 'name',
update: true,
transaction,
commit: false,
});
await transaction.commit();
console.log('ENTRIES', entries);
// await knex.schema.dropTable('batch_test');
await knex.destroy();
}
init();

136
src/tools/gamma_banners.js Normal file
View 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
View 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();

View File

@@ -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) => {

View 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();

View File

@@ -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,

View File

@@ -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

107
src/utils/batch-insert.js Executable file
View File

@@ -0,0 +1,107 @@
'use strict';
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,
concurrent = false,
transaction,
commit = false,
} = {}) {
if (!table) {
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');
}
if (items.length === 0) {
return [];
}
// 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 {
const queries = chunks.map((chunkItems) => {
const query = trx(table)
.insert(chunkItems)
.returning('*');
if (conflicts.length > 0) {
if (Array.isArray(update)) {
// udpate specified
return query
.onConflict(conflicts)
.merge(update);
}
if (update) {
// update all
return query
.onConflict(conflicts)
.merge();
}
}
// error on any conflict
if (conflict) {
return query;
}
// ignore duplicates, keep old entries as-is
return query
.onConflict()
.ignore();
});
const results = concurrent
? await Promise.all(queries)
: await queries.reduce(async (chain, query) => {
const acc = await chain;
const result = await query;
return acc.concat(result);
}, Promise.resolve([]));
if (!transaction || commit) {
await trx.commit();
}
return results;
} catch (error) {
if (!transaction || commit) {
await trx.rollback();
}
logger.error(`Failed batch insert: ${error.message} (${error.detail})`);
throw error;
}
}
module.exports = batchInsert;

View File

@@ -76,6 +76,8 @@ const actors = [
{ entity: 'seehimfuck', name: 'Sheem The Dream', fields: ['avatar', 'description', 'dateOfBirth', 'birthPlace', 'ethnicity', 'height', 'weight', 'hasTattoos', 'hasPiercings', 'penisLength', 'isCircumcised', 'socials'] },
{ entity: 'hushpass', name: 'Dylan Ryder', fields: ['avatar'] },
{ entity: 'interracialpass', name: 'Aidra Fox', fields: ['avatar', 'height', 'measurements'] },
{ entity: 'ravebunnys', name: 'Lacey Jayne', fields: ['avatar', 'height', 'measurements', 'dateOfBirth', 'birthPlace', 'description', 'ethnicity', 'weight', 'naturalBoobs'] },
{ entity: 'hotandtatted', name: 'Valerica Steele', url: 'https://hotandtatted.com/models/tattooed-pornstar-val-steele.html', fields: ['avatar', 'measurements', 'dateOfBirth', 'birthPlace', 'description', 'ethnicity', 'weight', 'foot', 'naturalBoobs', 'hasPiercings'] },
// kelly madison / 8K
{ entity: 'kellymadison', name: 'Ava Addams', fields: ['avatar', 'description', 'age', 'height', 'measurements', 'birthPlace', 'dateOfBirth', 'ethnicity'] },
{ entity: '8kmembers', name: 'Angie Lynx', fields: ['age', 'height', 'measurements', 'birthPlace', 'dateOfBirth', 'ethnicity'] },
@@ -88,7 +90,7 @@ const actors = [
{ entity: 'letsdoeit', name: 'Nicole Doshi', fields: ['avatar', 'description', 'gender', 'height', 'measurements', 'birthPlace', 'dateOfBirth'] },
{ entity: 'killergram', name: 'Clea Gaultier', fields: ['avatar', 'gender', 'hairColor', 'ethnicity'] },
{ entity: 'men', name: 'Cade Maddox', fields: ['avatar', 'description', 'gender', 'height', 'ethnicity', 'penisLength', 'dateOfBirth', 'weight', 'hairColor', 'hasTattoos'] },
{ entity: 'metrohd', name: 'April Olsen', fields: ['avatar', 'description', 'gender', 'birthPlace', 'height', 'measurements', 'dateOfBirth', 'weight'] },
{ entity: 'metrohd', name: 'Vanna Bardot', fields: ['avatar', 'description', 'gender', 'birthPlace', 'height', 'measurements', 'dateOfBirth', 'weight', 'hairColor', 'ethnicity', 'hasTattoos'] },
{ entity: 'mofos', name: 'Ariana Starr', fields: ['avatar', 'description', 'gender', 'birthPlace', 'height', 'measurements', 'dateOfBirth'] },
{ entity: 'propertysex', name: 'Desiree Dulce', fields: ['avatar', 'description', 'gender', 'birthPlace', 'height', 'measurements', 'dateOfBirth', 'weight', 'hairColor', 'ethnicity', 'hasPiercings'] },
{ entity: 'sexyhub', name: 'Angie Lynx', fields: ['avatar', 'description', 'gender', 'birthPlace', 'height', 'measurements', 'dateOfBirth'] },
@@ -117,7 +119,7 @@ const actors = [
{ entity: 'devilsfilm', name: 'Katrina Colt', fields: ['avatar', 'gender'] },
{ entity: 'diabolic', name: 'Kira Noir', fields: ['avatar', 'gender'] },
{ entity: 'evilangel', name: 'Francesca Le', fields: ['avatar', 'gender'] },
{ entity: 'fantasymassage', name: 'Cherry Kiss', fields: ['avatar', 'gender', 'description', 'eyes', 'hairColor'] },
{ entity: 'fantasymassage', name: 'Cherry Kiss', fields: ['avatar', 'gender'] },
{ entity: 'filthykings', name: 'Armani Black', fields: ['avatar', 'gender'] },
{ entity: 'gangbangcreampie', name: 'Luna Lovely', fields: ['avatar', 'gender', 'description'] },
{ entity: 'girlsway', name: 'Adriana Chechik', fields: ['avatar', 'gender', 'description', 'eyes', 'hairColor'] },
@@ -143,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'] },
{ 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'] },
@@ -151,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'] },
@@ -308,9 +311,10 @@ const validators = {
hasTattoos: (value) => typeof value === 'boolean',
hasPiercings: (value) => typeof value === 'boolean',
avatar: async (value) => [].concat(value).reduce(async (chain, url) => {
// testing all avatar fallbacks is too time-consuming, just ensure one is valid
const acc = await chain;
if (!acc) {
if (acc) {
return acc;
}
@@ -329,7 +333,7 @@ const validators = {
// profiler in this context is shorthand for profile scraper
async function init() {
const entitiesBySlug = await fetchEntitiesBySlug(Object.keys(actorScrapers), { types: ['channel', 'network', 'info'], prefer: 'channel' });
const entitiesBySlug = await fetchEntitiesBySlug(Object.keys(actorScrapers), { types: ['channel', 'network', 'info'], prefer: 'options' });
await Object.entries(actorScrapers).reduce(async (chain, [entitySlug, scraper]) => {
await chain;
@@ -339,7 +343,6 @@ async function init() {
const tests = actors.filter((actor) => actor.entity === entitySlug);
// TODO: remove when all tests are written
if (tests.length === 0) {
console.log('TODO', entitySlug);
return;