Added Snow Valley (Sperm Mania) scraper.
This commit is contained in:
		
							parent
							
								
									91e31e8ce7
								
							
						
					
					
						commit
						1950dd2e62
					
				|  | @ -221,6 +221,7 @@ module.exports = { | |||
| 		'vrcosplayx', | ||||
| 		'teamskeet', | ||||
| 		'mylf', | ||||
| 		'spermmania', | ||||
| 		[ | ||||
| 			'letsdoeit', | ||||
| 			'mamacitaz', | ||||
|  |  | |||
|  | @ -0,0 +1,35 @@ | |||
| exports.up = async (knex) => { | ||||
| 	await knex.schema.alterTable('actors', (table) => { | ||||
| 		table.integer('leg'); | ||||
| 		table.integer('foot'); | ||||
| 		table.integer('thigh'); | ||||
| 	}); | ||||
| 
 | ||||
| 	await knex.schema.alterTable('actors_profiles', (table) => { | ||||
| 		table.integer('leg'); | ||||
| 		table.integer('foot'); | ||||
| 		table.integer('thigh'); | ||||
| 	}); | ||||
| 
 | ||||
| 	await knex.schema.alterTable('releases', (table) => { | ||||
| 		table.integer('video_count'); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| exports.down = async (knex) => { | ||||
| 	await knex.schema.alterTable('actors', (table) => { | ||||
| 		table.dropColumn('leg'); | ||||
| 		table.dropColumn('foot'); | ||||
| 		table.dropColumn('thigh'); | ||||
| 	}); | ||||
| 
 | ||||
| 	await knex.schema.alterTable('actors_profiles', (table) => { | ||||
| 		table.dropColumn('leg'); | ||||
| 		table.dropColumn('foot'); | ||||
| 		table.dropColumn('thigh'); | ||||
| 	}); | ||||
| 
 | ||||
| 	await knex.schema.alterTable('releases', (table) => { | ||||
| 		table.dropColumn('video_count'); | ||||
| 	}); | ||||
| }; | ||||
|  | @ -53,6 +53,7 @@ | |||
|                 "graphile-utils": "^4.14.0", | ||||
|                 "graphql": "^15.8.0", | ||||
|                 "html-entities": "^2.4.0", | ||||
|                 "https-proxy-agent": "^7.0.5", | ||||
|                 "iconv-lite": "^0.6.3", | ||||
|                 "inquirer": "^8.2.6", | ||||
|                 "inspector-api": "^1.4.8", | ||||
|  | @ -88,7 +89,7 @@ | |||
|                 "tunnel": "0.0.6", | ||||
|                 "ua-parser-js": "^1.0.37", | ||||
|                 "undici": "^5.28.1", | ||||
|                 "unprint": "^0.11.9", | ||||
|                 "unprint": "^0.11.13", | ||||
|                 "url-pattern": "^1.0.3", | ||||
|                 "v-tooltip": "^2.1.3", | ||||
|                 "video.js": "^8.6.1", | ||||
|  | @ -3861,6 +3862,18 @@ | |||
|                 "node-pre-gyp": "bin/node-pre-gyp" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { | ||||
|             "version": "5.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", | ||||
|             "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", | ||||
|             "dependencies": { | ||||
|                 "agent-base": "6", | ||||
|                 "debug": "4" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 6" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@mapbox/node-pre-gyp/node_modules/lru-cache": { | ||||
|             "version": "6.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|  | @ -10868,15 +10881,26 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/https-proxy-agent": { | ||||
|             "version": "5.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", | ||||
|             "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", | ||||
|             "version": "7.0.5", | ||||
|             "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", | ||||
|             "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", | ||||
|             "dependencies": { | ||||
|                 "agent-base": "6", | ||||
|                 "agent-base": "^7.0.2", | ||||
|                 "debug": "4" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 6" | ||||
|                 "node": ">= 14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/https-proxy-agent/node_modules/agent-base": { | ||||
|             "version": "7.1.1", | ||||
|             "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", | ||||
|             "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", | ||||
|             "dependencies": { | ||||
|                 "debug": "^4.3.4" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/human-signals": { | ||||
|  | @ -12100,18 +12124,6 @@ | |||
|                 "node": ">= 14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/jsdom/node_modules/https-proxy-agent": { | ||||
|             "version": "7.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", | ||||
|             "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", | ||||
|             "dependencies": { | ||||
|                 "agent-base": "^7.0.2", | ||||
|                 "debug": "4" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/jsdom/node_modules/whatwg-mimetype": { | ||||
|             "version": "4.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", | ||||
|  | @ -12806,6 +12818,19 @@ | |||
|                 "node": "^12.13.0 || ^14.15.0 || >=16.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/make-fetch-happen/node_modules/https-proxy-agent": { | ||||
|             "version": "5.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", | ||||
|             "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "agent-base": "6", | ||||
|                 "debug": "4" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 6" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/make-fetch-happen/node_modules/lru-cache": { | ||||
|             "version": "7.18.3", | ||||
|             "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", | ||||
|  | @ -13613,6 +13638,19 @@ | |||
|                 "node": ">= 6" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/node-gyp/node_modules/https-proxy-agent": { | ||||
|             "version": "5.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", | ||||
|             "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", | ||||
|             "dev": true, | ||||
|             "dependencies": { | ||||
|                 "agent-base": "6", | ||||
|                 "debug": "4" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 6" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/node-gyp/node_modules/lru-cache": { | ||||
|             "version": "6.0.0", | ||||
|             "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", | ||||
|  | @ -14411,18 +14449,6 @@ | |||
|                 "node": ">= 14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { | ||||
|             "version": "7.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", | ||||
|             "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", | ||||
|             "dependencies": { | ||||
|                 "agent-base": "^7.0.2", | ||||
|                 "debug": "4" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/pac-proxy-agent/node_modules/socks-proxy-agent": { | ||||
|             "version": "8.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", | ||||
|  | @ -15354,18 +15380,6 @@ | |||
|                 "node": ">= 14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/proxy-agent/node_modules/https-proxy-agent": { | ||||
|             "version": "7.0.2", | ||||
|             "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", | ||||
|             "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", | ||||
|             "dependencies": { | ||||
|                 "agent-base": "^7.0.2", | ||||
|                 "debug": "4" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 14" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/proxy-agent/node_modules/lru-cache": { | ||||
|             "version": "7.18.3", | ||||
|             "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", | ||||
|  | @ -18298,9 +18312,9 @@ | |||
|             } | ||||
|         }, | ||||
|         "node_modules/unprint": { | ||||
|             "version": "0.11.9", | ||||
|             "resolved": "https://registry.npmjs.org/unprint/-/unprint-0.11.9.tgz", | ||||
|             "integrity": "sha512-ROb7d1o4w0ATTgMW970/z3xURbslfc2D/AmYTzT5RoXsaSbQZTXa5lSCQ/iZGGyzrTX1UGVqot0+AQIYf2c3IQ==", | ||||
|             "version": "0.11.13", | ||||
|             "resolved": "https://registry.npmjs.org/unprint/-/unprint-0.11.13.tgz", | ||||
|             "integrity": "sha512-dEa3zdaXtK2TmRVWf4APunTUXZfnYb0Yv4RlddpFVA8fgYf0ER/m0JN/ZcbEfqg3x5YPiJEHpgLGH9pMv5lbqA==", | ||||
|             "dependencies": { | ||||
|                 "axios": "^0.27.2", | ||||
|                 "bottleneck": "^2.19.5", | ||||
|  | @ -18424,6 +18438,18 @@ | |||
|                 "node": ">= 6" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/unprint/node_modules/https-proxy-agent": { | ||||
|             "version": "5.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", | ||||
|             "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", | ||||
|             "dependencies": { | ||||
|                 "agent-base": "6", | ||||
|                 "debug": "4" | ||||
|             }, | ||||
|             "engines": { | ||||
|                 "node": ">= 6" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/unprint/node_modules/iconv-lite": { | ||||
|             "version": "0.4.24", | ||||
|             "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", | ||||
|  |  | |||
|  | @ -112,6 +112,7 @@ | |||
|         "graphile-utils": "^4.14.0", | ||||
|         "graphql": "^15.8.0", | ||||
|         "html-entities": "^2.4.0", | ||||
|         "https-proxy-agent": "^7.0.5", | ||||
|         "iconv-lite": "^0.6.3", | ||||
|         "inquirer": "^8.2.6", | ||||
|         "inspector-api": "^1.4.8", | ||||
|  | @ -147,7 +148,7 @@ | |||
|         "tunnel": "0.0.6", | ||||
|         "ua-parser-js": "^1.0.37", | ||||
|         "undici": "^5.28.1", | ||||
|         "unprint": "^0.11.9", | ||||
|         "unprint": "^0.11.13", | ||||
|         "url-pattern": "^1.0.3", | ||||
|         "v-tooltip": "^2.1.3", | ||||
|         "video.js": "^8.6.1", | ||||
|  |  | |||
|  | @ -328,6 +328,11 @@ const tags = [ | |||
| 		name: 'corporal punishment', | ||||
| 		slug: 'corporal-punishment', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'cosplay', | ||||
| 		slug: 'cosplay', | ||||
| 		group: 'roleplay', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'couples', | ||||
| 		slug: 'couples', | ||||
|  | @ -355,6 +360,14 @@ const tags = [ | |||
| 		name: 'cum licking', | ||||
| 		slug: 'cum-licking', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'cum fetish', | ||||
| 		slug: 'cum-fetish', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'cum play', | ||||
| 		slug: 'cum-play', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'cum on butt', | ||||
| 		slug: 'cum-on-butt', | ||||
|  | @ -825,7 +838,12 @@ const tags = [ | |||
| 	{ | ||||
| 		name: 'cum in mouth', | ||||
| 		slug: 'cum-in-mouth', | ||||
| 		description: 'A guy ejaculating in someone\'s mouth. If they keep their lips wrapped around his cock, it is an [oral creampie](/tag/oral-creampie). They may not be able to resist [swallowing](/tag/swallowing) the cum.', | ||||
| 		description: 'A cock ejaculating in your mouth. If you keep your lips wrapped around the cock, it is an [oral creampie](/tag/oral-creampie). You may not be able to resist [swallowing](/tag/swallowing) the cum.', | ||||
| 		group: 'finish', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'cum in panty', | ||||
| 		slug: 'cum-in-panty', | ||||
| 		group: 'finish', | ||||
| 	}, | ||||
| 	{ | ||||
|  | @ -996,6 +1014,12 @@ const tags = [ | |||
| 	{ | ||||
| 		name: 'solo', | ||||
| 		slug: 'solo', | ||||
| 		description: 'You don\'t need a man... or a woman! No one does it better than yourself.', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'solo foreplay', | ||||
| 		slug: 'solo-foreplay', | ||||
| 		description: 'Getting yourself all nice and wet before a good fucking.', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'skinny', | ||||
|  | @ -1289,6 +1313,18 @@ const tags = [ | |||
| 		slug: 'scripts', | ||||
| 		description: 'Scripts for haptic sex toys.', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'cat ears', | ||||
| 		slug: 'cat-ears', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'neko', | ||||
| 		slug: 'neko', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'ahegao', | ||||
| 		slug: 'ahegao', | ||||
| 	}, | ||||
| ]; | ||||
| 
 | ||||
| const aliases = [ | ||||
|  | @ -1411,6 +1447,10 @@ const aliases = [ | |||
| 		name: 'bald pussy', | ||||
| 		for: 'shaved', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'hairless pussy', | ||||
| 		for: 'shaved', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'ball gag', | ||||
| 		for: 'gag', | ||||
|  | @ -2313,6 +2353,10 @@ const aliases = [ | |||
| 		for: 'titty-fucking', | ||||
| 		secondary: true, | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'titjob', | ||||
| 		for: 'titty-fucking', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'tp', | ||||
| 		for: 'triple-penetration', | ||||
|  | @ -2639,6 +2683,14 @@ const aliases = [ | |||
| 		name: 'sex toy scripts', | ||||
| 		for: 'scripts', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'mouth cumshot', | ||||
| 		for: 'cum-in-mouth', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'oral cumshot', | ||||
| 		for: 'cum-in-mouth', | ||||
| 	}, | ||||
| ]; | ||||
| 
 | ||||
| const priorities = [ // higher index is higher priority
 | ||||
|  |  | |||
|  | @ -653,6 +653,11 @@ const networks = [ | |||
| 			parentSession: false, | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		slug: 'snowvalley', | ||||
| 		name: 'Snow Valley Group', | ||||
| 		hasLogo: false, | ||||
| 	}, | ||||
| 	{ | ||||
| 		slug: 'spizoo', | ||||
| 		name: 'Spizoo', | ||||
|  |  | |||
|  | @ -10720,6 +10720,121 @@ const sites = [ | |||
| 		tags: ['lesbian'], | ||||
| 		parent: 'sexyhub', | ||||
| 	}, | ||||
| 	// SNOW VALLEY GROUP
 | ||||
| 	{ | ||||
| 		slug: 'spermmania', | ||||
| 		name: 'Sperm Mania', | ||||
| 		url: 'https://www.spermmania.com', | ||||
| 		tags: ['cum-fetish'], | ||||
| 		independent: true, | ||||
| 		parent: 'snowvalley', | ||||
| 	}, | ||||
| 	{ | ||||
| 		slug: 'cospuri', | ||||
| 		name: 'Cospuri', | ||||
| 		url: 'https://www.cospuri.com', | ||||
| 		tags: ['cosplay'], | ||||
| 		independent: true, | ||||
| 		parent: 'snowvalley', | ||||
| 		parameters: { | ||||
| 			layout: 'cospuri', | ||||
| 			actors: 'https://www.cospuri.com/model', | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		slug: 'cutebutts', | ||||
| 		name: 'Cute Butts', | ||||
| 		url: 'https://www.cutebutts.com', | ||||
| 		independent: true, | ||||
| 		parent: 'snowvalley', | ||||
| 		parameters: { | ||||
| 			layout: 'cospuri', | ||||
| 			actors: 'https://www.cutebutts.com/model', | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		slug: 'fellatiojapan', | ||||
| 		name: 'Fellatio Japan', | ||||
| 		url: 'https://www.fellatiojapan.com', | ||||
| 		tags: ['blowjob', 'jav'], | ||||
| 		independent: true, | ||||
| 		parent: 'snowvalley', | ||||
| 		parameters: { | ||||
| 			layout: 'fellatio', | ||||
| 			actors: 'https://www.fellatiojapan.com/en/girl', | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		slug: 'handjobjapan', | ||||
| 		name: 'Handjob Japan', | ||||
| 		url: 'https://www.handjobjapan.com', | ||||
| 		tags: ['handjob', 'jav'], | ||||
| 		independent: true, | ||||
| 		parent: 'snowvalley', | ||||
| 		parameters: { | ||||
| 			layout: 'handjob', | ||||
| 			actors: 'https://www.handjobjapan.com/en/models', | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		slug: 'legsjapan', | ||||
| 		name: 'Legs Japan', | ||||
| 		url: 'https://www.legsjapan.com', | ||||
| 		tags: ['jav'], | ||||
| 		independent: true, | ||||
| 		parent: 'snowvalley', | ||||
| 		parameters: { | ||||
| 			layout: 'legs', | ||||
| 			actors: 'https://www.legsjapan.com/en/girl', | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		slug: 'uralesbian', | ||||
| 		name: 'Ura Lesbian', | ||||
| 		url: 'https://www.uralesbian.com', | ||||
| 		tags: ['lesbian', 'jav'], | ||||
| 		independent: true, | ||||
| 		parent: 'snowvalley', | ||||
| 		parameters: { | ||||
| 			layout: 'lesbian', | ||||
| 			actors: 'https://www.uralesbian.com/en/model', | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		slug: 'tokyofacefuck', | ||||
| 		name: 'Tokyo Facefuck', | ||||
| 		url: 'https://www.tokyofacefuck.com', | ||||
| 		tags: ['facefucking', 'blowjob', 'jav'], | ||||
| 		independent: true, | ||||
| 		parent: 'snowvalley', | ||||
| 		parameters: { | ||||
| 			layout: 'facefuck', | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		slug: 'cumbuffet', | ||||
| 		name: 'Cum Buffet', | ||||
| 		url: 'https://www.cumbuffet.com', | ||||
| 		tags: ['swallowing'], | ||||
| 		independent: true, | ||||
| 		parent: 'snowvalley', | ||||
| 		parameters: { | ||||
| 			layout: 'buffet', | ||||
| 			actors: 'https://www.cumbuffet.com/girl', | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		slug: 'transexjapan', | ||||
| 		name: 'Transex Japan', | ||||
| 		url: 'https://www.transexjapan.com', | ||||
| 		tags: ['transsexual', 'jav'], | ||||
| 		independent: true, | ||||
| 		parent: 'snowvalley', | ||||
| 		parameters: { | ||||
| 			layout: 'trans', | ||||
| 			actors: 'https://www.transexjapan.com/model', | ||||
| 		}, | ||||
| 	}, | ||||
| 	// SPIZOO
 | ||||
| 	{ | ||||
| 		slug: 'spizoo', | ||||
|  | @ -10734,6 +10849,12 @@ const sites = [ | |||
| 		tags: ['stripper'], | ||||
| 		parent: 'spizoo', | ||||
| 	}, | ||||
| 	{ | ||||
| 		slug: 'creamher', | ||||
| 		name: 'Goth Girlfriends', | ||||
| 		url: 'https://www.creamher.com', | ||||
| 		parent: 'spizoo', | ||||
| 	}, | ||||
| 	{ | ||||
| 		slug: 'gothgirlfriends', | ||||
| 		name: 'Goth Girlfriends', | ||||
|  |  | |||
|  | @ -267,6 +267,9 @@ function curateActor(actor, withDetails = false, isProfile = false) { | |||
| 			bust: actor.bust, | ||||
| 			waist: actor.waist, | ||||
| 			hip: actor.hip, | ||||
| 			foot: actor.foot, | ||||
| 			leg: actor.leg, | ||||
| 			thigh: actor.thigh, | ||||
| 			naturalBoobs: actor.natural_boobs, | ||||
| 			penisLength: actor.penis_length, | ||||
| 			penisGirth: actor.penis_girth, | ||||
|  | @ -359,6 +362,9 @@ function curateProfileEntry(profile) { | |||
| 		cup: profile.cup, | ||||
| 		bust: profile.bust, | ||||
| 		waist: profile.waist, | ||||
| 		leg: profile.leg, | ||||
| 		thigh: profile.thigh, | ||||
| 		foot: profile.foot, | ||||
| 		hip: profile.hip, | ||||
| 		penis_length: profile.penisLength, | ||||
| 		penis_girth: profile.penisGirth, | ||||
|  | @ -442,8 +448,13 @@ async function curateProfile(profile, actor) { | |||
| 		curatedProfile.waist = Number(profile.waist) || profile.waist?.match?.(/\d+/)?.[0] || null; | ||||
| 		curatedProfile.hip = Number(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; | ||||
| 
 | ||||
| 		// combined measurement value
 | ||||
| 		const measurements = profile.measurements?.match(/(\d+)(\w+)(?:\s*[-x]\s*(\d+)\s*[-x]\s*(\d+))?/); // ExCoGi uses x, Jules Jordan has spaces between the dashes
 | ||||
| 		// 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; | ||||
|  | @ -589,6 +600,9 @@ async function interpolateProfiles(actorIdsOrNames) { | |||
| 			'bust', | ||||
| 			'waist', | ||||
| 			'hip', | ||||
| 			'leg', | ||||
| 			'thigh', | ||||
| 			'foot', | ||||
| 			'shoe_size', | ||||
| 			'penis_length', | ||||
| 			'penis_girth', | ||||
|  |  | |||
|  | @ -130,7 +130,7 @@ async function scrapeRelease(baseRelease, entitiesByHostname, type = 'scene') { | |||
| 		return baseRelease; | ||||
| 	} | ||||
| 
 | ||||
| 	if ((!baseRelease.url && !baseRelease.path) || !argv.deep) { | ||||
| 	if ((!baseRelease.url && !baseRelease.path && !baseRelease.forceDeep) || !argv.deep) { | ||||
| 		return { | ||||
| 			...baseRelease, | ||||
| 			entity, | ||||
|  |  | |||
|  | @ -132,6 +132,7 @@ function toBaseSource(rawSource) { | |||
| 		if (rawSource.extract) baseSource.extract = rawSource.extract; | ||||
| 
 | ||||
| 		if (rawSource.expectType) baseSource.expectType = rawSource.expectType; | ||||
| 		if (typeof rawSource.followRedirects === 'boolean') baseSource.followRedirects = rawSource.followRedirects; | ||||
| 
 | ||||
| 		if (rawSource.stream) { | ||||
| 			baseSource.src = rawSource.stream; | ||||
|  | @ -623,6 +624,7 @@ async function fetchHttpSource(source, tempFileTarget, hashStream) { | |||
| 			...(source.host && { host: source.host }), | ||||
| 		}, | ||||
| 		stream: true, // sources are fetched in parallel, don't gobble up memory
 | ||||
| 		followRedirects: source.followRedirects, | ||||
| 		transforms: [hashStream], | ||||
| 		destination: tempFileTarget, | ||||
| 		...(source.interval && { interval: source.interval }), | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ function getEntryId(html) { | |||
| function getEntryIdFromTitle(release) { | ||||
| 	// return slugify([release.title, release.date && unprint.formatDate(release.date, 'YYYY-MM-DD')]); // date not shown on updates page
 | ||||
| 	// return slugify(release.title);
 | ||||
| 	return slugify([release.title, ...(release.actors?.map((actor) => actor.name).toSorted() || [])]); | ||||
| 	return slugify([release.title, ...(release.actors?.map((actor) => actor.name || actor).toSorted() || [])]); | ||||
| } | ||||
| 
 | ||||
| function scrapeAll(scenes, site, entryIdFromTitle) { | ||||
|  | @ -226,13 +226,13 @@ async function scrapeScene({ html, query }, context) { | |||
| 		}))); | ||||
| 	} | ||||
| 
 | ||||
| 	if (query.exists('.update_dvds a')) { | ||||
| 	if (query.exists('.player-scene-description a[href*="/dvd"]')) { | ||||
| 		release.movie = { | ||||
| 			url: query.url('.update_dvds a'), | ||||
| 			title: query.cnt('.update_dvds a'), | ||||
| 			url: query.url('.player-scene-description a[href*="/dvd"]'), | ||||
| 			title: query.content('.player-scene-description a[href*="/dvd"]'), | ||||
| 		}; | ||||
| 
 | ||||
| 		release.movie.entryId = new URL(release.movie.url).pathname.split('/').slice(-1)[0]?.replace('.html', ''); | ||||
| 		release.movie.entryId = new URL(release.movie.url).pathname.split('/').slice(-1)[0]?.replace('.html', '').toLowerCase(); | ||||
| 	} | ||||
| 
 | ||||
| 	release.stars = query.number('.avg_rating'); | ||||
|  | @ -244,28 +244,40 @@ async function scrapeScene({ html, query }, context) { | |||
| 	return release; | ||||
| } | ||||
| 
 | ||||
| function scrapeMovie({ el, query }, url, site) { | ||||
| 	const movie = { url, site }; | ||||
| function scrapeMovie({ query }, { url }) { | ||||
| 	const movie = {}; | ||||
| 
 | ||||
| 	movie.entryId = new URL(url).pathname.split('/').slice(-1)[0]?.replace('.html', '').toLowerCase(); | ||||
| 	movie.title = query.cnt('.title_bar span'); | ||||
| 	movie.covers = query.urls('#dvd-cover-flip > a'); | ||||
| 	movie.channel = slugify(query.q('.update_date a', true), ''); | ||||
| 	movie.title = query.attribute('meta[property="og:title"]', 'content'); | ||||
| 
 | ||||
| 	// movie.releases = Array.from(document.querySelectorAll('.cell.dvd_info > a'), el => el.href);
 | ||||
| 	const sceneQus = qu.initAll(el, '.dvd_details'); | ||||
| 	const scenes = scrapeAll(sceneQus, site); | ||||
| 	movie.covers = [query.img('img.dvd_box')]; // -2x etc is likely upscaled
 | ||||
| 
 | ||||
| 	const curatedScenes = scenes | ||||
| 		?.map((scene) => ({ ...scene, movie })) | ||||
| 		.sort((sceneA, sceneB) => sceneA.date - sceneB.date); | ||||
| 	const sceneTitles = query.contents('.title-heading-content-black-dvd'); | ||||
| 
 | ||||
| 	movie.date = curatedScenes?.[0]?.date; | ||||
| 	const scenes = query.all('.grid-container-scene').map((sceneEl, index) => { | ||||
| 		const scene = {}; | ||||
| 
 | ||||
| 	return { | ||||
| 		...movie, | ||||
| 		...(curatedScenes && { scenes: curatedScenes }), | ||||
| 	}; | ||||
| 		scene.url = unprint.query.url(sceneEl, 'a[href*="/scenes"]'); | ||||
| 		scene.title = sceneTitles[index]; | ||||
| 
 | ||||
| 		scene.date = unprint.query.date(sceneEl, '//span[contains(@class, "dvd-scene-description") and span[contains(text(), "Date")]]', 'MM/DD/YYYY'); | ||||
| 		scene.actors = unprint.query.contents(sceneEl, '.update_models a'); | ||||
| 
 | ||||
| 		scene.entryId = getEntryIdFromTitle(scene); | ||||
| 
 | ||||
| 		console.log(scene); | ||||
| 
 | ||||
| 		return scene; | ||||
| 	}); | ||||
| 
 | ||||
| 	movie.scenes = scenes?.sort((sceneA, sceneB) => sceneA.date - sceneB.date); | ||||
| 
 | ||||
| 	movie.date = movie.scenes?.[0]?.date; | ||||
| 	movie.datePrecision = 'month'; | ||||
| 
 | ||||
| 	console.log('jj movie', movie); | ||||
| 
 | ||||
| 	return movie; | ||||
| } | ||||
| 
 | ||||
| function scrapeProfile({ query }, url, name, entity) { | ||||
|  | @ -325,12 +337,6 @@ async function fetchUpcoming(site) { | |||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| async function fetchMovie(url, site) { | ||||
| 	const res = await qu.get(url); | ||||
| 
 | ||||
| 	return res.ok ? scrapeMovie(res.item, url, site) : res.status; | ||||
| } | ||||
| 
 | ||||
| async function fetchProfile({ name: actorName, url }, entity) { | ||||
| 	const actorSlugA = slugify(actorName, ''); | ||||
| 	const actorSlugB = slugify(actorName, '-'); | ||||
|  | @ -364,8 +370,8 @@ async function fetchProfile({ name: actorName, url }, entity) { | |||
| 
 | ||||
| module.exports = { | ||||
| 	fetchLatest, | ||||
| 	fetchMovie, | ||||
| 	fetchProfile, | ||||
| 	fetchUpcoming, | ||||
| 	scrapeScene, | ||||
| 	scrapeMovie, | ||||
| }; | ||||
|  |  | |||
|  | @ -63,6 +63,7 @@ const radical = require('./radical'); | |||
| const rickysroom = require('./rickysroom'); | ||||
| const sexlikereal = require('./sexlikereal'); | ||||
| const score = require('./score'); | ||||
| const snowvalley = require('./snowvalley'); | ||||
| const spizoo = require('./spizoo'); | ||||
| const teamskeet = require('./teamskeet'); | ||||
| const teencoreclub = require('./teencoreclub'); | ||||
|  | @ -163,6 +164,7 @@ const scrapers = { | |||
| 		score, | ||||
| 		sexlikereal, | ||||
| 		sexyhub: aylo, | ||||
| 		snowvalley, | ||||
| 		spizoo, | ||||
| 		swallowsalon: julesjordan, | ||||
| 		theflourish: archangel, | ||||
|  | @ -309,6 +311,15 @@ const scrapers = { | |||
| 		sexyhub: aylo, | ||||
| 		silverstonedvd: famedigital, | ||||
| 		silviasaint: famedigital, | ||||
| 		spermmania: snowvalley, | ||||
| 		handjobjapan: snowvalley, | ||||
| 		fellatiojapan: snowvalley, | ||||
| 		legsjapan: snowvalley, | ||||
| 		cumbuffet: snowvalley, | ||||
| 		cospuri: snowvalley, | ||||
| 		cutebutts: snowvalley, | ||||
| 		transexjapan: snowvalley, | ||||
| 		uralesbian: snowvalley, | ||||
| 		spizoo, | ||||
| 		swallowed: mikeadriano, | ||||
| 		milfcandy: archangel, | ||||
|  |  | |||
|  | @ -0,0 +1,867 @@ | |||
| 'use strict'; | ||||
| 
 | ||||
| const unprint = require('unprint'); | ||||
| 
 | ||||
| const slugify = require('../utils/slugify'); | ||||
| 
 | ||||
| const tagsMap = { | ||||
| 	'body bukkake': ['bukkake'], | ||||
| 	'creampie gangbang': ['gangbang', 'creampie'], | ||||
| 	'cum handjob': ['handjob'], | ||||
| 	'facial bukkake': ['facial', 'bukkake'], | ||||
| 	'massive creampie': ['creampie'], | ||||
| 	'massive cum handjob': ['handjob'], | ||||
| 	'panty cum': ['cum-in-panty'], | ||||
| 	'pussy bukkake': ['cum-on-pussy'], | ||||
| }; | ||||
| 
 | ||||
| function entryIdFromMedia(release) { | ||||
| 	return [release.poster, release.trailer, ...(release.photos || [])].flat().filter(Boolean)[0]?.match(/(?:(?:preview)|(?:samples)|(?:tour))\/(.*)\//)?.[1].toLowerCase(); | ||||
| } | ||||
| 
 | ||||
| function scrapeAll(scenes, tilesByEntryId, channel) { | ||||
| 	return scenes.map(({ query }) => { | ||||
| 		const release = {}; | ||||
| 
 | ||||
| 		// release.url = query.url('.title a');
 | ||||
| 
 | ||||
| 		release.title = query.content('.sample-title'); | ||||
| 
 | ||||
| 		// release.date = query.date('.date', 'MMM DD, YYYY');
 | ||||
| 		release.duration = query.duration('//div[contains(text(), "Runtime")]'); | ||||
| 
 | ||||
| 		release.actors = query.all('a[href*="actress/"]').map((actorEl) => ({ // actors can be only in title or dedicated field
 | ||||
| 			name: unprint.query.content(actorEl), | ||||
| 			url: unprint.query.url(actorEl, null, { origin: channel.url }), | ||||
| 		})); | ||||
| 
 | ||||
| 		release.tags = tagsMap[query.content('a[href*="type/"]')?.toLowerCase()]; | ||||
| 
 | ||||
| 		const posterBackground = query.style('.player'); | ||||
| 
 | ||||
| 		if (posterBackground?.background) { | ||||
| 			release.poster = posterBackground.background.match(/url\((.*)\)/)?.[1]?.trim(); | ||||
| 		} | ||||
| 
 | ||||
| 		release.photos = query.all('.sample-thumbs .thumb a').map((linkEl) => [ | ||||
| 			unprint.query.url(linkEl, null), | ||||
| 			unprint.query.img(linkEl, 'img'), | ||||
| 		].filter((src) => !src.includes('join'))); | ||||
| 
 | ||||
| 		release.trailer = query.video('.player source'); | ||||
| 
 | ||||
| 		release.photoCount = query.number('//div[contains(text(), "Photos")]'); | ||||
| 		release.cumshots = query.number('//div[contains(text(), "Cumshots")]'); | ||||
| 
 | ||||
| 		release.entryId = entryIdFromMedia(release); | ||||
| 
 | ||||
| 		const tile = tilesByEntryId[release.entryId]; | ||||
| 
 | ||||
| 		if (tile) { | ||||
| 			Object.entries(tile).forEach(([key, value]) => { | ||||
| 				if (!Object.hasOwn(release, key)) { | ||||
| 					release[key] = value; | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		return release; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| // page has no container divs, select all following siblings until the 'join' link indicating the end of the block
 | ||||
| function composeBlock(element, init = true, acc = '') { | ||||
| 	const newAcc = `${acc}${element.outerHTML}`; | ||||
| 
 | ||||
| 	// image albums also contain a join link, make sure not to select that one
 | ||||
| 	if (element.nextElementSibling.className.includes('join') | ||||
| 		|| !!element.nextElementSibling.querySelector('.item-join, .join-link') | ||||
| 		|| !!element.nextElementSibling.querySelector('h2 a[href*="join"]') | ||||
| 	) { | ||||
| 		if (init) { | ||||
| 			return unprint.init(newAcc); | ||||
| 		} | ||||
| 
 | ||||
| 		return newAcc; | ||||
| 	} | ||||
| 
 | ||||
| 	return composeBlock(element.nextElementSibling, init, newAcc); | ||||
| } | ||||
| 
 | ||||
| // used for both SpermMania and Fellation Japan, but different layouts
 | ||||
| function scrapeAllTiles(tiles, channel) { | ||||
| 	return tiles.map(({ query }) => { | ||||
| 		const release = {}; | ||||
| 		const sceneString = query.content(); | ||||
| 
 | ||||
| 		const originalEntryId = query.attribute('.scene-hover', 'data-path'); | ||||
| 		release.entryId = originalEntryId?.toLowerCase(); | ||||
| 
 | ||||
| 		release.title = query.content('.scene-title'); | ||||
| 
 | ||||
| 		release.date = query.date('.scene-date, .sDate', 'YYYY-MM-DD'); | ||||
| 		release.duration = query.duration('.data.orange') || unprint.extractDuration(sceneString.match(/([\d:]+)\s*min/)?.[1]); | ||||
| 
 | ||||
| 		release.actors = query.all('a[href*="actress/"], .sGirl a').map((actorEl) => ({ // actors can be only in title or dedicated field
 | ||||
| 			name: unprint.query.content(actorEl), | ||||
| 			url: channel.slug === 'fellatiojapan' | ||||
| 				? `${channel.url}/en/girl/${unprint.query.url(actorEl, null)}` | ||||
| 				: unprint.query.element(actorEl, null, { origin: channel.url }), | ||||
| 		})); | ||||
| 
 | ||||
| 		release.tags = [...query.contents('.data a[href*="/tag"]'), ...(tagsMap[query.content('.scene-type')?.toLowerCase()] || [])].filter(Boolean); | ||||
| 
 | ||||
| 		const posterBackground = query.style('.scene-img'); | ||||
| 		const posterUrl = posterBackground?.background?.match(/url\((.*)\)/)?.[1]?.trim(); | ||||
| 
 | ||||
| 		if (posterUrl) { | ||||
| 			release.poster = [ | ||||
| 				posterUrl | ||||
| 					.replace('-sm', '-lg') | ||||
| 					.replace('-med', '-lg'), | ||||
| 				posterUrl.replace('-sm', '-med'), | ||||
| 				posterUrl, | ||||
| 			]; | ||||
| 		} | ||||
| 
 | ||||
| 		release.teaser = originalEntryId && `https://img.${channel.slug}.com/preview/${originalEntryId}/hover.mp4`; | ||||
| 
 | ||||
| 		release.photoCount = Number(sceneString.match(/(\d+) photos/)?.[1]) || null; | ||||
| 		release.cumshots = Number(sceneString.match(/(\d+) cumshots/)?.[1]) || null; | ||||
| 
 | ||||
| 		return release; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| // Sperm Mania
 | ||||
| async function fetchLatestTiles(channel) { | ||||
| 	const res = await unprint.get(`${channel.url}/tour`, { selectAll: '.scene' }); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		const tiles = scrapeAllTiles(res.context, channel); | ||||
| 
 | ||||
| 		return Object.fromEntries(tiles.map((tile) => [tile.entryId, tile])); | ||||
| 	} | ||||
| 
 | ||||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| // SpermMania, sample feed with limited info
 | ||||
| async function fetchLatest(channel, page = 1) { | ||||
| 	const url = `${channel.url}/samples?page=${page}`; | ||||
| 
 | ||||
| 	const [res, tilesByEntryId] = await Promise.all([ | ||||
| 		unprint.get(url, { selectAll: '.sample-title, .item-title' }), | ||||
| 		fetchLatestTiles(channel), | ||||
| 	]); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		const expandedContext = res.context.map(({ element }) => composeBlock(element)); | ||||
| 
 | ||||
| 		return scrapeAll(expandedContext, tilesByEntryId, channel); | ||||
| 	} | ||||
| 
 | ||||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| function scrapeAllCospuri(scenes, channel) { | ||||
| 	return scenes.map(({ query }) => { | ||||
| 		const release = {}; | ||||
| 
 | ||||
| 		release.url = query.url('.scene-thumb a'); | ||||
| 		release.entryId = new URL(release.url).searchParams.get('id') | ||||
| 			|| new URL(release.url).pathname.match(/\/sample\/(.*)\//)[1]; | ||||
| 
 | ||||
| 		release.title = query.content('.title'); | ||||
| 
 | ||||
| 		release.date = query.date('.date', 'YYYY・MM・DD', { match: /\d{4}・\d{2}・\d{2}/ }); | ||||
| 		release.duration = query.duration('.length'); | ||||
| 		release.photoCount = query.number('.photos'); | ||||
| 
 | ||||
| 		release.actors = query.all('.model a[href*="/model"]').map((actorEl) => ({ | ||||
| 			name: unprint.query.content(actorEl), | ||||
| 			url: unprint.query.url(actorEl, null, { origin: channel.url }), | ||||
| 		})); | ||||
| 
 | ||||
| 		release.tags = [...query.contents('.tags .tag, .tag-box .tag'), query.content('.model .channel')].filter(Boolean); | ||||
| 
 | ||||
| 		const posterBackground = query.style('.scene-thumb'); | ||||
| 		const posterUrl = posterBackground?.background?.match(/url\((.*)\)/)?.[1]?.trim(); | ||||
| 
 | ||||
| 		if (posterUrl) { | ||||
| 			release.poster = [ | ||||
| 				posterUrl | ||||
| 					.replace('-med', '-lg') | ||||
| 					.replace('-sm', '-lg'), | ||||
| 				posterUrl.replace('-sm', '-med'), | ||||
| 				posterUrl, | ||||
| 			]; | ||||
| 		} | ||||
| 
 | ||||
| 		release.teaser = query.video('.scene-hover', { attribute: 'data-path' }); | ||||
| 
 | ||||
| 		return release; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| // Cospuri, Cute Butts, paginated sample tiles with full info
 | ||||
| async function fetchLatestCospuri(channel, page) { | ||||
| 	const url = `${channel.url}/samples?page=${page}`; | ||||
| 
 | ||||
| 	const res = await unprint.get(url, { selectAll: '.scene' }); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		return scrapeAllCospuri(res.context, channel); | ||||
| 	} | ||||
| 
 | ||||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| function curatePhotos(sources) { | ||||
| 	return sources | ||||
| 		.filter(Boolean).map((src) => [ | ||||
| 			src.replace(/(\d+)s.jpg/, (match, photoIndex) => `${photoIndex}.jpg`), | ||||
| 			src, | ||||
| 		].map((url) => ({ | ||||
| 			src: url, | ||||
| 			followRedirects: false, | ||||
| 		}))); | ||||
| } | ||||
| 
 | ||||
| function scrapeAllFellatio(scenes, channel) { | ||||
| 	return scenes.map(({ query }) => { | ||||
| 		const release = {}; | ||||
| 
 | ||||
| 		release.duration = query.duration('.tour-data'); | ||||
| 		release.photoCount = query.number('.tour-data', { match: /(\d+) photos/, matchIndex: 1 }); | ||||
| 
 | ||||
| 		release.actors = query.all('.tour-data a[href*="girl/"]').map((actorEl) => ({ | ||||
| 			name: unprint.query.content(actorEl), | ||||
| 			url: `${channel.url}/en/${unprint.query.url(actorEl, null)}`, | ||||
| 		})); | ||||
| 
 | ||||
| 		release.tags = query.contents('.tour-data a[href*="tag/"]'); | ||||
| 
 | ||||
| 		const posterBackground = query.style('.player'); | ||||
| 		const posterUrl = posterBackground?.background?.match(/url\((.*)\)/)?.[1]?.trim(); | ||||
| 
 | ||||
| 		if (posterUrl) { | ||||
| 			release.poster = posterUrl; | ||||
| 		} | ||||
| 
 | ||||
| 		release.photos = curatePhotos(query.imgs('.tour-thumb img')); | ||||
| 		release.trailer = query.video(); | ||||
| 
 | ||||
| 		release.entryId = entryIdFromMedia(release); | ||||
| 		release.path = release.actors[0]?.url; | ||||
| 
 | ||||
| 		return release; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| // Fellatio Japan
 | ||||
| async function fetchLatestFellatio(channel, page) { | ||||
| 	const url = `${channel.url}/en/samples/?page=${page}`; | ||||
| 	const res = await unprint.get(url, { selectAll: '.tour-data' }); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		const expandedContext = res.context.map(({ element }) => composeBlock(element)); | ||||
| 
 | ||||
| 		return scrapeAllFellatio(expandedContext, channel); | ||||
| 	} | ||||
| 
 | ||||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| function scrapeAllHandjob(scenes, _channel) { | ||||
| 	return scenes.map(({ query }) => { | ||||
| 		const release = {}; | ||||
| 
 | ||||
| 		release.title = query.content('.blurb'); | ||||
| 
 | ||||
| 		release.duration = query.duration('.item-rtitle'); | ||||
| 		release.photoCount = query.number('//h3[contains(text(), "Scene Photos")]/strong'); | ||||
| 
 | ||||
| 		release.actors = query.text('.item-ltitle h1')?.split(/,\s*/).map((actor) => actor.trim()); | ||||
| 
 | ||||
| 		const posterBackground = query.style('.player'); | ||||
| 		const posterUrl = posterBackground?.background?.match(/url\((.*)\)/)?.[1]?.trim(); | ||||
| 
 | ||||
| 		if (posterUrl) { | ||||
| 			release.poster = posterUrl; | ||||
| 		} | ||||
| 
 | ||||
| 		release.photos = curatePhotos(query.imgs('img.thumb, img.rthumb')); | ||||
| 		release.trailer = query.video(); | ||||
| 
 | ||||
| 		release.entryId = entryIdFromMedia(release); | ||||
| 
 | ||||
| 		return release; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| // Handjob Japan
 | ||||
| async function fetchLatestHandjob(channel, page) { | ||||
| 	const url = `${channel.url}/en/samples/?page=${page}`; | ||||
| 	const res = await unprint.get(url, { selectAll: '.item-title' }); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		const expandedContext = res.context.map(({ element }) => composeBlock(element)); | ||||
| 
 | ||||
| 		return scrapeAllHandjob(expandedContext, channel); | ||||
| 	} | ||||
| 
 | ||||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| function scrapeAllLegs(scenes, channel) { | ||||
| 	return scenes.map(({ query }) => { | ||||
| 		const release = {}; | ||||
| 
 | ||||
| 		release.title = query.content('.tContent h3 strong'); | ||||
| 
 | ||||
| 		release.duration = query.duration('//h3[contains(text(), "length")]/strong'); | ||||
| 		release.photoCount = query.number('//h3[contains(text(), "photos")]/strong'); | ||||
| 
 | ||||
| 		release.actors = query.all('.tContent a[href*="girl/"]').map((actorEl) => ({ | ||||
| 			name: unprint.query.content(actorEl), | ||||
| 			url: `${channel.url}/en/${unprint.query.url(actorEl, null)}`, | ||||
| 		})); | ||||
| 
 | ||||
| 		release.tags = query.contents('a[href*="tag/"]'); | ||||
| 
 | ||||
| 		const posterBackground = query.style('.player'); | ||||
| 		const posterUrl = posterBackground?.background?.match(/url\((.*)\)/)?.[1]?.trim(); | ||||
| 
 | ||||
| 		if (posterUrl) { | ||||
| 			release.poster = posterUrl; | ||||
| 		} | ||||
| 
 | ||||
| 		release.photos = curatePhotos(query.imgs('.tThumbs img')); | ||||
| 		release.trailer = query.video(); | ||||
| 
 | ||||
| 		release.entryId = entryIdFromMedia(release); | ||||
| 
 | ||||
| 		return release; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| // Legs Japan
 | ||||
| async function fetchLatestLegs(channel, page) { | ||||
| 	const url = `${channel.url}/en/samples/?page=${page}`; | ||||
| 	const res = await unprint.get(url, { selectAll: '.player' }); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		const expandedContext = res.context.map(({ element }) => composeBlock(element)); | ||||
| 
 | ||||
| 		return scrapeAllLegs(expandedContext, channel); | ||||
| 	} | ||||
| 
 | ||||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| function scrapeAllFacefuck(scenes) { | ||||
| 	return scenes.map(({ query }) => { | ||||
| 		const release = {}; | ||||
| 
 | ||||
| 		release.description = query.content('.infotxt'); | ||||
| 		release.actors = query.content('.info h1').split(',').map((actor) => actor.trim()); | ||||
| 
 | ||||
| 		const posterBackground = query.style('.player'); | ||||
| 		const posterUrl = posterBackground?.background?.match(/url\((.*)\)/)?.[1]?.trim(); | ||||
| 
 | ||||
| 		if (posterUrl) { | ||||
| 			release.poster = posterUrl; | ||||
| 		} | ||||
| 
 | ||||
| 		release.photos = curatePhotos(query.imgs('.thumb img')); | ||||
| 		release.trailer = query.video(); | ||||
| 
 | ||||
| 		release.entryId = entryIdFromMedia(release); | ||||
| 
 | ||||
| 		return release; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| // Tokyo Facefuck
 | ||||
| async function fetchLatestFacefuck(channel, page) { | ||||
| 	const url = `${channel.url}/en/?page=${page}`; | ||||
| 	const res = await unprint.get(url, { selectAll: '.girl.box' }); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		return scrapeAllFacefuck(res.context, channel); | ||||
| 	} | ||||
| 
 | ||||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| function scrapeAllTrans(scenes) { | ||||
| 	return scenes.map(([{ query }, { query: albumQuery }]) => { | ||||
| 		const release = {}; | ||||
| 
 | ||||
| 		release.title = query.content('.sample-info h1'); | ||||
| 		release.actors = query.content('.sample-info a strong').split(',').map((actor) => actor.trim()); | ||||
| 
 | ||||
| 		release.description = query.content('.sample-desc')?.replace('""', '') || null; // usually empty, but let's try it just in case
 | ||||
| 
 | ||||
| 		release.duration = query.duration('.sample-info'); | ||||
| 		release.photoCount = albumQuery.number('.sample-info', { match: /(\d+) photos/i, matchIndex: 1 }); | ||||
| 
 | ||||
| 		const posterBackground = query.style('.player'); | ||||
| 		const posterUrl = posterBackground?.background?.match(/url\((.*)\)/)?.[1]?.trim(); | ||||
| 
 | ||||
| 		if (posterUrl) { | ||||
| 			release.poster = [ | ||||
| 				posterUrl, | ||||
| 				posterUrl.replace(/-\d.jpg/, '-2.jpg'), | ||||
| 				posterUrl.replace(/-\d.jpg/, '-1.jpg'), | ||||
| 			]; | ||||
| 		} | ||||
| 
 | ||||
| 		release.photos = curatePhotos(albumQuery.styles('.sample-lg, .sample-thumb').map((style) => style['background-image']?.match(/url\((.*)\)/)?.[1])); | ||||
| 		release.trailer = query.video(); | ||||
| 
 | ||||
| 		release.entryId = entryIdFromMedia(release); | ||||
| 
 | ||||
| 		return release; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| // Trans Sex Japan
 | ||||
| async function fetchLatestTrans(channel, page) { | ||||
| 	const url = `${channel.url}/samples?page=${page}`; | ||||
| 	const res = await unprint.get(url, { select: '.stage' }); | ||||
| 
 | ||||
| 	const videoHeads = unprint.initAll(res.context.element, '//div[contains(@class, "col-1") and .//div[contains(@class, "player")]]'); | ||||
| 	const albumHeads = unprint.initAll(res.context.element, '//div[div[contains(@class, "sample-thumbs")]]'); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		const videoBlocks = videoHeads.map(({ element }) => composeBlock(element)); | ||||
| 		const albumBlocks = albumHeads.map(({ element }) => composeBlock(element)); | ||||
| 
 | ||||
| 		const mergedContext = videoBlocks.map((context, index) => [context, albumBlocks[index]]); | ||||
| 
 | ||||
| 		return scrapeAllTrans(mergedContext, channel); | ||||
| 	} | ||||
| 
 | ||||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| function scrapeAllLesbianTiles(scenes, channel) { | ||||
| 	return scenes.map(({ query }) => { | ||||
| 		const release = {}; | ||||
| 
 | ||||
| 		release.entryId = query.attribute('.scene-hover', 'data-path'); | ||||
| 
 | ||||
| 		// supplementary data, filter items without entry ID
 | ||||
| 		if (!release.entryId || query.content('.content-overlay')?.includes('photo')) { | ||||
| 			return null; | ||||
| 		} | ||||
| 
 | ||||
| 		release.title = query.content('.content-title'); | ||||
| 		release.duration = query.duration('.content-size-model'); | ||||
| 
 | ||||
| 		release.actors = query.all('.content-size-model a').map((actorEl) => ({ | ||||
| 			name: unprint.query.content(actorEl), | ||||
| 			url: unprint.query.url(actorEl, null, { origin: channel.url }), | ||||
| 		})); | ||||
| 
 | ||||
| 		release.tags = query.contents('.content-tags a'); | ||||
| 
 | ||||
| 		const posterBackground = query.style('.vidthumb'); | ||||
| 		const posterUrl = posterBackground?.background?.match(/url\((.*)\)/)?.[1]?.trim(); | ||||
| 
 | ||||
| 		if (posterUrl) { | ||||
| 			release.poster = [ | ||||
| 				posterUrl | ||||
| 					.replace('-sm', '-lg') | ||||
| 					.replace('-med', '-lg'), | ||||
| 				posterUrl.replace('-sm', '-med'), | ||||
| 				posterUrl, | ||||
| 			]; | ||||
| 		} | ||||
| 
 | ||||
| 		release.teaser = `${channel.url}/content/${release.entryId}/hover.mp4`; | ||||
| 
 | ||||
| 		return release; | ||||
| 	}).filter(Boolean); | ||||
| } | ||||
| 
 | ||||
| function scrapeAllLesbian(scenes, channel, tiles) { | ||||
| 	return scenes.map(({ query }) => { | ||||
| 		const release = {}; | ||||
| 
 | ||||
| 		if (query.exists('a[href*="samples"]')) { | ||||
| 			return null; | ||||
| 		} | ||||
| 
 | ||||
| 		release.actors = query.all('a[href*="model/"]').map((actorEl) => ({ | ||||
| 			name: unprint.query.content(actorEl), | ||||
| 			url: `${channel.url}/en/${unprint.query.url(actorEl, null)}`, | ||||
| 		})); | ||||
| 
 | ||||
| 		release.duration = unprint.extractTimestamp(`${query.content('.tour-datum')?.split(' ').at(-1)}M`); | ||||
| 		release.videoCount = query.number('.tour-datum', { match: /(\d+) hd scenes/i, matchIndex: 1 }); | ||||
| 		release.photoCount = query.number('//div[text()[contains(., "Photos")]]', { match: /(\d+) photos/i, matchIndex: 1 }); | ||||
| 
 | ||||
| 		const posterBackground = query.style('.player'); | ||||
| 		const posterUrl = posterBackground?.background?.match(/url\((.*)\)/)?.[1]?.trim(); | ||||
| 
 | ||||
| 		if (posterUrl) { | ||||
| 			release.poster = posterUrl; | ||||
| 		} | ||||
| 
 | ||||
| 		release.trailer = query.video(); | ||||
| 		release.photos = curatePhotos(query.imgs('.tour-thumb img')); | ||||
| 
 | ||||
| 		release.entryId = slugify([entryIdFromMedia(release), ...release.actors.map((actor) => actor.name)]); | ||||
| 
 | ||||
| 		const relatedTiles = tiles.filter((tile) => tile.actors.length === release.actors.length && tile.actors.every((tileActor) => release.actors.some((releaseActor) => tileActor.name === releaseActor.name))); | ||||
| 
 | ||||
| 		// if we found the same number of tiles as videos in this set, we can be pretty sure they relate to this set
 | ||||
| 		// if there are more, we have no way of determining which of the videos belong to this set
 | ||||
| 		if (relatedTiles.length === release.videoCount) { | ||||
| 			const sortedTiles = relatedTiles.toSorted((tileA, tileB) => tileA.entryId.localeCompare(tileB.entryId)); // entry IDs appear chronological
 | ||||
| 
 | ||||
| 			release.tags = relatedTiles.flatMap((tile) => tile.tags); | ||||
| 
 | ||||
| 			release.chapters = sortedTiles.map((tile, index, array) => { | ||||
| 				const time = array.slice(0, index).reduce((acc, relatedTile) => acc + relatedTile.duration, 0); | ||||
| 
 | ||||
| 				return { | ||||
| 					title: tile.title, | ||||
| 					time, | ||||
| 					duration: tile.duration, | ||||
| 					tags: tile.tags, | ||||
| 					poster: tile.poster, | ||||
| 				}; | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		return release; | ||||
| 	}).filter(Boolean); | ||||
| } | ||||
| 
 | ||||
| // Uralesbian
 | ||||
| async function fetchLatestLesbianTiles(channel, _page) { | ||||
| 	// each sample on the samples page represents multiple videos, so for this site we start with the update tiles instead
 | ||||
| 	// l=0		language, 0 = English, 1 = Japanese
 | ||||
| 	// s=1		unclear, seems to be some sort of set, s=1 is everything, s=4 is front page
 | ||||
| 	// c=5000	limit, only seems to apply to 'everything' set, seemingly unlimited by default but apply for good measure
 | ||||
| 	// no known pagination parameter at this moment, so we try to get everything
 | ||||
| 	const url = `${channel.url}/getdata.php?l=0&c=5000`; | ||||
| 	const res = await unprint.get(url, { selectAll: '.content-obj' }); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		return scrapeAllLesbianTiles(res.context, channel); | ||||
| 	} | ||||
| 
 | ||||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| // Uralesbian
 | ||||
| async function fetchLatestLesbian(channel, page) { | ||||
| 	const url = `${channel.url}/en/samples?page=${page}`; | ||||
| 
 | ||||
| 	const [res, tiles] = await Promise.all([ | ||||
| 		unprint.get(url, { selectAll: '.tour-obj' }), | ||||
| 		fetchLatestLesbianTiles(channel), | ||||
| 	]); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		return scrapeAllLesbian(res.context, channel, tiles); | ||||
| 	} | ||||
| 
 | ||||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| function scrapeAllBuffet(scenes, channel) { | ||||
| 	return scenes.map(({ query }) => { | ||||
| 		const release = {}; | ||||
| 
 | ||||
| 		release.url = query.url('.video-link'); | ||||
| 		release.entryId = new URL(release.url).pathname.match(/sample\/(\w+)\//)[1]; | ||||
| 
 | ||||
| 		release.title = query.content('.video-link'); | ||||
| 		release.date = query.date('.date', 'MMM D, YYYY'); | ||||
| 
 | ||||
| 		release.actors = query.all('.model-name a').map((actorEl) => ({ | ||||
| 			name: unprint.query.content(actorEl), | ||||
| 			url: unprint.query.url(actorEl, null, { origin: channel.url }), | ||||
| 		})); | ||||
| 
 | ||||
| 		const posterUrl = query.img('.thumb'); | ||||
| 
 | ||||
| 		if (posterUrl) { | ||||
| 			release.poster = [ | ||||
| 				posterUrl | ||||
| 					.replace('-sm', '-lg') | ||||
| 					.replace('-med', '-lg'), | ||||
| 				posterUrl.replace('-sm', '-med'), | ||||
| 				posterUrl, | ||||
| 			]; | ||||
| 		} | ||||
| 
 | ||||
| 		return release; | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| // Uralesbian
 | ||||
| async function fetchLatestBuffet(channel, _page) { | ||||
| 	const url = `${channel.url}/samples`; // no pagination
 | ||||
| 	const res = await unprint.get(url, { selectAll: '.videos .video' }); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		return scrapeAllBuffet(res.context, channel); | ||||
| 	} | ||||
| 
 | ||||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| function scrapeSceneBuffet({ query }, { url, entity }) { | ||||
| 	const release = {}; | ||||
| 
 | ||||
| 	release.entryId = new URL(url).pathname.match(/sample\/(\w+)\//)[1]; | ||||
| 
 | ||||
| 	release.title = query.text('.pg-nav h2'); | ||||
| 
 | ||||
| 	release.actors = query.all('.tags a[href*="girl/"]').map((actorEl) => ({ | ||||
| 		name: unprint.query.content(actorEl), | ||||
| 		url: unprint.query.url(actorEl, null, { origin: entity.url }), | ||||
| 	})); | ||||
| 
 | ||||
| 	release.tags = query.contents('.tag-list a'); | ||||
| 
 | ||||
| 	const posterBackground = query.style('.player'); | ||||
| 	const posterUrl = posterBackground?.background?.match(/url\((.*)\)/)?.[1]?.trim(); | ||||
| 
 | ||||
| 	if (posterUrl) { | ||||
| 		release.poster = [ | ||||
| 			posterUrl.replace('-sm', '-lg'), // should already be -lg, but just in case
 | ||||
| 			posterUrl.replace('-lg', '-sm'), | ||||
| 		]; | ||||
| 	} | ||||
| 
 | ||||
| 	release.trailer = query.video('.player source'); | ||||
| 	release.photos = query.imgs('.photos .photo', { attribute: 'href' }); | ||||
| 
 | ||||
| 	return release; | ||||
| } | ||||
| 
 | ||||
| function scrapeSceneCospuri({ query }, { url, entity }) { | ||||
| 	const release = {}; | ||||
| 
 | ||||
| 	release.entryId = new URL(url).searchParams.get('id') | ||||
| 		|| new URL(url).pathname.match(/\/sample\/(.*)\//)[1]; | ||||
| 
 | ||||
| 	release.description = query.content('.detail-box .description'); | ||||
| 
 | ||||
| 	release.date = query.date([ | ||||
| 		'.detail-box .date', // cospuri
 | ||||
| 		'//div[contains(@class, "details")]//span[strong[contains(text(), "Date")]]', // cute butts
 | ||||
| 	], 'YYYY・MM・DD', { match: /\d{4}・\d{2}・\d{2}/ }); | ||||
| 
 | ||||
| 	release.duration = query.duration([ | ||||
| 		'.detail-box .length', | ||||
| 		'//div[contains(@class, "details")]//span[strong[contains(text(), "Runtime")]]', // cute butts
 | ||||
| 	]); | ||||
| 
 | ||||
| 	release.photoCount = query.number([ | ||||
| 		'.detail-box .photos', | ||||
| 		'//div[contains(@class, "details")]//span[strong[contains(text(), "Photos")]]', // cute butts
 | ||||
| 	]); | ||||
| 
 | ||||
| 	release.actors = query.all('.sample-model a, .model a').map((actorEl) => ({ | ||||
| 		name: unprint.query.content(actorEl), | ||||
| 		url: unprint.query.url(actorEl, null, { origin: entity.url }), | ||||
| 	})); | ||||
| 
 | ||||
| 	release.tags = [...query.contents('.tag'), query.content('.sample-channel')].filter(Boolean); | ||||
| 
 | ||||
| 	const posterBackground = query.style('.player'); | ||||
| 	const posterUrl = posterBackground?.background?.match(/url\((.*)\)/)?.[1]?.trim(); | ||||
| 
 | ||||
| 	if (posterUrl) { | ||||
| 		release.poster = posterUrl; | ||||
| 	} | ||||
| 
 | ||||
| 	release.photos = query.attributes('.thumb a', 'data-asset').map((photoIndex) => [ | ||||
| 		`https://img.${entity.slug}.com/preview/${release.entryId}/${photoIndex}.jpg`, | ||||
| 		`https://img.${entity.slug}.com/preview/${release.entryId}/${photoIndex}s.jpg`, | ||||
| 	]); | ||||
| 
 | ||||
| 	release.trailer = `https://img.${entity.slug}.com/preview/${release.entryId}/sample.mp4`; | ||||
| 
 | ||||
| 	if (query.exists('.detail-box .fourK')) { | ||||
| 		release.qualities = [2160]; | ||||
| 	} | ||||
| 
 | ||||
| 	return release; | ||||
| } | ||||
| 
 | ||||
| // Fellatio Japan
 | ||||
| async function fetchSceneFellatio(url, channel, baseRelease) { | ||||
| 	if (!baseRelease.entryId || !baseRelease.path) { | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	// no dedicated scene page, but there are dates on actor page; use that as 'deep' scrape
 | ||||
| 	// can't use front page like on Sperm Mania because dates are missing
 | ||||
| 	const res = await unprint.get(baseRelease.path, { selectAll: '.scene-obj' }); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		const tiles = scrapeAllTiles(res.context, channel); | ||||
| 
 | ||||
| 		return tiles.find((tile) => tile.entryId === baseRelease.entryId) || null; | ||||
| 	} | ||||
| 
 | ||||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| function extractSizes(sizes) { | ||||
| 	return { | ||||
| 		cup: sizes.match(/b\d+-(\w+)/i)?.[1], | ||||
| 		bust: unprint.extractNumber(sizes.match(/b(\d+)/i)?.[1]), | ||||
| 		waist: unprint.extractNumber(sizes.match(/w(\d+)/i)?.[1]), | ||||
| 		hip: unprint.extractNumber(sizes.match(/h(\d+)/i)?.[1]), | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| // SpermMania, Handjob Japan
 | ||||
| function scrapeProfile({ query }, channel, url) { | ||||
| 	const profile = { url }; | ||||
| 
 | ||||
| 	const bio = Object.fromEntries(query.all('.actr-item, .profile tr, #profile tr, .profile-info li, .model-detail .item, .model-item').map((bioEl) => [ | ||||
| 		slugify(unprint.query.content(bioEl, 'td, b, .model-item-title') || unprint.query.text(bioEl), '_'), | ||||
| 		unprint.query.url(bioEl) || unprint.query.content(bioEl, 'strong, td:last-child, span, .model-item-contents') || unprint.query.text(bioEl), // ensure social links have priority over text
 | ||||
| 	])); | ||||
| 
 | ||||
| 	profile.birthPlace = bio.from || bio.country; | ||||
| 
 | ||||
| 	profile.description = [ | ||||
| 		bio.hobbies && `Hobbies: ${bio.hobbies}`, | ||||
| 		bio.skills && `Skills: ${bio.skills}`, | ||||
| 		bio.fun_fact, | ||||
| 		query.content('h2 + p'), | ||||
| 	].filter(Boolean).join('. ') || null; | ||||
| 
 | ||||
| 	profile.age = unprint.extractNumber(bio.age); | ||||
| 	profile.height = unprint.extractNumber(bio.height); | ||||
| 
 | ||||
| 	const sizes = bio.sizes || bio.measurements; | ||||
| 
 | ||||
| 	if (/b\d+/i.test(sizes)) { | ||||
| 		const measurements = extractSizes(sizes); | ||||
| 
 | ||||
| 		profile.cup = measurements.cup; | ||||
| 		profile.bust = measurements.bust; | ||||
| 		profile.waist = measurements.waist; | ||||
| 		profile.hip = measurements.hip; | ||||
| 	} else { | ||||
| 		profile.measurements = bio.measurements; | ||||
| 	} | ||||
| 
 | ||||
| 	profile.foot = unprint.extractNumber(bio.foot_size); | ||||
| 	profile.leg = unprint.extractNumber(bio.leg_length); | ||||
| 	profile.thigh = unprint.extractNumber(bio.thigh_width); | ||||
| 
 | ||||
| 	profile.social = [bio.homepage, bio.twitter].filter(Boolean); | ||||
| 
 | ||||
| 	const avatar = query.img('.scene-array img[src*="/actress"], img.portrait, .profile-img img') | ||||
| 		|| query.img('.costume-bg', { attribute: 'data-img' }) | ||||
| 		|| query.style('.model-profile, #profile, .carousel-item')?.['background-image']?.match(/url\((.*)\)/)?.[1]; | ||||
| 
 | ||||
| 	if (avatar) { | ||||
| 		profile.avatar = [ | ||||
| 			avatar.replace('-header.jpg', '.jpg'), // Transex Japan, prefer avatar over header banner
 | ||||
| 			avatar, | ||||
| 		]; | ||||
| 	} | ||||
| 
 | ||||
| 	profile.photos = [ | ||||
| 		...query.imgs('.costume-bg', { attribute: 'data-img' }).slice(1), | ||||
| 		avatar?.includes('-header.jpg') && avatar, | ||||
| 	].filter(Boolean); | ||||
| 
 | ||||
| 	return profile; | ||||
| } | ||||
| 
 | ||||
| function scrapeProfileLesbian({ query, html }, channel, url) { | ||||
| 	const profile = { url }; | ||||
| 
 | ||||
| 	profile.age = query.number('//strong[contains(text(), "Age")]/following-sibling::text()[1]'); | ||||
| 	profile.height = query.number('//strong[contains(text(), "Height")]/following-sibling::text()[1]'); | ||||
| 	profile.birthPlace = query.content('//img[contains(@src, "from")]/following-sibling::text()[1]')?.replace(/^from/i, '').trim() || null; | ||||
| 
 | ||||
| 	const sizes = query.content('//strong[contains(text(), "Measurements")]/following-sibling::text()[1]'); | ||||
| 
 | ||||
| 	if (/b\d+/i.test(sizes)) { | ||||
| 		const measurements = extractSizes(sizes); | ||||
| 
 | ||||
| 		profile.cup = measurements.cup; | ||||
| 		profile.bust = measurements.bust; | ||||
| 		profile.waist = measurements.waist; | ||||
| 		profile.hip = measurements.hip; | ||||
| 	} | ||||
| 
 | ||||
| 	profile.avatar = html.match(/https:\/\/img.uralesbian.com\/models\/\d+\.jpg/)?.[0]; | ||||
| 
 | ||||
| 	return profile; | ||||
| } | ||||
| 
 | ||||
| async function fetchProfile({ slug, url: actorUrl }, { entity, parameters }) { | ||||
| 	const url = actorUrl || (parameters.actors | ||||
| 		? `${parameters.actors}/${slug}` | ||||
| 		: `${entity.url}/actress/${slug}`); | ||||
| 
 | ||||
| 	const res = await unprint.get(url); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		if (parameters.layout === 'lesbian') { | ||||
| 			return scrapeProfileLesbian(res.context, entity, url); | ||||
| 		} | ||||
| 
 | ||||
| 		return scrapeProfile(res.context, entity, url); | ||||
| 	} | ||||
| 
 | ||||
| 	return res.status; | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
| 	fetchLatest, | ||||
| 	fetchProfile, | ||||
| 	cospuri: { | ||||
| 		fetchLatest: fetchLatestCospuri, | ||||
| 		scrapeScene: scrapeSceneCospuri, | ||||
| 		fetchProfile, | ||||
| 	}, | ||||
| 	fellatio: { | ||||
| 		fetchLatest: fetchLatestFellatio, | ||||
| 		fetchScene: fetchSceneFellatio, | ||||
| 		fetchProfile, | ||||
| 	}, | ||||
| 	handjob: { | ||||
| 		fetchLatest: fetchLatestHandjob, | ||||
| 		fetchProfile, | ||||
| 	}, | ||||
| 	legs: { | ||||
| 		fetchLatest: fetchLatestLegs, | ||||
| 		fetchProfile, | ||||
| 	}, | ||||
| 	facefuck: { | ||||
| 		fetchLatest: fetchLatestFacefuck, | ||||
| 	}, | ||||
| 	trans: { | ||||
| 		fetchLatest: fetchLatestTrans, | ||||
| 		fetchProfile, | ||||
| 	}, | ||||
| 	lesbian: { | ||||
| 		fetchLatest: fetchLatestLesbian, | ||||
| 		fetchProfile, | ||||
| 	}, | ||||
| 	buffet: { | ||||
| 		fetchLatest: fetchLatestBuffet, | ||||
| 		scrapeScene: scrapeSceneBuffet, | ||||
| 		fetchProfile, | ||||
| 	}, | ||||
| }; | ||||
|  | @ -1,7 +1,9 @@ | |||
| 'use strict'; | ||||
| 
 | ||||
| const config = require('config'); | ||||
| const unprint = require('unprint'); | ||||
| const format = require('template-format'); | ||||
| const { HttpsProxyAgent } = require('https-proxy-agent'); | ||||
| 
 | ||||
| const qu = require('../utils/qu'); | ||||
| const slugify = require('../utils/slugify'); | ||||
|  | @ -137,11 +139,14 @@ function scrapeProfile({ query, el }) { | |||
| 	return profile; | ||||
| } | ||||
| 
 | ||||
| const agent = new HttpsProxyAgent(`http://${config.proxy.host}:${config.proxy.port}`); | ||||
| 
 | ||||
| async function fetchLatest(channel, page) { | ||||
| 	// const res = await qu.getAll(`${channel.url}/categories/movies_${page}_d.html`, '.thumb-big, .thumb-video, .thumbnail, .thumbnail-popular, .full-thumbnail');
 | ||||
| 
 | ||||
| 	const res = await unprint.get(`${channel.url}${format(channel.parameters?.latest || '/categories/movies_{page}_d.html', { page })}`, { | ||||
| 		selectAll: '.thumb-big, .thumb-video, .thumbnail, .thumbnail-popular, .full-thumbnail', | ||||
| 		httpsAgent: agent, | ||||
| 	}); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
|  |  | |||
|  | @ -349,7 +349,7 @@ async function storeMovies(movies, useBatchId) { | |||
| 		return []; | ||||
| 	} | ||||
| 
 | ||||
| 	const { uniqueReleases } = await filterDuplicateReleases(movies, 'movies'); | ||||
| 	const { uniqueReleases, duplicateReleaseEntries } = await filterDuplicateReleases(movies, 'movies'); | ||||
| 	const [{ id: batchId }] = useBatchId ? [{ id: useBatchId }] : await knex('batches').insert({ showcased: argv.showcased, comment: null }).returning('id'); | ||||
| 
 | ||||
| 	const curatedMovieEntries = await Promise.all(uniqueReleases.map((release) => curateReleaseEntry(release, batchId, null, 'movie'))); | ||||
|  | @ -362,7 +362,15 @@ async function storeMovies(movies, useBatchId) { | |||
| 
 | ||||
| 	await associateReleaseMedia(moviesWithId, 'movie'); | ||||
| 
 | ||||
| 	return moviesWithId; | ||||
| 	return [...moviesWithId, ...duplicateReleaseEntries.map((entry) => ({ | ||||
| 		// used to map new movie scenes to existing movie entries
 | ||||
| 		id: entry.id, | ||||
| 		entryId: entry.entry_id, | ||||
| 		entityId: entry.entity_id, | ||||
| 		entity: { | ||||
| 			id: entry.entity_id, | ||||
| 		}, | ||||
| 	}))]; | ||||
| } | ||||
| 
 | ||||
| async function storeSeries(series, useBatchId) { | ||||
|  |  | |||
|  | @ -483,4 +483,5 @@ module.exports = { | |||
| 	getCookieJar, | ||||
| 	destroyBypassSessions, | ||||
| 	destroyBrowserSessions, | ||||
| 	proxyAgent, | ||||
| }; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue