Switched to tabs. Adding missing actor entries when scraping actors, with batch ID.

This commit is contained in:
ThePendulum 2020-05-14 04:26:05 +02:00
parent f1eb29c713
commit 11eb66f834
178 changed files with 16594 additions and 16929 deletions

View File

@ -5,7 +5,7 @@ root = true
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_style = tab
indent_size = 4
# Matches multiple files with brace expansion notation

View File

@ -7,13 +7,14 @@
"sourceType": "module"
},
"rules": {
"indent": ["error", "tab"],
"no-tabs": "off",
"no-unused-vars": ["error", {"argsIgnorePattern": "^_"}],
"no-console": 0,
"indent": "off",
"template-curly-spacing": "off",
"max-len": 0,
"vue/no-v-html": 0,
"vue/html-indent": ["error", 4],
"vue/html-indent": ["error", "tab"],
"vue/multiline-html-element-content-newline": 0,
"vue/singleline-html-element-content-newline": 0,
"no-param-reassign": ["error", {

View File

@ -1,248 +1,248 @@
<template>
<div
v-if="actor"
class="content actor"
>
<FilterBar :fetch-releases="fetchActor" />
<div
v-if="actor"
class="content actor"
>
<FilterBar :fetch-releases="fetchActor" />
<div class="actor-header">
<h2 class="header-name">
<span v-if="actor.network">{{ actor.name }} ({{ actor.network.name }})</span>
<span v-else="">{{ actor.name }}</span>
<div class="actor-header">
<h2 class="header-name">
<span v-if="actor.network">{{ actor.name }} ({{ actor.network.name }})</span>
<span v-else="">{{ actor.name }}</span>
<Gender
:gender="actor.gender"
class="header-gender"
/>
</h2>
<Gender
:gender="actor.gender"
class="header-gender"
/>
</h2>
<li
v-if="actor.aliases.length"
class="bio-item"
>
<dfn class="bio-label">Also known as</dfn>
<span>{{ actor.aliases.join(', ') }}</span>
</li>
<li
v-if="actor.aliases.length"
class="bio-item"
>
<dfn class="bio-label">Also known as</dfn>
<span>{{ actor.aliases.join(', ') }}</span>
</li>
<Social
v-if="actor.social && actor.social.length > 0"
:actor="actor"
class="header-social"
/>
</div>
<Social
v-if="actor.social && actor.social.length > 0"
:actor="actor"
class="header-social"
/>
</div>
<div class="actor-inner">
<div
class="profile"
:class="{ expanded, 'with-avatar': !!actor.avatar }"
>
<a
v-if="actor.avatar"
:href="`/media/${actor.avatar.path}`"
target="_blank"
rel="noopener noreferrer"
class="avatar-link"
>
<img
:src="`/media/${actor.avatar.thumbnail}`"
:title="actor.avatar.copyright && `© ${actor.avatar.copyright}`"
class="avatar"
>
</a>
<div class="actor-inner">
<div
class="profile"
:class="{ expanded, 'with-avatar': !!actor.avatar }"
>
<a
v-if="actor.avatar"
:href="`/media/${actor.avatar.path}`"
target="_blank"
rel="noopener noreferrer"
class="avatar-link"
>
<img
:src="`/media/${actor.avatar.thumbnail}`"
:title="actor.avatar.copyright && `© ${actor.avatar.copyright}`"
class="avatar"
>
</a>
<span
v-show="expanded"
class="expand collapse-header noselect"
@click="expanded = false"
><Icon icon="arrow-up3" /></span>
<span
v-show="expanded"
class="expand collapse-header noselect"
@click="expanded = false"
><Icon icon="arrow-up3" /></span>
<ul class="bio nolist">
<li
v-if="actor.birthdate"
class="bio-item"
>
<dfn class="bio-label"><Icon icon="cake" />Birthdate</dfn>
<ul class="bio nolist">
<li
v-if="actor.birthdate"
class="bio-item"
>
<dfn class="bio-label"><Icon icon="cake" />Birthdate</dfn>
<span
v-if="actor.birthdate"
class="birthdate"
>{{ formatDate(actor.birthdate, 'MMMM D, YYYY') }}<span class="age">{{ actor.age }}</span></span>
</li>
<span
v-if="actor.birthdate"
class="birthdate"
>{{ formatDate(actor.birthdate, 'MMMM D, YYYY') }}<span class="age">{{ actor.age }}</span></span>
</li>
<li
v-if="actor.origin"
class="bio-item birth"
>
<dfn class="bio-label"><Icon icon="home2" />Born in</dfn>
<li
v-if="actor.origin"
class="bio-item birth"
>
<dfn class="bio-label"><Icon icon="home2" />Born in</dfn>
<span>
<span
v-if="actor.origin.city"
class="city hideable"
>{{ actor.origin.city }}</span><span
v-if="actor.origin.state && (!actor.origin.city || (actor.origin.country && actor.origin.country.alpha2 === 'US'))"
class="state hideable"
>{{ actor.origin.city ? `, ${actor.origin.state}` : actor.origin.state }}</span>
<span>
<span
v-if="actor.origin.city"
class="city hideable"
>{{ actor.origin.city }}</span><span
v-if="actor.origin.state && (!actor.origin.city || (actor.origin.country && actor.origin.country.alpha2 === 'US'))"
class="state hideable"
>{{ actor.origin.city ? `, ${actor.origin.state}` : actor.origin.state }}</span>
<span
v-if="actor.origin.country"
class="country birthcountry"
>
<img
class="flag"
:src="`/img/flags/svg-simple/${actor.origin.country.alpha2.toLowerCase()}.svg`"
>{{ actor.origin.country.alias || actor.origin.country.name }}
</span>
</span>
</li>
<span
v-if="actor.origin.country"
class="country birthcountry"
>
<img
class="flag"
:src="`/img/flags/svg-simple/${actor.origin.country.alpha2.toLowerCase()}.svg`"
>{{ actor.origin.country.alias || actor.origin.country.name }}
</span>
</span>
</li>
<li
v-if="actor.residence"
class="bio-item residence"
>
<dfn class="bio-label"><Icon icon="location" />Lives in</dfn>
<li
v-if="actor.residence"
class="bio-item residence"
>
<dfn class="bio-label"><Icon icon="location" />Lives in</dfn>
<span>
<span
v-if="actor.residence.city"
class="city hideable"
>{{ actor.residence.city }}</span><span
v-if="actor.residence.state && actor.residence.country && actor.residence.country.alpha2 === 'US'"
class="state hideable"
>{{ actor.residence.city ? `, ${actor.residence.state}` : actor.residence.state }}</span>
<span>
<span
v-if="actor.residence.city"
class="city hideable"
>{{ actor.residence.city }}</span><span
v-if="actor.residence.state && actor.residence.country && actor.residence.country.alpha2 === 'US'"
class="state hideable"
>{{ actor.residence.city ? `, ${actor.residence.state}` : actor.residence.state }}</span>
<span
v-if="actor.residence.country"
class="country"
>
<img
class="flag"
:src="`/img/flags/${actor.residence.country.alpha2.toLowerCase()}.png`"
>{{ actor.residence.country.alias || actor.residence.country.name }}
</span>
</span>
</li>
<span
v-if="actor.residence.country"
class="country"
>
<img
class="flag"
:src="`/img/flags/${actor.residence.country.alpha2.toLowerCase()}.png`"
>{{ actor.residence.country.alias || actor.residence.country.name }}
</span>
</span>
</li>
<li
v-if="actor.ethnicity"
class="bio-item ethnicity hideable"
>
<dfn class="bio-label"><Icon icon="earth2" />Ethnicity</dfn>
<span>{{ actor.ethnicity }}</span>
</li>
<li
v-if="actor.ethnicity"
class="bio-item ethnicity hideable"
>
<dfn class="bio-label"><Icon icon="earth2" />Ethnicity</dfn>
<span>{{ actor.ethnicity }}</span>
</li>
<li
v-if="actor.bust || actor.waist || actor.hip"
title="bust-waist-hip"
class="bio-item"
>
<dfn class="bio-label"><Icon icon="ruler" />Figure</dfn>
<span>
<Icon
v-if="actor.naturalBoobs === false"
v-tooltip="'Boobs enhanced'"
icon="magic-wand"
class="enhanced"
/>{{ actor.bust || '??' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}
</span>
</li>
<li
v-if="actor.bust || actor.waist || actor.hip"
title="bust-waist-hip"
class="bio-item"
>
<dfn class="bio-label"><Icon icon="ruler" />Figure</dfn>
<span>
<Icon
v-if="actor.naturalBoobs === false"
v-tooltip="'Boobs enhanced'"
icon="magic-wand"
class="enhanced"
/>{{ actor.bust || '??' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}
</span>
</li>
<li
v-if="actor.height"
class="bio-item height"
>
<dfn class="bio-label"><Icon icon="height" />Height</dfn>
<span>
<span class="height-metric">{{ actor.height.metric }} cm</span>
<span class="height-imperial">{{ actor.height.imperial }}</span>
</span>
</li>
<li
v-if="actor.height"
class="bio-item height"
>
<dfn class="bio-label"><Icon icon="height" />Height</dfn>
<span>
<span class="height-metric">{{ actor.height.metric }} cm</span>
<span class="height-imperial">{{ actor.height.imperial }}</span>
</span>
</li>
<li
v-if="actor.weight"
class="bio-item weight hideable"
>
<dfn class="bio-label"><Icon icon="scale" />Weight</dfn>
<li
v-if="actor.weight"
class="bio-item weight hideable"
>
<dfn class="bio-label"><Icon icon="scale" />Weight</dfn>
<span>
<span class="weight-metric">{{ actor.weight.metric }} kg</span>
<span class="weight-imperial">{{ actor.weight.imperial }} lbs</span>
</span>
</li>
<span>
<span class="weight-metric">{{ actor.weight.metric }} kg</span>
<span class="weight-imperial">{{ actor.weight.imperial }} lbs</span>
</span>
</li>
<li
v-if="actor.hasTattoos"
class="bio-item tattoos hideable"
>
<dfn class="bio-label"><Icon icon="flower" />Tattoos</dfn>
<li
v-if="actor.hasTattoos"
class="bio-item tattoos hideable"
>
<dfn class="bio-label"><Icon icon="flower" />Tattoos</dfn>
<span
v-if="actor.tattoos"
v-tooltip="actor.tattoos"
class="bio-value"
>{{ actor.tattoos }}</span>
<span v-else>Yes</span>
</li>
<span
v-if="actor.tattoos"
v-tooltip="actor.tattoos"
class="bio-value"
>{{ actor.tattoos }}</span>
<span v-else>Yes</span>
</li>
<li
v-if="actor.hasPiercings"
class="bio-item piercings hideable"
>
<dfn class="bio-label"><Icon icon="trophy4" />Piercings</dfn>
<li
v-if="actor.hasPiercings"
class="bio-item piercings hideable"
>
<dfn class="bio-label"><Icon icon="trophy4" />Piercings</dfn>
<span
v-if="actor.piercings"
v-tooltip="actor.piercings"
class="bio-value"
>{{ actor.piercings }}</span>
<span v-else>Yes</span>
</li>
<span
v-if="actor.piercings"
v-tooltip="actor.piercings"
class="bio-value"
>{{ actor.piercings }}</span>
<span v-else>Yes</span>
</li>
<li class="bio-item scraped hideable">Updated {{ formatDate(actor.scrapedAt, 'YYYY-MM-DD HH:mm') }}, ID: {{ actor.id }}</li>
</ul>
<li class="bio-item scraped hideable">Updated {{ formatDate(actor.updatedAt, 'YYYY-MM-DD HH:mm') }}, ID: {{ actor.id }}</li>
</ul>
<span
v-show="!expanded"
class="expand expand-header collapse-header noselect"
@click="expanded = true"
><Icon icon="arrow-down3" /></span>
<span
v-show="!expanded"
class="expand expand-header collapse-header noselect"
@click="expanded = true"
><Icon icon="arrow-down3" /></span>
<p
v-if="actor.description"
class="description"
>{{ actor.description }}</p>
<p
v-if="actor.description"
class="description"
>{{ actor.description }}</p>
<Social
v-if="actor.social && actor.social.length > 0"
:actor="actor"
class="profile-social"
/>
<Social
v-if="actor.social && actor.social.length > 0"
:actor="actor"
class="profile-social"
/>
<span
v-show="expanded"
class="expand expand-header collapse-header noselect"
@click="expanded = false"
><Icon icon="arrow-up3" /></span>
</div>
<span
v-show="expanded"
class="expand expand-header collapse-header noselect"
@click="expanded = false"
><Icon icon="arrow-up3" /></span>
</div>
<div class="actor-content">
<div
v-if="actor.avatar || (actor.photos && actor.photos.length > 0)"
class="photos-container"
>
<Photos :actor="actor" />
<div class="actor-content">
<div
v-if="actor.avatar || (actor.photos && actor.photos.length > 0)"
class="photos-container"
>
<Photos :actor="actor" />
<Photos
:actor="actor"
:class="{ expanded }"
class="compact"
/>
</div>
<Photos
:actor="actor"
:class="{ expanded }"
class="compact"
/>
</div>
<Releases :releases="actor.releases" />
</div>
</div>
</div>
<Releases :releases="actor.releases" />
</div>
</div>
</div>
</template>
<script>
@ -253,52 +253,52 @@ import Gender from './gender.vue';
import Social from './social.vue';
async function fetchActor() {
this.actor = await this.$store.dispatch('fetchActorBySlug', {
actorSlug: this.$route.params.actorSlug,
range: this.$route.params.range,
});
this.actor = await this.$store.dispatch('fetchActorBySlug', {
actorSlug: this.$route.params.actorSlug,
range: this.$route.params.range,
});
}
async function route() {
await this.fetchActor();
await this.fetchActor();
}
function scrollPhotos(event) {
event.currentTarget.scrollLeft += event.deltaY; // eslint-disable-line no-param-reassign
event.currentTarget.scrollLeft += event.deltaY; // eslint-disable-line no-param-reassign
}
async function mounted() {
await this.fetchActor();
await this.fetchActor();
if (this.actor) {
this.pageTitle = this.actor.name;
}
if (this.actor) {
this.pageTitle = this.actor.name;
}
}
export default {
components: {
FilterBar,
Photos,
Releases,
Gender,
Social,
},
data() {
return {
actor: null,
releases: null,
pageTitle: null,
expanded: false,
};
},
watch: {
$route: route,
},
mounted,
methods: {
fetchActor,
scrollPhotos,
},
components: {
FilterBar,
Photos,
Releases,
Gender,
Social,
},
data() {
return {
actor: null,
releases: null,
pageTitle: null,
expanded: false,
};
},
watch: {
$route: route,
},
mounted,
methods: {
fetchActor,
scrollPhotos,
},
};
</script>
@ -426,7 +426,7 @@ export default {
}
.bio-value {
margin: 0 0 0 2rem;
margin: 0 0 0 2rem;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;

View File

@ -1,124 +1,124 @@
<template>
<div
v-if="network"
class="content"
>
<FilterBar :fetch-releases="fetchNetwork" />
<div
v-if="network"
class="content"
>
<FilterBar :fetch-releases="fetchNetwork" />
<div
class="network"
:class="{ nosites: sites.length === 0 && networks.length === 0 }"
>
<div
v-show="sites.length > 0 || networks.length > 0"
class="sidebar"
:class="{ expanded }"
>
<a
v-tooltip.bottom="`Go to ${network.url}`"
:href="network.url"
target="_blank"
rel="noopener noreferrer"
class="title"
>
<img
:src="`/img/logos/${network.slug}/network.png`"
class="logo"
>
</a>
<div
class="network"
:class="{ nosites: sites.length === 0 && networks.length === 0 }"
>
<div
v-show="sites.length > 0 || networks.length > 0"
class="sidebar"
:class="{ expanded }"
>
<a
v-tooltip.bottom="`Go to ${network.url}`"
:href="network.url"
target="_blank"
rel="noopener noreferrer"
class="title"
>
<img
:src="`/img/logos/${network.slug}/thumbs/network.png`"
class="logo"
>
</a>
<p
v-if="network.description"
class="description"
>{{ network.description }}</p>
<p
v-if="network.description"
class="description"
>{{ network.description }}</p>
<Sites
v-if="sites.length"
:sites="sites"
:class="{ expanded }"
/>
<Sites
v-if="sites.length"
:sites="sites"
:class="{ expanded }"
/>
<div
v-if="networks.length > 0"
class="networks"
>
<Network
v-for="childNetwork in networks"
:key="`network-${childNetwork.id}`"
:network="childNetwork"
/>
</div>
<div
v-if="networks.length > 0"
class="networks"
>
<Network
v-for="childNetwork in networks"
:key="`network-${childNetwork.id}`"
:network="childNetwork"
/>
</div>
<Network
v-if="network.parent"
:network="network.parent"
class="parent"
/>
</div>
<Network
v-if="network.parent"
:network="network.parent"
class="parent"
/>
</div>
<template v-if="sites.length > 0 || networks.length > 0">
<span
v-show="!expanded"
class="expand expand-sidebar noselect"
@click="expanded = true"
><Icon icon="arrow-right3" /></span>
<template v-if="sites.length > 0 || networks.length > 0">
<span
v-show="!expanded"
class="expand expand-sidebar noselect"
@click="expanded = true"
><Icon icon="arrow-right3" /></span>
<span
v-show="expanded"
class="expand expand-sidebar noselect"
@click="expanded = false"
><Icon icon="arrow-left3" /></span>
</template>
<span
v-show="expanded"
class="expand expand-sidebar noselect"
@click="expanded = false"
><Icon icon="arrow-left3" /></span>
</template>
<div
class="header"
:class="{ hideable: sites.length > 0 || networks.length > 0 }"
>
<a
v-tooltip.bottom="`Go to ${network.url}`"
:href="network.url"
target="_blank"
rel="noopener noreferrer"
class="title"
>
<img
:src="`/img/logos/${network.slug}/network.png`"
class="logo"
>
</a>
</div>
<div
class="header"
:class="{ hideable: sites.length > 0 || networks.length > 0 }"
>
<a
v-tooltip.bottom="`Go to ${network.url}`"
:href="network.url"
target="_blank"
rel="noopener noreferrer"
class="title"
>
<img
:src="`/img/logos/${network.slug}/thumbs/network.png`"
class="logo"
>
</a>
</div>
<div class="content-inner">
<template v-if="sites.length > 0 || networks.length > 0">
<span
v-show="expanded"
class="expand collapse-header noselect"
@click="expanded = false"
><Icon icon="arrow-up3" /></span>
<div class="content-inner">
<template v-if="sites.length > 0 || networks.length > 0">
<span
v-show="expanded"
class="expand collapse-header noselect"
@click="expanded = false"
><Icon icon="arrow-up3" /></span>
<Sites
:sites="sites"
:class="{ expanded }"
class="compact"
/>
<Sites
:sites="sites"
:class="{ expanded }"
class="compact"
/>
<span
v-show="!expanded"
class="expand expand-header noselect"
@click="expanded = true"
><Icon icon="arrow-down3" /></span>
<span
v-show="!expanded"
class="expand expand-header noselect"
@click="expanded = true"
><Icon icon="arrow-down3" /></span>
<span
v-show="expanded"
class="expand expand-header noselect"
@click="expanded = false"
><Icon icon="arrow-up3" /></span>
</template>
<span
v-show="expanded"
class="expand expand-header noselect"
@click="expanded = false"
><Icon icon="arrow-up3" /></span>
</template>
<Releases :releases="releases" />
</div>
</div>
</div>
<Releases :releases="releases" />
</div>
</div>
</div>
</template>
<script>
@ -128,59 +128,59 @@ import Sites from '../sites/sites.vue';
import Network from '../tile/network.vue';
async function fetchNetwork() {
this.network = await this.$store.dispatch('fetchNetworkBySlug', {
networkSlug: this.$route.params.networkSlug,
range: this.$route.params.range,
});
this.network = await this.$store.dispatch('fetchNetworkBySlug', {
networkSlug: this.$route.params.networkSlug,
range: this.$route.params.range,
});
if (this.network.studios) {
this.studios = this.network.studios.map(studio => ({
...studio,
network: this.network,
}));
}
if (this.network.studios) {
this.studios = this.network.studios.map(studio => ({
...studio,
network: this.network,
}));
}
this.networks = this.network.networks;
this.sites = this.network.sites
.filter(site => !site.independent);
this.networks = this.network.networks;
this.sites = this.network.sites
.filter(site => !site.independent);
this.releases = this.network.releases;
this.releases = this.network.releases;
}
async function route() {
await this.fetchNetwork();
await this.fetchNetwork();
}
async function mounted() {
await this.fetchNetwork();
this.pageTitle = this.network.name;
await this.fetchNetwork();
this.pageTitle = this.network.name;
}
export default {
components: {
FilterBar,
Releases,
Sites,
Network,
},
data() {
return {
network: null,
sites: [],
networks: [],
studios: [],
releases: [],
pageTitle: null,
expanded: false,
};
},
watch: {
$route: route,
},
mounted,
methods: {
fetchNetwork,
},
components: {
FilterBar,
Releases,
Sites,
Network,
},
data() {
return {
network: null,
sites: [],
networks: [],
studios: [],
releases: [],
pageTitle: null,
expanded: false,
};
},
watch: {
$route: route,
},
mounted,
methods: {
fetchNetwork,
},
};
</script>

View File

@ -1,25 +1,25 @@
<template>
<a
:href="`/site/${site.slug}`"
:title="site.name"
class="tile"
>
<img
:src="`/img/logos/${site.network.slug}/${site.slug}.png`"
:alt="site.name"
class="logo"
>
</a>
<a
:href="`/site/${site.slug}`"
:title="site.name"
class="tile"
>
<img
:src="`/img/logos/${site.network.slug}/thumbs/${site.slug}.png`"
:alt="site.name"
class="logo"
>
</a>
</template>
<script>
export default {
props: {
site: {
type: Object,
default: null,
},
},
props: {
site: {
type: Object,
default: null,
},
},
};
</script>

View File

@ -1,59 +1,61 @@
import { graphql, get } from '../api';
import {
releasePosterFragment,
releaseActorsFragment,
releaseTagsFragment,
releasePosterFragment,
releaseActorsFragment,
releaseTagsFragment,
} from '../fragments';
import { curateRelease } from '../curate';
import getDateRange from '../get-date-range';
function curateActor(actor) {
if (!actor) {
return null;
}
if (!actor) {
return null;
}
const curatedActor = {
...actor,
height: actor.heightMetric && {
metric: actor.heightMetric,
imperial: actor.heightImperial,
},
weight: actor.weightMetric && {
metric: actor.weightMetric,
imperial: actor.weightImperial,
},
origin: actor.birthCountry && {
city: actor.birthCity,
state: actor.birthState,
country: actor.birthCountry,
},
residence: actor.residenceCountry && {
city: actor.residenceCity,
state: actor.residenceState,
country: actor.residenceCountry,
},
};
const curatedActor = {
...actor,
height: actor.heightMetric && {
metric: actor.heightMetric,
imperial: actor.heightImperial,
},
weight: actor.weightMetric && {
metric: actor.weightMetric,
imperial: actor.weightImperial,
},
origin: actor.birthCountry && {
city: actor.birthCity,
state: actor.birthState,
country: actor.birthCountry,
},
residence: actor.residenceCountry && {
city: actor.residenceCity,
state: actor.residenceState,
country: actor.residenceCountry,
},
scrapedAt: new Date(actor.createdAt),
updatedAt: new Date(actor.updatedAt),
};
if (actor.avatar) {
curatedActor.avatar = actor.avatar.media;
}
if (actor.avatar) {
curatedActor.avatar = actor.avatar.media;
}
if (actor.releases) {
curatedActor.releases = actor.releases.map(release => curateRelease(release.release));
}
if (actor.releases) {
curatedActor.releases = actor.releases.map(release => curateRelease(release.release));
}
if (actor.photos) {
curatedActor.photos = actor.photos.map(photo => photo.media);
}
if (actor.photos) {
curatedActor.photos = actor.photos.map(photo => photo.media);
}
return curatedActor;
return curatedActor;
}
function initActorActions(store, _router) {
async function fetchActorBySlug({ _commit }, { actorSlug, limit = 100, range = 'latest' }) {
const { before, after, orderBy } = getDateRange(range);
async function fetchActorBySlug({ _commit }, { actorSlug, limit = 100, range = 'latest' }) {
const { before, after, orderBy } = getDateRange(range);
const { actors: [actor] } = await graphql(`
const { actors: [actor] } = await graphql(`
query Actor(
$actorSlug: String!
$limit:Int = 1000,
@ -90,6 +92,8 @@ function initActorActions(store, _router) {
tattoos
piercings
description
createdAt
updatedAt
network {
id
name
@ -184,27 +188,27 @@ function initActorActions(store, _router) {
}
}
`, {
actorSlug,
limit,
after,
before,
orderBy: orderBy === 'DATE_DESC' ? 'RELEASE_BY_RELEASE_ID__DATE_DESC' : 'RELEASE_BY_RELEASE_ID__DATE_ASC',
exclude: store.state.ui.filter,
});
actorSlug,
limit,
after,
before,
orderBy: orderBy === 'DATE_DESC' ? 'RELEASE_BY_RELEASE_ID__DATE_DESC' : 'RELEASE_BY_RELEASE_ID__DATE_ASC',
exclude: store.state.ui.filter,
});
return curateActor(actor);
}
return curateActor(actor);
}
async function fetchActors({ _commit }, {
limit = 100,
letter,
gender,
}) {
const genderFilter = gender === null
? 'isNull: true'
: `equalTo: "${gender}"`;
async function fetchActors({ _commit }, {
limit = 100,
letter,
gender,
}) {
const genderFilter = gender === null
? 'isNull: true'
: `equalTo: "${gender}"`;
const { actors } = await graphql(`
const { actors } = await graphql(`
query Actors(
$limit: Int,
$letter: String! = "",
@ -249,28 +253,28 @@ function initActorActions(store, _router) {
}
}
`, {
limit,
letter,
});
limit,
letter,
});
return actors.map(actor => curateActor(actor));
}
return actors.map(actor => curateActor(actor));
}
async function fetchActorReleases({ _commit }, actorId) {
const releases = await get(`/actors/${actorId}/releases`, {
filter: store.state.ui.filter,
after: store.getters.after,
before: store.getters.before,
});
async function fetchActorReleases({ _commit }, actorId) {
const releases = await get(`/actors/${actorId}/releases`, {
filter: store.state.ui.filter,
after: store.getters.after,
before: store.getters.before,
});
return releases;
}
return releases;
}
return {
fetchActorBySlug,
fetchActors,
fetchActorReleases,
};
return {
fetchActorBySlug,
fetchActors,
fetchActorReleases,
};
}
export default initActorActions;

View File

@ -3,11 +3,11 @@ import mutations from './mutations';
import actions from './actions';
function initActorsStore(store, router) {
return {
state,
mutations,
actions: actions(store, router),
};
return {
state,
mutations,
actions: actions(store, router),
};
}
export default initActorsStore;

View File

@ -1,71 +1,71 @@
import config from 'config';
async function get(endpoint, query = {}) {
const curatedQuery = Object.entries(query).reduce((acc, [key, value]) => (value ? { ...acc, [key]: value } : acc), {}); // remove empty values
const q = new URLSearchParams(curatedQuery).toString();
const curatedQuery = Object.entries(query).reduce((acc, [key, value]) => (value ? { ...acc, [key]: value } : acc), {}); // remove empty values
const q = new URLSearchParams(curatedQuery).toString();
const res = await fetch(`${config.api.url}${endpoint}?${q}`, {
method: 'GET',
mode: 'cors',
credentials: 'same-origin',
});
const res = await fetch(`${config.api.url}${endpoint}?${q}`, {
method: 'GET',
mode: 'cors',
credentials: 'same-origin',
});
if (res.ok) {
return res.json();
}
if (res.ok) {
return res.json();
}
const errorMsg = await res.text();
const errorMsg = await res.text();
throw new Error(errorMsg);
throw new Error(errorMsg);
}
async function post(endpoint, data) {
const res = await fetch(`${config.api.url}${endpoint}`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify(data),
});
const res = await fetch(`${config.api.url}${endpoint}`, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify(data),
});
if (res.ok) {
return res.json();
}
if (res.ok) {
return res.json();
}
const errorMsg = await res.text();
const errorMsg = await res.text();
throw new Error(errorMsg);
throw new Error(errorMsg);
}
async function graphql(query, variables = null) {
const res = await fetch('/graphql', {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify({
query,
variables,
}),
});
const res = await fetch('/graphql', {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify({
query,
variables,
}),
});
if (res.ok) {
const { data } = await res.json();
if (res.ok) {
const { data } = await res.json();
return data;
}
return data;
}
const errorMsg = await res.text();
const errorMsg = await res.text();
throw new Error(errorMsg);
throw new Error(errorMsg);
}
export {
get,
post,
graphql,
get,
post,
graphql,
};

View File

@ -3,11 +3,11 @@ import mutations from './mutations';
import actions from './actions';
function initAuthStore(store, router) {
return {
state,
mutations,
actions: actions(store, router),
};
return {
state,
mutations,
actions: actions(store, router),
};
}
export default initAuthStore;

View File

@ -1,4 +1,4 @@
export default {
authenticated: false,
user: null,
authenticated: false,
user: null,
};

View File

@ -1,10 +1,10 @@
export default {
api: {
url: `${window.location.origin}/api`,
},
filename: {
pattern: '{site.name} - {title} ({actors.$n.name}, {date} {shootId})',
separator: ', ',
date: 'DD-MM-YYYY',
},
api: {
url: `${window.location.origin}/api`,
},
filename: {
pattern: '{site.name} - {title} ({actors.$n.name}, {date} {shootId})',
separator: ', ',
date: 'DD-MM-YYYY',
},
};

View File

@ -1,94 +1,94 @@
import dayjs from 'dayjs';
function curateActor(actor, release) {
const curatedActor = {
...actor,
origin: actor.originCountry && {
country: actor.originCountry,
},
};
const curatedActor = {
...actor,
origin: actor.originCountry && {
country: actor.originCountry,
},
};
if (actor.avatar) curatedActor.avatar = actor.avatar.media;
if (actor.avatar) curatedActor.avatar = actor.avatar.media;
if (release && release.date && curatedActor.birthdate) {
curatedActor.ageThen = dayjs(release.date).diff(actor.birthdate, 'year');
}
if (release && release.date && curatedActor.birthdate) {
curatedActor.ageThen = dayjs(release.date).diff(actor.birthdate, 'year');
}
return curatedActor;
return curatedActor;
}
function curateRelease(release) {
const curatedRelease = {
...release,
actors: [],
poster: release.poster && release.poster.media,
tags: release.tags ? release.tags.map(({ tag }) => tag) : [],
};
const curatedRelease = {
...release,
actors: [],
poster: release.poster && release.poster.media,
tags: release.tags ? release.tags.map(({ tag }) => tag) : [],
};
if (release.site) curatedRelease.network = release.site.network;
if (release.scenes) curatedRelease.scenes = release.scenes.map(({ scene }) => curateRelease(scene));
if (release.movies) curatedRelease.movies = release.movies.map(({ movie }) => curateRelease(movie));
if (release.photos) curatedRelease.photos = release.photos.map(({ media }) => media);
if (release.covers) curatedRelease.covers = release.covers.map(({ media }) => media);
if (release.trailer) curatedRelease.trailer = release.trailer.media;
if (release.teaser) curatedRelease.teaser = release.teaser.media;
if (release.actors) curatedRelease.actors = release.actors.map(({ actor }) => curateActor(actor, curatedRelease));
if (release.movieTags && release.movieTags.length > 0) curatedRelease.tags = release.movieTags.map(({ tag }) => tag);
if (release.movieActors && release.movieActors.length > 0) curatedRelease.actors = release.movieActors.map(({ actor }) => curateActor(actor, curatedRelease));
if (release.site) curatedRelease.network = release.site.network;
if (release.scenes) curatedRelease.scenes = release.scenes.map(({ scene }) => curateRelease(scene));
if (release.movies) curatedRelease.movies = release.movies.map(({ movie }) => curateRelease(movie));
if (release.photos) curatedRelease.photos = release.photos.map(({ media }) => media);
if (release.covers) curatedRelease.covers = release.covers.map(({ media }) => media);
if (release.trailer) curatedRelease.trailer = release.trailer.media;
if (release.teaser) curatedRelease.teaser = release.teaser.media;
if (release.actors) curatedRelease.actors = release.actors.map(({ actor }) => curateActor(actor, curatedRelease));
if (release.movieTags && release.movieTags.length > 0) curatedRelease.tags = release.movieTags.map(({ tag }) => tag);
if (release.movieActors && release.movieActors.length > 0) curatedRelease.actors = release.movieActors.map(({ actor }) => curateActor(actor, curatedRelease));
return curatedRelease;
return curatedRelease;
}
function curateSite(site, network) {
const curatedSite = {
id: site.id,
name: site.name,
slug: site.slug,
url: site.url,
independent: site.independent,
};
const curatedSite = {
id: site.id,
name: site.name,
slug: site.slug,
url: site.url,
independent: site.independent,
};
if (site.releases) curatedSite.releases = site.releases.map(release => curateRelease(release));
if (site.network || network) curatedSite.network = site.network || network;
if (site.tags) curatedSite.tags = site.tags.map(({ tag }) => tag);
if (site.releases) curatedSite.releases = site.releases.map(release => curateRelease(release));
if (site.network || network) curatedSite.network = site.network || network;
if (site.tags) curatedSite.tags = site.tags.map(({ tag }) => tag);
return curatedSite;
return curatedSite;
}
function curateNetwork(network, releases) {
const curatedNetwork = {
id: network.id,
name: network.name,
slug: network.slug,
url: network.url,
networks: [],
};
const curatedNetwork = {
id: network.id,
name: network.name,
slug: network.slug,
url: network.url,
networks: [],
};
if (network.parent) curatedNetwork.parent = curateNetwork(network.parent);
if (network.sites) curatedNetwork.sites = network.sites.map(site => curateSite(site, curatedNetwork));
if (network.networks) curatedNetwork.networks = network.networks.map(subNetwork => curateNetwork(subNetwork));
if (network.studios) curatedNetwork.studios = network.studios;
if (releases) curatedNetwork.releases = releases.map(release => curateRelease(release));
if (network.parent) curatedNetwork.parent = curateNetwork(network.parent);
if (network.sites) curatedNetwork.sites = network.sites.map(site => curateSite(site, curatedNetwork));
if (network.networks) curatedNetwork.networks = network.networks.map(subNetwork => curateNetwork(subNetwork));
if (network.studios) curatedNetwork.studios = network.studios;
if (releases) curatedNetwork.releases = releases.map(release => curateRelease(release));
return curatedNetwork;
return curatedNetwork;
}
function curateTag(tag) {
const curatedTag = {
...tag,
};
const curatedTag = {
...tag,
};
if (tag.releases) curatedTag.releases = tag.releases.map(({ release }) => curateRelease(release));
if (tag.photos) curatedTag.photos = tag.photos.map(({ media }) => media);
if (tag.poster) curatedTag.poster = tag.poster.media;
if (tag.releases) curatedTag.releases = tag.releases.map(({ release }) => curateRelease(release));
if (tag.photos) curatedTag.photos = tag.photos.map(({ media }) => media);
if (tag.poster) curatedTag.poster = tag.poster.media;
return curatedTag;
return curatedTag;
}
export {
curateActor,
curateRelease,
curateSite,
curateNetwork,
curateTag,
curateActor,
curateRelease,
curateSite,
curateNetwork,
curateTag,
};

View File

@ -278,14 +278,14 @@ const releaseFragment = `
`;
export {
releaseActorsFragment,
releaseFields,
releaseTagsFragment,
releasePosterFragment,
releasePhotosFragment,
releaseTrailerFragment,
releasesFragment,
releaseFragment,
siteFragment,
sitesFragment,
releaseActorsFragment,
releaseFields,
releaseTagsFragment,
releasePosterFragment,
releasePhotosFragment,
releaseTrailerFragment,
releasesFragment,
releaseFragment,
siteFragment,
sitesFragment,
};

View File

@ -1,30 +1,30 @@
import dayjs from 'dayjs';
const dateRanges = {
latest: () => ({
after: '1900-01-01',
before: dayjs(new Date()).add(1, 'day').format('YYYY-MM-DD'),
orderBy: 'DATE_DESC',
}),
upcoming: () => ({
after: dayjs(new Date()).format('YYYY-MM-DD'),
before: '2100-01-01',
orderBy: 'DATE_ASC',
}),
new: () => ({
after: '1900-01-01',
before: '2100-01-01',
orderBy: 'CREATED_AT_DESC',
}),
all: () => ({
after: '1900-01-01',
before: '2100-01-01',
orderBy: 'DATE_DESC',
}),
latest: () => ({
after: '1900-01-01',
before: dayjs(new Date()).add(1, 'day').format('YYYY-MM-DD'),
orderBy: 'DATE_DESC',
}),
upcoming: () => ({
after: dayjs(new Date()).format('YYYY-MM-DD'),
before: '2100-01-01',
orderBy: 'DATE_ASC',
}),
new: () => ({
after: '1900-01-01',
before: '2100-01-01',
orderBy: 'CREATED_AT_DESC',
}),
all: () => ({
after: '1900-01-01',
before: '2100-01-01',
orderBy: 'DATE_DESC',
}),
};
function getDateRange(range) {
return dateRanges[range]();
return dateRanges[range]();
}
export default getDateRange;

View File

@ -14,48 +14,48 @@ import Container from '../components/container/container.vue';
import Icon from '../components/icon/icon.vue';
function init() {
const store = initStore(router);
const store = initStore(router);
initUiObservers(store, router);
initUiObservers(store, router);
if (window.env.sfw) {
store.dispatch('setSfw', true);
}
if (window.env.sfw) {
store.dispatch('setSfw', true);
}
Vue.mixin({
components: {
Icon,
},
watch: {
pageTitle(title) {
if (title) {
document.title = `traxxx - ${title}`;
return;
}
Vue.mixin({
components: {
Icon,
},
watch: {
pageTitle(title) {
if (title) {
document.title = `traxxx - ${title}`;
return;
}
document.title = 'traxxx';
},
},
methods: {
formatDate: (date, format) => dayjs(date).format(format),
isAfter: (dateA, dateB) => dayjs(dateA).isAfter(dateB),
isBefore: (dateA, dateB) => dayjs(dateA).isBefore(dateB),
},
});
document.title = 'traxxx';
},
},
methods: {
formatDate: (date, format) => dayjs(date).format(format),
isAfter: (dateA, dateB) => dayjs(dateA).isAfter(dateB),
isBefore: (dateA, dateB) => dayjs(dateA).isBefore(dateB),
},
});
Vue.use(VTooltip);
Vue.use(VueLazyLoad, {
throttleWait: 0,
});
Vue.use(VTooltip);
Vue.use(VueLazyLoad, {
throttleWait: 0,
});
new Vue({ // eslint-disable-line no-new
el: '#container',
store,
router,
render(createElement) {
return createElement(Container);
},
});
new Vue({ // eslint-disable-line no-new
el: '#container',
store,
router,
render(createElement) {
return createElement(Container);
},
});
}
init();

View File

@ -4,10 +4,10 @@ import { curateNetwork } from '../curate';
import getDateRange from '../get-date-range';
function initNetworksActions(store, _router) {
async function fetchNetworkBySlug({ _commit }, { networkSlug, limit = 100, range = 'latest' }) {
const { before, after, orderBy } = getDateRange(range);
async function fetchNetworkBySlug({ _commit }, { networkSlug, limit = 100, range = 'latest' }) {
const { before, after, orderBy } = getDateRange(range);
const { network, releases } = await graphql(`
const { network, releases } = await graphql(`
query Network(
$networkSlug: String!
$limit:Int = 1000,
@ -107,21 +107,21 @@ function initNetworksActions(store, _router) {
}
}
`, {
networkSlug,
limit,
after,
before,
orderBy,
afterTime: store.getters.after,
beforeTime: store.getters.before,
exclude: store.state.ui.filter,
});
networkSlug,
limit,
after,
before,
orderBy,
afterTime: store.getters.after,
beforeTime: store.getters.before,
exclude: store.state.ui.filter,
});
return curateNetwork(network, releases);
}
return curateNetwork(network, releases);
}
async function fetchNetworks({ _commit }) {
const { networks } = await graphql(`
async function fetchNetworks({ _commit }) {
const { networks } = await graphql(`
query Networks {
networks(orderBy: NAME_ASC) {
id
@ -133,13 +133,13 @@ function initNetworksActions(store, _router) {
}
`);
return networks.map(network => curateNetwork(network));
}
return networks.map(network => curateNetwork(network));
}
return {
fetchNetworkBySlug,
fetchNetworks,
};
return {
fetchNetworkBySlug,
fetchNetworks,
};
}
export default initNetworksActions;

View File

@ -3,11 +3,11 @@ import mutations from './mutations';
import actions from './actions';
function initNetworksStore(store, router) {
return {
state,
mutations,
actions: actions(store, router),
};
return {
state,
mutations,
actions: actions(store, router),
};
}
export default initNetworksStore;

View File

@ -4,10 +4,10 @@ import { curateRelease } from '../curate';
import getDateRange from '../get-date-range';
function initReleasesActions(store, _router) {
async function fetchReleases({ _commit }, { limit = 100, range = 'latest' }) {
const { before, after, orderBy } = getDateRange(range);
async function fetchReleases({ _commit }, { limit = 100, range = 'latest' }) {
const { before, after, orderBy } = getDateRange(range);
const { releases } = await graphql(`
const { releases } = await graphql(`
query Releases(
$limit:Int = 1000,
$after:Date = "1900-01-01",
@ -18,18 +18,18 @@ function initReleasesActions(store, _router) {
${releasesFragment}
}
`, {
limit,
after,
before,
orderBy,
exclude: store.state.ui.filter,
});
limit,
after,
before,
orderBy,
exclude: store.state.ui.filter,
});
return releases.map(release => curateRelease(release));
}
return releases.map(release => curateRelease(release));
}
async function searchReleases({ _commit }, { query, limit = 20 }) {
const res = await graphql(`
async function searchReleases({ _commit }, { query, limit = 20 }) {
const res = await graphql(`
query SearchReleases(
$query: String!
$limit: Int = 20
@ -88,34 +88,34 @@ function initReleasesActions(store, _router) {
}
}
`, {
query,
limit,
});
query,
limit,
});
if (!res) return [];
if (!res) return [];
return res.releases.map(release => curateRelease(release));
}
return res.releases.map(release => curateRelease(release));
}
async function fetchReleaseById({ _commit }, releaseId) {
// const release = await get(`/releases/${releaseId}`);
async function fetchReleaseById({ _commit }, releaseId) {
// const release = await get(`/releases/${releaseId}`);
const { release } = await graphql(`
const { release } = await graphql(`
query Release($releaseId:Int!) {
${releaseFragment}
}
`, {
releaseId: Number(releaseId),
});
releaseId: Number(releaseId),
});
return curateRelease(release);
}
return curateRelease(release);
}
return {
fetchReleases,
fetchReleaseById,
searchReleases,
};
return {
fetchReleases,
fetchReleaseById,
searchReleases,
};
}
export default initReleasesActions;

View File

@ -1,14 +1,14 @@
import Vue from 'vue';
function setCache(state, { target, releases }) {
Vue.set(state.cache, target, releases);
Vue.set(state.cache, target, releases);
}
function deleteCache(state, target) {
Vue.delete(state.cache, target);
Vue.delete(state.cache, target);
}
export default {
setCache,
deleteCache,
setCache,
deleteCache,
};

View File

@ -3,11 +3,11 @@ import mutations from './mutations';
import actions from './actions';
function initReleasesStore(store, router) {
return {
state,
mutations,
actions: actions(store, router),
};
return {
state,
mutations,
actions: actions(store, router),
};
}
export default initReleasesStore;

View File

@ -1,3 +1,3 @@
export default {
cache: {},
cache: {},
};

View File

@ -16,139 +16,139 @@ import NotFound from '../components/errors/404.vue';
Vue.use(VueRouter);
const routes = [
{
path: '/',
redirect: {
name: 'latest',
},
},
{
path: '/home',
redirect: {
name: 'latest',
},
},
{
path: '/latest',
component: Home,
name: 'latest',
},
{
path: '/upcoming',
component: Home,
name: 'upcoming',
},
{
path: '/new',
component: Home,
name: 'new',
},
{
path: '/scene/:releaseId/:releaseSlug?',
component: Release,
name: 'scene',
},
{
path: '/movie/:releaseId/:releaseSlug?',
component: Release,
name: 'movie',
},
{
path: '/actor/:actorSlug',
name: 'actor',
redirect: from => ({
name: 'actorRange',
params: {
...from.params,
range: 'latest',
},
}),
},
{
path: '/actor/:actorSlug/:range',
component: Actor,
name: 'actorRange',
},
{
path: '/site/:siteSlug',
component: Site,
name: 'site',
redirect: from => ({
name: 'siteRange',
params: {
...from.params,
range: 'latest',
},
}),
},
{
path: '/site/:siteSlug/:range',
component: Site,
name: 'siteRange',
},
{
path: '/network/:networkSlug',
component: Network,
name: 'network',
redirect: from => ({
name: 'networkRange',
params: {
...from.params,
range: 'latest',
},
}),
},
{
path: '/network/:networkSlug/:range',
component: Network,
name: 'networkRange',
},
{
path: '/tag/:tagSlug',
component: Tag,
name: 'tag',
redirect: from => ({
name: 'tagRange',
params: {
...from.params,
range: 'latest',
},
}),
},
{
path: '/tag/:tagSlug/:range',
component: Tag,
name: 'tagRange',
},
{
path: '/actors/:gender?/:letter?',
component: Actors,
name: 'actors',
},
{
path: '/networks',
component: Networks,
name: 'networks',
},
{
path: '/tags',
component: Tags,
name: 'tags',
},
{
path: '/search',
component: Search,
name: 'search',
},
{
path: '*',
component: NotFound,
},
{
path: '/',
redirect: {
name: 'latest',
},
},
{
path: '/home',
redirect: {
name: 'latest',
},
},
{
path: '/latest',
component: Home,
name: 'latest',
},
{
path: '/upcoming',
component: Home,
name: 'upcoming',
},
{
path: '/new',
component: Home,
name: 'new',
},
{
path: '/scene/:releaseId/:releaseSlug?',
component: Release,
name: 'scene',
},
{
path: '/movie/:releaseId/:releaseSlug?',
component: Release,
name: 'movie',
},
{
path: '/actor/:actorSlug',
name: 'actor',
redirect: from => ({
name: 'actorRange',
params: {
...from.params,
range: 'latest',
},
}),
},
{
path: '/actor/:actorSlug/:range',
component: Actor,
name: 'actorRange',
},
{
path: '/site/:siteSlug',
component: Site,
name: 'site',
redirect: from => ({
name: 'siteRange',
params: {
...from.params,
range: 'latest',
},
}),
},
{
path: '/site/:siteSlug/:range',
component: Site,
name: 'siteRange',
},
{
path: '/network/:networkSlug',
component: Network,
name: 'network',
redirect: from => ({
name: 'networkRange',
params: {
...from.params,
range: 'latest',
},
}),
},
{
path: '/network/:networkSlug/:range',
component: Network,
name: 'networkRange',
},
{
path: '/tag/:tagSlug',
component: Tag,
name: 'tag',
redirect: from => ({
name: 'tagRange',
params: {
...from.params,
range: 'latest',
},
}),
},
{
path: '/tag/:tagSlug/:range',
component: Tag,
name: 'tagRange',
},
{
path: '/actors/:gender?/:letter?',
component: Actors,
name: 'actors',
},
{
path: '/networks',
component: Networks,
name: 'networks',
},
{
path: '/tags',
component: Tags,
name: 'tags',
},
{
path: '/search',
component: Search,
name: 'search',
},
{
path: '*',
component: NotFound,
},
];
const router = new VueRouter({
mode: 'history',
routes,
mode: 'history',
routes,
});
export default router;

View File

@ -4,10 +4,10 @@ import { curateSite } from '../curate';
import getDateRange from '../get-date-range';
function initSitesActions(store, _router) {
async function fetchSiteBySlug({ _commit }, { siteSlug, limit = 100, range = 'latest' }) {
const { before, after, orderBy } = getDateRange(range);
async function fetchSiteBySlug({ _commit }, { siteSlug, limit = 100, range = 'latest' }) {
const { before, after, orderBy } = getDateRange(range);
const { site } = await graphql(`
const { site } = await graphql(`
query Site(
$siteSlug: String!,
$limit:Int = 100,
@ -37,20 +37,20 @@ function initSitesActions(store, _router) {
}
}
`, {
siteSlug,
limit,
after,
before,
orderBy,
isNew: store.getters.isNew,
exclude: store.state.ui.filter,
});
siteSlug,
limit,
after,
before,
orderBy,
isNew: store.getters.isNew,
exclude: store.state.ui.filter,
});
return curateSite(site);
}
return curateSite(site);
}
async function fetchSites({ _commit }, { limit = 100 }) {
const { sites } = await graphql(`
async function fetchSites({ _commit }, { limit = 100 }) {
const { sites } = await graphql(`
query Sites(
$actorSlug: String!
$limit:Int = 100,
@ -64,16 +64,16 @@ function initSitesActions(store, _router) {
}
}
`, {
limit,
after: store.getters.after,
before: store.getters.before,
});
limit,
after: store.getters.after,
before: store.getters.before,
});
return sites;
}
return sites;
}
async function searchSites({ _commit }, { query, limit = 20 }) {
const { sites } = await graphql(`
async function searchSites({ _commit }, { query, limit = 20 }) {
const { sites } = await graphql(`
query SearchSites(
$query: String!
$limit:Int = 20,
@ -93,18 +93,18 @@ function initSitesActions(store, _router) {
}
}
`, {
query,
limit,
});
query,
limit,
});
return sites;
}
return sites;
}
return {
fetchSiteBySlug,
fetchSites,
searchSites,
};
return {
fetchSiteBySlug,
fetchSites,
searchSites,
};
}
export default initSitesActions;

View File

@ -3,11 +3,11 @@ import mutations from './mutations';
import actions from './actions';
function initSitesStore(store, router) {
return {
state,
mutations,
actions: actions(store, router),
};
return {
state,
mutations,
actions: actions(store, router),
};
}
export default initSitesStore;

View File

@ -10,19 +10,19 @@ import initActorsStore from './actors/actors';
import initTagsStore from './tags/tags';
function initStore(router) {
Vue.use(Vuex);
Vue.use(Vuex);
const store = new Vuex.Store();
const store = new Vuex.Store();
store.registerModule('ui', initUiStore(store, router));
store.registerModule('auth', initAuthStore(store, router));
store.registerModule('releases', initReleasesStore(store, router));
store.registerModule('actors', initActorsStore(store, router));
store.registerModule('sites', initSitesStore(store, router));
store.registerModule('networks', initNetworksStore(store, router));
store.registerModule('tags', initTagsStore(store, router));
store.registerModule('ui', initUiStore(store, router));
store.registerModule('auth', initAuthStore(store, router));
store.registerModule('releases', initReleasesStore(store, router));
store.registerModule('actors', initActorsStore(store, router));
store.registerModule('sites', initSitesStore(store, router));
store.registerModule('networks', initNetworksStore(store, router));
store.registerModule('tags', initTagsStore(store, router));
return store;
return store;
}
export default initStore;

View File

@ -1,15 +1,15 @@
import { graphql, get } from '../api';
import {
releaseFields,
releaseFields,
} from '../fragments';
import { curateTag } from '../curate';
import getDateRange from '../get-date-range';
function initTagsActions(store, _router) {
async function fetchTagBySlug({ _commit }, { tagSlug, limit = 100, range = 'latest' }) {
const { before, after, orderBy } = getDateRange(range);
async function fetchTagBySlug({ _commit }, { tagSlug, limit = 100, range = 'latest' }) {
const { before, after, orderBy } = getDateRange(range);
const { tagBySlug } = await graphql(`
const { tagBySlug } = await graphql(`
query Tag(
$tagSlug:String!
$limit:Int = 1000,
@ -85,24 +85,24 @@ function initTagsActions(store, _router) {
}
}
`, {
tagSlug,
limit,
after,
before,
orderBy: orderBy === 'DATE_DESC' ? 'RELEASE_BY_RELEASE_ID__DATE_DESC' : 'RELEASE_BY_RELEASE_ID__DATE_ASC',
exclude: store.state.ui.filter,
});
tagSlug,
limit,
after,
before,
orderBy: orderBy === 'DATE_DESC' ? 'RELEASE_BY_RELEASE_ID__DATE_DESC' : 'RELEASE_BY_RELEASE_ID__DATE_ASC',
exclude: store.state.ui.filter,
});
return curateTag(tagBySlug, store);
}
return curateTag(tagBySlug, store);
}
async function fetchTags({ _commit }, {
limit = 100,
slugs = [],
_group,
_priority,
}) {
const { tags } = await graphql(`
async function fetchTags({ _commit }, {
limit = 100,
slugs = [],
_group,
_priority,
}) {
const { tags } = await graphql(`
query Tags(
$slugs: [String!] = [],
$limit: Int = 100
@ -133,28 +133,28 @@ function initTagsActions(store, _router) {
}
}
`, {
slugs,
limit,
});
slugs,
limit,
});
return tags.map(tag => curateTag(tag, store.state.ui.sfw));
}
return tags.map(tag => curateTag(tag, store.state.ui.sfw));
}
async function fetchTagReleases({ _commit }, tagId) {
const releases = await get(`/tags/${tagId}/releases`, {
filter: store.state.ui.filter,
after: store.getters.after,
before: store.getters.before,
});
async function fetchTagReleases({ _commit }, tagId) {
const releases = await get(`/tags/${tagId}/releases`, {
filter: store.state.ui.filter,
after: store.getters.after,
before: store.getters.before,
});
return releases;
}
return releases;
}
return {
fetchTagBySlug,
fetchTags,
fetchTagReleases,
};
return {
fetchTagBySlug,
fetchTags,
fetchTagReleases,
};
}
export default initTagsActions;

View File

@ -3,11 +3,11 @@ import mutations from './mutations';
import actions from './actions';
function initTagsStore(store, router) {
return {
state,
mutations,
actions: actions(store, router),
};
return {
state,
mutations,
actions: actions(store, router),
};
}
export default initTagsStore;

View File

@ -1,35 +1,35 @@
function initUiActions(_store, _router) {
function setFilter({ commit }, filter) {
commit('setFilter', filter);
localStorage.setItem('filter', filter);
}
function setFilter({ commit }, filter) {
commit('setFilter', filter);
localStorage.setItem('filter', filter);
}
function setRange({ commit }, range) {
commit('setRange', range);
}
function setRange({ commit }, range) {
commit('setRange', range);
}
function setBatch({ commit }, batch) {
commit('setBatch', batch);
localStorage.setItem('batch', batch);
}
function setBatch({ commit }, batch) {
commit('setBatch', batch);
localStorage.setItem('batch', batch);
}
function setTheme({ commit }, theme) {
commit('setTheme', theme);
localStorage.setItem('theme', theme);
}
function setTheme({ commit }, theme) {
commit('setTheme', theme);
localStorage.setItem('theme', theme);
}
async function setSfw({ commit }, sfw) {
commit('setSfw', sfw);
localStorage.setItem('sfw', sfw);
}
async function setSfw({ commit }, sfw) {
commit('setSfw', sfw);
localStorage.setItem('sfw', sfw);
}
return {
setFilter,
setRange,
setBatch,
setSfw,
setTheme,
};
return {
setFilter,
setRange,
setBatch,
setSfw,
setTheme,
};
}
export default initUiActions;

View File

@ -1,47 +1,47 @@
import dayjs from 'dayjs';
const dateRanges = {
latest: () => ({
after: '1900-01-01',
before: dayjs(new Date()).add(1, 'day').format('YYYY-MM-DD'),
orderBy: 'DATE_DESC',
}),
upcoming: () => ({
after: dayjs(new Date()).format('YYYY-MM-DD'),
before: '2100-01-01',
orderBy: 'DATE_ASC',
}),
new: () => ({
after: '1900-01-01',
before: '2100-01-01',
orderBy: 'CREATED_AT_DESC',
}),
all: () => ({
after: '1900-01-01',
before: '2100-01-01',
orderBy: 'DATE_DESC',
}),
latest: () => ({
after: '1900-01-01',
before: dayjs(new Date()).add(1, 'day').format('YYYY-MM-DD'),
orderBy: 'DATE_DESC',
}),
upcoming: () => ({
after: dayjs(new Date()).format('YYYY-MM-DD'),
before: '2100-01-01',
orderBy: 'DATE_ASC',
}),
new: () => ({
after: '1900-01-01',
before: '2100-01-01',
orderBy: 'CREATED_AT_DESC',
}),
all: () => ({
after: '1900-01-01',
before: '2100-01-01',
orderBy: 'DATE_DESC',
}),
};
function rangeDates(state) {
return dateRanges[state.range]();
return dateRanges[state.range]();
}
function before(state) {
return dateRanges[state.range]().before;
return dateRanges[state.range]().before;
}
function after(state) {
return dateRanges[state.range]().after;
return dateRanges[state.range]().after;
}
function orderBy(state) {
return dateRanges[state.range]().orderBy;
return dateRanges[state.range]().orderBy;
}
export default {
rangeDates,
before,
after,
orderBy,
rangeDates,
before,
after,
orderBy,
};

View File

@ -1,27 +1,27 @@
function setFilter(state, filter) {
state.filter = filter;
state.filter = filter;
}
function setRange(state, range) {
state.range = range;
state.range = range;
}
function setBatch(state, batch) {
state.batch = batch;
state.batch = batch;
}
function setSfw(state, sfw) {
state.sfw = sfw;
state.sfw = sfw;
}
function setTheme(state, theme) {
state.theme = theme;
state.theme = theme;
}
export default {
setFilter,
setRange,
setBatch,
setSfw,
setTheme,
setFilter,
setRange,
setBatch,
setSfw,
setTheme,
};

View File

@ -1,25 +1,25 @@
function initUiObservers(store, _router) {
document.addEventListener('keypress', (event) => {
if (event.target.tagName === 'INPUT') {
return;
}
document.addEventListener('keypress', (event) => {
if (event.target.tagName === 'INPUT') {
return;
}
if (event.key === 's') {
store.dispatch('setSfw', true);
}
if (event.key === 's') {
store.dispatch('setSfw', true);
}
if (event.key === 'n') {
store.dispatch('setSfw', false);
}
if (event.key === 'n') {
store.dispatch('setSfw', false);
}
if (event.key === 'd') {
store.dispatch('setTheme', 'dark');
}
if (event.key === 'd') {
store.dispatch('setTheme', 'dark');
}
if (event.key === 'l') {
store.dispatch('setTheme', 'light');
}
});
if (event.key === 'l') {
store.dispatch('setTheme', 'light');
}
});
}
export default initUiObservers;

View File

@ -4,9 +4,9 @@ const storedSfw = localStorage.getItem('sfw');
const storedTheme = localStorage.getItem('theme');
export default {
filter: storedFilter ? storedFilter.split(',') : ['gay', 'transsexual'],
range: 'latest',
batch: storedBatch || 'all',
sfw: storedSfw === 'true' || false,
theme: storedTheme || 'light',
filter: storedFilter ? storedFilter.split(',') : ['gay', 'transsexual'],
range: 'latest',
batch: storedBatch || 'all',
sfw: storedSfw === 'true' || false,
theme: storedTheme || 'light',
};

View File

@ -4,12 +4,12 @@ import getters from './getters';
import actions from './actions';
function initUiStore(store, router) {
return {
state,
mutations,
getters,
actions: actions(store, router),
};
return {
state,
mutations,
getters,
actions: actions(store, router),
};
}
export default initUiStore;

View File

@ -1,177 +1,181 @@
module.exports = {
database: {
host: '127.0.0.1',
user: 'user',
password: 'password',
database: 'traxxx',
},
web: {
host: '0.0.0.0',
port: 5000,
sfwHost: '0.0.0.0',
sfwPort: 5001,
},
// include: [],
// exclude: [],
exclude: [
['21sextreme', [
// no longer updated
'mightymistress',
'dominatedgirls',
'homepornreality',
'peeandblow',
'cummingmatures',
'mandyiskinky',
'speculumplays',
'creampiereality',
]],
['aziani', [
'amberathome',
'marycarey',
'racqueldevonshire',
]],
['blowpass', ['sunlustxxx']],
['ddfnetwork', [
'fuckinhd',
'bustylover',
]],
['famedigital', [
'daringsex',
'lowartfilms',
]],
['pornpros', [
'milfhumiliation',
'humiliated',
'flexiblepositions',
'publicviolations',
'amateurviolations',
'squirtdisgrace',
'cumdisgrace',
'webcamhackers',
'collegeteens',
]],
['score', [
'bigboobbundle',
'milfbundle',
'pornmegaload',
'scorelandtv',
'scoretv',
]],
],
profiles: [
[
'evilangel',
'famedigital',
],
[
// Gamma; Evil Angel + Devil's Film, Pure Taboo (unavailable), Burning Angel and Wicked have their own assets
'xempire',
'blowpass',
],
[
// MindGeek; Brazzers and Mile High Media have their own assets
'realitykings',
'mofos',
'digitalplayground',
'twistys',
'babes',
'fakehub',
'sexyhub',
'metrohd',
'iconmale',
'men',
'transangels',
],
'wicked',
'burningangel',
'brazzers',
'milehighmedia',
[
'vixen',
'tushy',
'blacked',
'tushyraw',
'blackedraw',
'deeper',
],
[
// Nubiles
'nubiles',
'nubilesporn',
'deeplush',
'brattysis',
'nfbusty',
'anilos',
'hotcrazymess',
'thatsitcomshow',
],
'21sextury',
'julesjordan',
'naughtyamerica',
'cherrypimps',
'pimpxxx',
[
'hussiepass',
'hushpass',
'interracialpass',
'interracialpovs',
'povpornstars',
'seehimfuck',
'eyeontheguy',
],
[
// Full Porn Network
'analized',
'hergape',
'jamesdeen',
'dtfsluts',
'analbbc',
'analviolation',
'baddaddypov',
'girlfaction',
'homemadeanalwhores',
'mugfucked',
'onlyprince',
'pervertgallery',
'povperverts',
],
'private',
'ddfnetwork',
'bangbros',
'kellymadison',
'gangbangcreampie',
'gloryholesecrets',
'aziani',
'legalporno',
'score',
'boobpedia',
'pornhub',
'freeones',
'freeonesLegacy',
],
proxy: {
enable: false,
host: '',
port: 8888,
hostnames: [
'www.vixen.com',
'www.blacked.com',
'www.blackedraw.com',
'www.tushy.com',
'www.tushyraw.com',
'www.deeper.com',
],
},
fetchAfter: [1, 'week'],
nullDateLimit: 3,
media: {
path: './media',
thumbnailSize: 320, // width for 16:9 will be exactly 576px
thumbnailQuality: 100,
lazySize: 90,
lazyQuality: 90,
videoQuality: [480, 360, 320, 540, 720, 1080, 2160, 270, 240, 180],
limit: 25, // max number of photos per release
},
titleSlugLength: 50,
database: {
host: '127.0.0.1',
user: 'user',
password: 'password',
database: 'traxxx',
},
web: {
host: '0.0.0.0',
port: 5000,
sfwHost: '0.0.0.0',
sfwPort: 5001,
},
// include: [],
// exclude: [],
exclude: [
['21sextreme', [
// no longer updated
'mightymistress',
'dominatedgirls',
'homepornreality',
'peeandblow',
'cummingmatures',
'mandyiskinky',
'speculumplays',
'creampiereality',
]],
['aziani', [
'amberathome',
'marycarey',
'racqueldevonshire',
]],
'boobpedia',
['blowpass', ['sunlustxxx']],
['ddfnetwork', [
'fuckinhd',
'bustylover',
]],
['famedigital', [
'daringsex',
'lowartfilms',
]],
'freeones',
['pornpros', [
'milfhumiliation',
'humiliated',
'flexiblepositions',
'publicviolations',
'amateurviolations',
'squirtdisgrace',
'cumdisgrace',
'webcamhackers',
'collegeteens',
]],
['score', [
'bigboobbundle',
'milfbundle',
'pornmegaload',
'scorelandtv',
'scoretv',
]],
['mindgeek', [
'pornhub',
]],
],
profiles: [
[
'evilangel',
'famedigital',
],
[
// Gamma; Evil Angel + Devil's Film, Pure Taboo (unavailable), Burning Angel and Wicked have their own assets
'xempire',
'blowpass',
],
[
// MindGeek; Brazzers and Mile High Media have their own assets
'realitykings',
'mofos',
'digitalplayground',
'twistys',
'babes',
'fakehub',
'sexyhub',
'metrohd',
'iconmale',
'men',
'transangels',
],
'wicked',
'burningangel',
'brazzers',
'milehighmedia',
[
'vixen',
'tushy',
'blacked',
'tushyraw',
'blackedraw',
'deeper',
],
[
// Nubiles
'nubiles',
'nubilesporn',
'deeplush',
'brattysis',
'nfbusty',
'anilos',
'hotcrazymess',
'thatsitcomshow',
],
'21sextury',
'julesjordan',
'naughtyamerica',
'cherrypimps',
'pimpxxx',
[
'hussiepass',
'hushpass',
'interracialpass',
'interracialpovs',
'povpornstars',
'seehimfuck',
'eyeontheguy',
],
[
// Full Porn Network
'analized',
'hergape',
'jamesdeen',
'dtfsluts',
'analbbc',
'analviolation',
'baddaddypov',
'girlfaction',
'homemadeanalwhores',
'mugfucked',
'onlyprince',
'pervertgallery',
'povperverts',
],
'private',
'ddfnetwork',
'bangbros',
'kellymadison',
'gangbangcreampie',
'gloryholesecrets',
'aziani',
'legalporno',
'score',
'boobpedia',
'pornhub',
'freeones',
],
proxy: {
enable: false,
host: '',
port: 8888,
hostnames: [
'www.vixen.com',
'www.blacked.com',
'www.blackedraw.com',
'www.tushy.com',
'www.tushyraw.com',
'www.deeper.com',
],
},
fetchAfter: [1, 'week'],
nullDateLimit: 3,
media: {
path: './media',
thumbnailSize: 320, // width for 16:9 will be exactly 576px
thumbnailQuality: 100,
lazySize: 90,
lazyQuality: 90,
videoQuality: [480, 360, 320, 540, 720, 1080, 2160, 270, 240, 180],
limit: 25, // max number of photos per release
},
titleSlugLength: 50,
};

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -6,10 +6,11 @@
},
"rules": {
"strict": 0,
"indent": ["error", "tab"],
"no-tabs": "off",
"no-unused-vars": ["error", {"argsIgnorePattern": "^_"}],
"no-console": 0,
"no-underscore-dangle": 0,
"indent": "off",
"prefer-destructuring": "off",
"template-curly-spacing": "off",
"object-curly-newline": "off",

View File

@ -18,522 +18,522 @@ const { curateSites } = require('./sites');
const { storeMedia, associateMedia } = require('./media');
async function curateActor(actor) {
const [aliases, avatar, photos, social] = await Promise.all([
knex('actors').where({ alias_for: actor.id }),
knex('actors_avatars')
.where('actor_id', actor.id)
.join('media', 'media.id', 'actors_avatars.media_id')
.first(),
knex('actors_photos')
.where('actor_id', actor.id)
.join('media', 'media.id', 'actors_photos.media_id')
.orderBy('index'),
knex('actors_social')
.where('actor_id', actor.id)
.orderBy('platform', 'desc'),
]);
const [aliases, avatar, photos, social] = await Promise.all([
knex('actors').where({ alias_for: actor.id }),
knex('actors_avatars')
.where('actor_id', actor.id)
.join('media', 'media.id', 'actors_avatars.media_id')
.first(),
knex('actors_photos')
.where('actor_id', actor.id)
.join('media', 'media.id', 'actors_photos.media_id')
.orderBy('index'),
knex('actors_social')
.where('actor_id', actor.id)
.orderBy('platform', 'desc'),
]);
const curatedActor = {
id: actor.id,
gender: actor.gender,
name: actor.name,
description: actor.description,
birthdate: actor.birthdate && new Date(actor.birthdate),
country: actor.country_alpha2,
origin: (actor.birth_city || actor.birth_state || actor.birth_country_alpha2) ? {} : null,
residence: (actor.residence_city || actor.residence_state || actor.residence_country_alpha2) ? {} : null,
ethnicity: actor.ethnicity,
height: actor.height,
weight: actor.weight,
bust: actor.bust,
waist: actor.waist,
hip: actor.hip,
naturalBoobs: actor.natural_boobs,
aliases: aliases.map(({ name }) => name),
slug: actor.slug,
avatar,
photos,
hasTattoos: actor.has_tattoos,
hasPiercings: actor.has_piercings,
tattoos: actor.tattoos,
piercings: actor.piercings,
social,
scrapedAt: actor.scraped_at,
};
const curatedActor = {
id: actor.id,
gender: actor.gender,
name: actor.name,
description: actor.description,
birthdate: actor.birthdate && new Date(actor.birthdate),
country: actor.country_alpha2,
origin: (actor.birth_city || actor.birth_state || actor.birth_country_alpha2) ? {} : null,
residence: (actor.residence_city || actor.residence_state || actor.residence_country_alpha2) ? {} : null,
ethnicity: actor.ethnicity,
height: actor.height,
weight: actor.weight,
bust: actor.bust,
waist: actor.waist,
hip: actor.hip,
naturalBoobs: actor.natural_boobs,
aliases: aliases.map(({ name }) => name),
slug: actor.slug,
avatar,
photos,
hasTattoos: actor.has_tattoos,
hasPiercings: actor.has_piercings,
tattoos: actor.tattoos,
piercings: actor.piercings,
social,
scrapedAt: actor.scraped_at,
};
if (curatedActor.birthdate) {
curatedActor.age = moment().diff(curatedActor.birthdate, 'years');
}
if (curatedActor.birthdate) {
curatedActor.age = moment().diff(curatedActor.birthdate, 'years');
}
if (actor.birth_city) curatedActor.origin.city = actor.birth_city;
if (actor.birth_state) curatedActor.origin.state = actor.birth_state;
if (actor.birth_city) curatedActor.origin.city = actor.birth_city;
if (actor.birth_state) curatedActor.origin.state = actor.birth_state;
if (actor.birth_country_alpha2) {
curatedActor.origin.country = {
alpha2: actor.birth_country_alpha2,
name: actor.birth_country_name,
alias: actor.birth_country_alias,
};
}
if (actor.birth_country_alpha2) {
curatedActor.origin.country = {
alpha2: actor.birth_country_alpha2,
name: actor.birth_country_name,
alias: actor.birth_country_alias,
};
}
if (actor.residence_city) curatedActor.residence.city = actor.residence_city;
if (actor.residence_state) curatedActor.residence.state = actor.residence_state;
if (actor.residence_city) curatedActor.residence.city = actor.residence_city;
if (actor.residence_state) curatedActor.residence.state = actor.residence_state;
if (actor.residence_country_alpha2) {
curatedActor.residence.country = {
alpha2: actor.residence_country_alpha2,
name: actor.residence_country_name,
alias: actor.residence_country_alias,
};
}
if (actor.residence_country_alpha2) {
curatedActor.residence.country = {
alpha2: actor.residence_country_alpha2,
name: actor.residence_country_name,
alias: actor.residence_country_alias,
};
}
return curatedActor;
return curatedActor;
}
function curateActors(releases) {
return Promise.all(releases.map(async release => curateActor(release)));
return Promise.all(releases.map(async release => curateActor(release)));
}
function curateActorEntry(actor, scraped, scrapeSuccess) {
const curatedActor = {
name: capitalize(actor.name),
slug: slugify(actor.name),
birthdate: actor.birthdate,
description: actor.description,
gender: actor.gender,
ethnicity: actor.ethnicity,
bust: actor.bust,
waist: actor.waist,
hip: actor.hip,
natural_boobs: actor.naturalBoobs,
height: actor.height,
weight: actor.weight,
hair: actor.hair,
eyes: actor.eyes,
has_tattoos: actor.hasTattoos,
has_piercings: actor.hasPiercings,
tattoos: actor.tattoos,
piercings: actor.piercings,
};
const curatedActor = {
name: capitalize(actor.name),
slug: slugify(actor.name),
birthdate: actor.birthdate,
description: actor.description,
gender: actor.gender,
ethnicity: actor.ethnicity,
bust: actor.bust,
waist: actor.waist,
hip: actor.hip,
natural_boobs: actor.naturalBoobs,
height: actor.height,
weight: actor.weight,
hair: actor.hair,
eyes: actor.eyes,
has_tattoos: actor.hasTattoos,
has_piercings: actor.hasPiercings,
tattoos: actor.tattoos,
piercings: actor.piercings,
};
if (actor.id) {
curatedActor.id = actor.id;
}
if (actor.id) {
curatedActor.id = actor.id;
}
if (actor.birthPlace) {
curatedActor.birth_city = actor.birthPlace.city;
curatedActor.birth_state = actor.birthPlace.state;
curatedActor.birth_country_alpha2 = actor.birthPlace.country;
}
if (actor.birthPlace) {
curatedActor.birth_city = actor.birthPlace.city;
curatedActor.birth_state = actor.birthPlace.state;
curatedActor.birth_country_alpha2 = actor.birthPlace.country;
}
if (actor.residencePlace) {
curatedActor.residence_city = actor.residencePlace.city;
curatedActor.residence_state = actor.residencePlace.state;
curatedActor.residence_country_alpha2 = actor.residencePlace.country;
}
if (actor.residencePlace) {
curatedActor.residence_city = actor.residencePlace.city;
curatedActor.residence_state = actor.residencePlace.state;
curatedActor.residence_country_alpha2 = actor.residencePlace.country;
}
if (scraped) {
curatedActor.scraped_at = new Date();
curatedActor.scrape_success = scrapeSuccess;
}
if (scraped) {
curatedActor.scraped_at = new Date();
curatedActor.scrape_success = scrapeSuccess;
}
return curatedActor;
return curatedActor;
}
function curateSocialEntry(url, actorId) {
const platforms = [
// links supplied by PH often look like domain.com/domain.com/username
{
label: 'twitter',
pattern: 'http(s)\\://(*)twitter.com/:username(/)(?*)',
format: username => `https://www.twitter.com/${username}`,
},
{
label: 'youtube',
pattern: 'http(s)\\://(*)youtube.com/channel/:username(?*)',
format: username => `https://www.youtube.com/channel/${username}`,
},
{
label: 'instagram',
pattern: 'http(s)\\://(*)instagram.com/:username(/)(?*)',
format: username => `https://www.instagram.com/${username}`,
},
{
label: 'snapchat',
pattern: 'http(s)\\://(*)snapchat.com/add/:username(/)(?*)',
format: username => `https://www.snapchat.com/add/${username}`,
},
{
label: 'tumblr',
pattern: 'http(s)\\://:username.tumblr.com(*)',
format: username => `https://${username}.tumblr.com`,
},
{
label: 'onlyfans',
pattern: 'http(s)\\://(*)onlyfans.com/:username(/)(?*)',
format: username => `https://www.onlyfans.com/${username}`,
},
{
label: 'fancentro',
pattern: 'http(s)\\://(*)fancentro.com/:username(/)(?*)',
format: username => `https://www.fancentro.com/${username}`,
},
{
label: 'modelhub',
pattern: 'http(s)\\://(*)modelhub.com/:username(/)(?*)',
format: username => `https://www.modelhub.com/${username}`,
},
];
const platforms = [
// links supplied by PH often look like domain.com/domain.com/username
{
label: 'twitter',
pattern: 'http(s)\\://(*)twitter.com/:username(/)(?*)',
format: username => `https://www.twitter.com/${username}`,
},
{
label: 'youtube',
pattern: 'http(s)\\://(*)youtube.com/channel/:username(?*)',
format: username => `https://www.youtube.com/channel/${username}`,
},
{
label: 'instagram',
pattern: 'http(s)\\://(*)instagram.com/:username(/)(?*)',
format: username => `https://www.instagram.com/${username}`,
},
{
label: 'snapchat',
pattern: 'http(s)\\://(*)snapchat.com/add/:username(/)(?*)',
format: username => `https://www.snapchat.com/add/${username}`,
},
{
label: 'tumblr',
pattern: 'http(s)\\://:username.tumblr.com(*)',
format: username => `https://${username}.tumblr.com`,
},
{
label: 'onlyfans',
pattern: 'http(s)\\://(*)onlyfans.com/:username(/)(?*)',
format: username => `https://www.onlyfans.com/${username}`,
},
{
label: 'fancentro',
pattern: 'http(s)\\://(*)fancentro.com/:username(/)(?*)',
format: username => `https://www.fancentro.com/${username}`,
},
{
label: 'modelhub',
pattern: 'http(s)\\://(*)modelhub.com/:username(/)(?*)',
format: username => `https://www.modelhub.com/${username}`,
},
];
const match = platforms.reduce((acc, platform) => {
if (acc) return acc;
const match = platforms.reduce((acc, platform) => {
if (acc) return acc;
const patternMatch = new UrlPattern(platform.pattern).match(url);
const patternMatch = new UrlPattern(platform.pattern).match(url);
if (patternMatch) {
return {
platform: platform.label,
original: url,
username: patternMatch.username,
url: platform.format ? platform.format(patternMatch.username) : url,
};
}
if (patternMatch) {
return {
platform: platform.label,
original: url,
username: patternMatch.username,
url: platform.format ? platform.format(patternMatch.username) : url,
};
}
return null;
}, null) || { url };
return null;
}, null) || { url };
return {
url: match.url,
platform: match.platform,
actor_id: actorId,
};
return {
url: match.url,
platform: match.platform,
actor_id: actorId,
};
}
async function curateSocialEntries(urls, actorId) {
if (!urls) {
return [];
}
if (!urls) {
return [];
}
const existingSocialLinks = await knex('actors_social').where('actor_id', actorId);
const existingSocialLinks = await knex('actors_social').where('actor_id', actorId);
return urls.reduce((acc, url) => {
const socialEntry = curateSocialEntry(url, actorId);
return urls.reduce((acc, url) => {
const socialEntry = curateSocialEntry(url, actorId);
if (acc.some(entry => socialEntry.url.toLowerCase() === entry.url.toLowerCase()) || existingSocialLinks.some(entry => socialEntry.url.toLowerCase() === entry.url.toLowerCase())) {
// prevent duplicates
return acc;
}
if (acc.some(entry => socialEntry.url.toLowerCase() === entry.url.toLowerCase()) || existingSocialLinks.some(entry => socialEntry.url.toLowerCase() === entry.url.toLowerCase())) {
// prevent duplicates
return acc;
}
return [...acc, socialEntry];
}, []);
return [...acc, socialEntry];
}, []);
}
async function fetchActors(queryObject, limit = 100) {
const releases = await knex('actors')
.select(
'actors.*',
'birth_countries.alpha2 as birth_country_alpha2', 'birth_countries.name as birth_country_name', 'birth_countries.alias as birth_country_alias',
'residence_countries.alpha2 as residence_country_alpha2', 'residence_countries.name as residence_country_name', 'residence_countries.alias as residence_country_alias',
)
.leftJoin('countries as birth_countries', 'actors.birth_country_alpha2', 'birth_countries.alpha2')
.leftJoin('countries as residence_countries', 'actors.residence_country_alpha2', 'residence_countries.alpha2')
.orderBy(['actors.name', 'actors.gender'])
.where(builder => whereOr(queryObject, 'actors', builder))
.limit(limit);
const releases = await knex('actors')
.select(
'actors.*',
'birth_countries.alpha2 as birth_country_alpha2', 'birth_countries.name as birth_country_name', 'birth_countries.alias as birth_country_alias',
'residence_countries.alpha2 as residence_country_alpha2', 'residence_countries.name as residence_country_name', 'residence_countries.alias as residence_country_alias',
)
.leftJoin('countries as birth_countries', 'actors.birth_country_alpha2', 'birth_countries.alpha2')
.leftJoin('countries as residence_countries', 'actors.residence_country_alpha2', 'residence_countries.alpha2')
.orderBy(['actors.name', 'actors.gender'])
.where(builder => whereOr(queryObject, 'actors', builder))
.limit(limit);
return curateActors(releases);
return curateActors(releases);
}
async function storeSocialLinks(urls, actorId) {
const curatedSocialEntries = await curateSocialEntries(urls, actorId);
const curatedSocialEntries = await curateSocialEntries(urls, actorId);
await knex('actors_social').insert(curatedSocialEntries);
await knex('actors_social').insert(curatedSocialEntries);
}
async function storeAvatars(avatars, actorId) {
if (!avatars || avatars.length === 0) {
return [];
}
if (!avatars || avatars.length === 0) {
return [];
}
const avatarsBySource = await storeMedia(avatars, 'actor', 'avatar');
await associateMedia({ [actorId]: avatars }, avatarsBySource, 'actor', 'photo', 'avatar');
const avatarsBySource = await storeMedia(avatars, 'actor', 'avatar');
await associateMedia({ [actorId]: avatars }, avatarsBySource, 'actor', 'photo', 'avatar');
return avatarsBySource;
return avatarsBySource;
}
async function storeActor(actor, scraped = false, scrapeSuccess = false) {
const curatedActor = curateActorEntry(actor, scraped, scrapeSuccess);
const curatedActor = curateActorEntry(actor, scraped, scrapeSuccess);
const [actorEntry] = await knex('actors')
.insert(curatedActor)
.returning('*');
const [actorEntry] = await knex('actors')
.insert(curatedActor)
.returning('*');
await storeSocialLinks(actor.social, actorEntry.id);
await storeSocialLinks(actor.social, actorEntry.id);
if (actor.avatars) {
await storeAvatars(actor.avatars, actorEntry.id);
}
if (actor.avatars) {
await storeAvatars(actor.avatars, actorEntry.id);
}
logger.info(`Added new entry for actor '${actor.name}'`);
logger.info(`Added new entry for actor '${actor.name}'`);
return actorEntry;
return actorEntry;
}
async function updateActor(actor, scraped = false, scrapeSuccess = false) {
const curatedActor = curateActorEntry(actor, scraped, scrapeSuccess);
const curatedActor = curateActorEntry(actor, scraped, scrapeSuccess);
const [actorEntry] = await knex('actors')
.where({ id: actor.id })
.update(curatedActor)
.returning('*');
const [actorEntry] = await knex('actors')
.where({ id: actor.id })
.update(curatedActor)
.returning('*');
await storeSocialLinks(actor.social, actor.id);
await storeSocialLinks(actor.social, actor.id);
logger.info(`Updated entry for actor '${actor.name}'`);
logger.info(`Updated entry for actor '${actor.name}'`);
return actorEntry;
return actorEntry;
}
async function mergeProfiles(profiles, actor) {
if (profiles.filter(Boolean).length === 0) {
return null;
}
if (profiles.filter(Boolean).length === 0) {
return null;
}
const mergedProfile = profiles.reduce((prevProfile, profile) => {
if (profile === null) {
return prevProfile;
}
const mergedProfile = profiles.reduce((prevProfile, profile) => {
if (profile === null) {
return prevProfile;
}
const accProfile = {
id: actor ? actor.id : null,
name: actor ? actor.name : (prevProfile.name || profile.name),
description: prevProfile.description || profile.description,
gender: prevProfile.gender || profile.gender,
birthdate: !prevProfile.birthdate || Number.isNaN(Number(prevProfile.birthdate)) ? profile.birthdate : prevProfile.birthdate,
birthPlace: prevProfile.birthPlace || profile.birthPlace,
residencePlace: prevProfile.residencePlace || profile.residencePlace,
nationality: prevProfile.nationality || profile.nationality, // used to derive country when not available
ethnicity: prevProfile.ethnicity || profile.ethnicity,
bust: prevProfile.bust || (/\d+\w+/.test(profile.bust) ? profile.bust : null),
waist: prevProfile.waist || profile.waist,
hip: prevProfile.hip || profile.hip,
naturalBoobs: prevProfile.naturalBoobs === undefined ? profile.naturalBoobs : prevProfile.naturalBoobs,
height: prevProfile.height || profile.height,
weight: prevProfile.weight || profile.weight,
hair: prevProfile.hair || profile.hair,
eyes: prevProfile.eyes || profile.eyes,
hasPiercings: prevProfile.hasPiercings === undefined ? profile.hasPiercings : prevProfile.hasPiercings,
hasTattoos: prevProfile.hasTattoos === undefined ? profile.hasTattoos : prevProfile.hasTattoos,
piercings: prevProfile.piercings || profile.piercings,
tattoos: prevProfile.tattoos || profile.tattoos,
social: prevProfile.social.concat(profile.social || []),
releases: prevProfile.releases.concat(profile.releases ? profile.releases : []), // don't flatten fallbacks
};
const accProfile = {
id: actor ? actor.id : null,
name: actor ? actor.name : (prevProfile.name || profile.name),
description: prevProfile.description || profile.description,
gender: prevProfile.gender || profile.gender,
birthdate: !prevProfile.birthdate || Number.isNaN(Number(prevProfile.birthdate)) ? profile.birthdate : prevProfile.birthdate,
birthPlace: prevProfile.birthPlace || profile.birthPlace,
residencePlace: prevProfile.residencePlace || profile.residencePlace,
nationality: prevProfile.nationality || profile.nationality, // used to derive country when not available
ethnicity: prevProfile.ethnicity || profile.ethnicity,
bust: prevProfile.bust || (/\d+\w+/.test(profile.bust) ? profile.bust : null),
waist: prevProfile.waist || profile.waist,
hip: prevProfile.hip || profile.hip,
naturalBoobs: prevProfile.naturalBoobs === undefined ? profile.naturalBoobs : prevProfile.naturalBoobs,
height: prevProfile.height || profile.height,
weight: prevProfile.weight || profile.weight,
hair: prevProfile.hair || profile.hair,
eyes: prevProfile.eyes || profile.eyes,
hasPiercings: prevProfile.hasPiercings === undefined ? profile.hasPiercings : prevProfile.hasPiercings,
hasTattoos: prevProfile.hasTattoos === undefined ? profile.hasTattoos : prevProfile.hasTattoos,
piercings: prevProfile.piercings || profile.piercings,
tattoos: prevProfile.tattoos || profile.tattoos,
social: prevProfile.social.concat(profile.social || []),
releases: prevProfile.releases.concat(profile.releases ? profile.releases : []), // don't flatten fallbacks
};
if (profile.avatar) {
const avatar = Array.isArray(profile.avatar)
? profile.avatar.map(avatarX => ({
src: avatarX.src || avatarX,
scraper: profile.scraper,
copyright: avatarX.copyright === undefined ? capitalize(profile.site?.name || profile.scraper) : profile.avatar.copyright,
}))
: {
src: profile.avatar.src || profile.avatar,
scraper: profile.scraper,
copyright: profile.avatar.copyright === undefined ? capitalize(profile.site?.name || profile.scraper) : profile.avatar.copyright,
};
if (profile.avatar) {
const avatar = Array.isArray(profile.avatar)
? profile.avatar.map(avatarX => ({
src: avatarX.src || avatarX,
scraper: profile.scraper,
copyright: avatarX.copyright === undefined ? capitalize(profile.site?.name || profile.scraper) : profile.avatar.copyright,
}))
: {
src: profile.avatar.src || profile.avatar,
scraper: profile.scraper,
copyright: profile.avatar.copyright === undefined ? capitalize(profile.site?.name || profile.scraper) : profile.avatar.copyright,
};
accProfile.avatars = prevProfile.avatars.concat([avatar]); // don't flatten fallbacks
} else {
accProfile.avatars = prevProfile.avatars;
}
accProfile.avatars = prevProfile.avatars.concat([avatar]); // don't flatten fallbacks
} else {
accProfile.avatars = prevProfile.avatars;
}
return accProfile;
}, {
social: [],
avatars: [],
releases: [],
});
return accProfile;
}, {
social: [],
avatars: [],
releases: [],
});
const [birthPlace, residencePlace] = await Promise.all([
resolvePlace(mergedProfile.birthPlace),
resolvePlace(mergedProfile.residencePlace),
]);
const [birthPlace, residencePlace] = await Promise.all([
resolvePlace(mergedProfile.birthPlace),
resolvePlace(mergedProfile.residencePlace),
]);
mergedProfile.birthPlace = birthPlace;
mergedProfile.residencePlace = residencePlace;
mergedProfile.birthPlace = birthPlace;
mergedProfile.residencePlace = residencePlace;
if (!mergedProfile.birthPlace && mergedProfile.nationality) {
const country = await knex('countries')
.where('nationality', 'ilike', `%${mergedProfile.nationality}%`)
.orderBy('priority', 'desc')
.first();
if (!mergedProfile.birthPlace && mergedProfile.nationality) {
const country = await knex('countries')
.where('nationality', 'ilike', `%${mergedProfile.nationality}%`)
.orderBy('priority', 'desc')
.first();
mergedProfile.birthPlace = {
country: country.alpha2,
};
}
mergedProfile.birthPlace = {
country: country.alpha2,
};
}
return mergedProfile;
return mergedProfile;
}
async function scrapeProfiles(sources, actorName, actorEntry, sitesBySlug) {
return Promise.map(sources, async (source) => {
// const [scraperSlug, scraper] = source;
const profileScrapers = [].concat(source).map(slug => ({ scraperSlug: slug, scraper: scrapers.actors[slug] }));
return Promise.map(sources, async (source) => {
// const [scraperSlug, scraper] = source;
const profileScrapers = [].concat(source).map(slug => ({ scraperSlug: slug, scraper: scrapers.actors[slug] }));
try {
return await profileScrapers.reduce(async (outcome, { scraper, scraperSlug }) => outcome.catch(async () => {
if (!scraper) {
logger.warn(`No profile profile scraper available for ${scraperSlug}`);
throw Object.assign(new Error(`No profile scraper available for ${scraperSlug}`));
}
try {
return await profileScrapers.reduce(async (outcome, { scraper, scraperSlug }) => outcome.catch(async () => {
if (!scraper) {
logger.warn(`No profile profile scraper available for ${scraperSlug}`);
throw Object.assign(new Error(`No profile scraper available for ${scraperSlug}`));
}
logger.verbose(`Searching '${actorName}' on ${scraperSlug}`);
logger.verbose(`Searching '${actorName}' on ${scraperSlug}`);
const site = sitesBySlug[scraperSlug] || null;
const profile = await scraper.fetchProfile(actorEntry ? actorEntry.name : actorName, scraperSlug, site, include);
const site = sitesBySlug[scraperSlug] || null;
const profile = await scraper.fetchProfile(actorEntry ? actorEntry.name : actorName, scraperSlug, site, include);
if (profile && typeof profile !== 'number') {
logger.verbose(`Found profile for '${actorName}' on ${scraperSlug}`);
if (profile && typeof profile !== 'number') {
logger.verbose(`Found profile for '${actorName}' on ${scraperSlug}`);
return {
...profile,
name: actorName,
scraper: scraperSlug,
site,
releases: profile.releases?.map(release => (typeof release === 'string'
? { url: release, site }
: { ...release, site: release.site || site }
)),
};
}
return {
...profile,
name: actorName,
scraper: scraperSlug,
site,
releases: profile.releases?.map(release => (typeof release === 'string'
? { url: release, site }
: { ...release, site: release.site || site }
)),
};
}
logger.verbose(`No profile for '${actorName}' available on ${scraperSlug}: ${profile}`);
throw Object.assign(new Error(`Profile for ${actorName} not available on ${scraperSlug}`), { warn: false });
}), Promise.reject(new Error()));
} catch (error) {
if (error.warn !== false) {
logger.warn(`Error in scraper ${source}: ${error.message}`);
// logger.error(error.stack);
}
}
logger.verbose(`No profile for '${actorName}' available on ${scraperSlug}: ${profile}`);
throw Object.assign(new Error(`Profile for ${actorName} not available on ${scraperSlug}`), { warn: false });
}), Promise.reject(new Error()));
} catch (error) {
if (error.warn !== false) {
logger.warn(`Error in scraper ${source}: ${error.message}`);
// logger.error(error.stack);
}
}
return null;
});
return null;
});
}
async function scrapeActors(actorNames) {
return Promise.map(actorNames || argv.actors, async (actorName) => {
try {
const actorSlug = slugify(actorName);
const actorEntry = await knex('actors').where({ slug: actorSlug }).first();
const sources = argv.sources || config.profiles || Object.keys(scrapers.actors);
return Promise.map(actorNames || argv.actors, async (actorName) => {
try {
const actorSlug = slugify(actorName);
const actorEntry = await knex('actors').where({ slug: actorSlug }).first();
const sources = argv.sources || config.profiles || Object.keys(scrapers.actors);
const finalSources = argv.withReleases ? sources.flat() : sources; // ignore race-to-success grouping when scenes are requested
const finalSources = argv.withReleases ? sources.flat() : sources; // ignore race-to-success grouping when scenes are requested
const [siteEntries, networkEntries] = await Promise.all([
knex('sites')
.leftJoin('networks', 'sites.network_id', 'networks.id')
.select(
'sites.*',
'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', 'networks.parameters as network_parameters',
)
.whereIn('sites.slug', finalSources.flat()),
knex('networks').select('*').whereIn('slug', finalSources.flat()),
]);
const [siteEntries, networkEntries] = await Promise.all([
knex('sites')
.leftJoin('networks', 'sites.network_id', 'networks.id')
.select(
'sites.*',
'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description', 'networks.parameters as network_parameters',
)
.whereIn('sites.slug', finalSources.flat()),
knex('networks').select('*').whereIn('slug', finalSources.flat()),
]);
const sites = await curateSites(siteEntries, true);
const networks = networkEntries.map(network => ({ ...network, isFallback: true }));
const sitesBySlug = [].concat(networks, sites).reduce((acc, site) => ({ ...acc, [site.slug]: site }), {});
const sites = await curateSites(siteEntries, true);
const networks = networkEntries.map(network => ({ ...network, isFallback: true }));
const sitesBySlug = [].concat(networks, sites).reduce((acc, site) => ({ ...acc, [site.slug]: site }), {});
const profiles = await scrapeProfiles(sources, actorName, actorEntry, sitesBySlug);
const profile = await mergeProfiles(profiles, actorEntry);
const profiles = await scrapeProfiles(sources, actorName, actorEntry, sitesBySlug);
const profile = await mergeProfiles(profiles, actorEntry);
if (profile === null) {
logger.warn(`Could not find profile for actor '${actorName}'`);
if (profile === null) {
logger.warn(`Could not find profile for actor '${actorName}'`);
if (argv.save && !actorEntry) {
await storeActor({ name: actorName }, false, false);
}
if (argv.save && !actorEntry) {
await storeActor({ name: actorName }, false, false);
}
return null;
}
return null;
}
if (argv.inspect) {
console.log(profile);
logger.info(`Found ${profile.releases.length} releases for ${actorName}`);
}
if (argv.inspect) {
console.log(profile);
logger.info(`Found ${profile.releases.length} releases for ${actorName}`);
}
if (argv.save) {
if (actorEntry && profile) {
await Promise.all([
updateActor(profile, true, true),
storeAvatars(profile.avatars, actorEntry.id),
]);
if (argv.save) {
if (actorEntry && profile) {
await Promise.all([
updateActor(profile, true, true),
storeAvatars(profile.avatars, actorEntry.id),
]);
return profile;
}
return profile;
}
await storeActor(profile, true, true);
}
await storeActor(profile, true, true);
}
return profile;
} catch (error) {
console.log(error);
logger.warn(`${actorName}: ${error}`);
return profile;
} catch (error) {
console.log(error);
logger.warn(`${actorName}: ${error}`);
return null;
}
}, {
concurrency: 3,
});
return null;
}
}, {
concurrency: 3,
});
}
async function scrapeBasicActors() {
const basicActors = await knex('actors').where('scraped_at', null);
const basicActors = await knex('actors').where('scraped_at', null);
return scrapeActors(basicActors.map(actor => actor.name));
return scrapeActors(basicActors.map(actor => actor.name));
}
async function associateActors(mappedActors, releases) {
const [existingActorEntries, existingAssociationEntries] = await Promise.all([
knex('actors')
.whereIn('name', Object.values(mappedActors).map(actor => actor.name))
.orWhereIn('slug', Object.keys(mappedActors)),
knex('releases_actors').whereIn('release_id', releases.map(release => release.id)),
]);
const [existingActorEntries, existingAssociationEntries] = await Promise.all([
knex('actors')
.whereIn('name', Object.values(mappedActors).map(actor => actor.name))
.orWhereIn('slug', Object.keys(mappedActors)),
knex('releases_actors').whereIn('release_id', releases.map(release => release.id)),
]);
const associations = await Promise.map(Object.entries(mappedActors), async ([actorSlug, actor]) => {
try {
const actorEntry = existingActorEntries.find(actorX => actorX.slug === actorSlug)
const associations = await Promise.map(Object.entries(mappedActors), async ([actorSlug, actor]) => {
try {
const actorEntry = existingActorEntries.find(actorX => actorX.slug === actorSlug)
|| await storeActor(actor);
// if a scene
return Array.from(actor.releaseIds)
.map(releaseId => ({
release_id: releaseId,
actor_id: actorEntry.id,
}))
.filter(association => !existingAssociationEntries
// remove associations already in database
.some(associationEntry => associationEntry.actor_id === association.actor_id
// if a scene
return Array.from(actor.releaseIds)
.map(releaseId => ({
release_id: releaseId,
actor_id: actorEntry.id,
}))
.filter(association => !existingAssociationEntries
// remove associations already in database
.some(associationEntry => associationEntry.actor_id === association.actor_id
&& associationEntry.release_id === association.release_id));
} catch (error) {
logger.error(actor.name, error);
return null;
}
});
} catch (error) {
logger.error(actor.name, error);
return null;
}
});
await knex('releases_actors').insert(associations.filter(association => association).flat());
await knex('releases_actors').insert(associations.filter(association => association).flat());
// basic actor scraping is failure prone, don't run together with actor association
// await scrapebasicactors(),
// basic actor scraping is failure prone, don't run together with actor association
// await scrapebasicactors(),
}
module.exports = {
associateActors,
fetchActors,
scrapeActors,
scrapeBasicActors,
associateActors,
fetchActors,
scrapeActors,
scrapeBasicActors,
};

View File

@ -1,125 +1,156 @@
'use strict';
const config = require('config');
const Promise = require('bluebird');
// const logger = require('./logger')(__filename);
const knex = require('./knex');
const scrapers = require('./scrapers/scrapers');
const argv = require('./argv');
const slugify = require('./utils/slugify');
const capitalize = require('./utils/capitalize');
function toBaseActors(actorsOrNames, release) {
return actorsOrNames.map((actorOrName) => {
const name = capitalize(actorOrName.name || actorOrName);
const slug = slugify(name);
return actorsOrNames.map((actorOrName) => {
const name = capitalize(actorOrName.name || actorOrName);
const slug = slugify(name);
const baseActor = {
name,
slug,
network: release.site.network,
};
const baseActor = {
name,
slug,
network: release?.site.network,
};
if (actorOrName.name) {
return {
...actorOrName,
...baseActor,
};
}
if (actorOrName.name) {
return {
...actorOrName,
...baseActor,
};
}
return baseActor;
});
return baseActor;
});
}
function curateActorEntry(baseActor, batchId) {
return {
name: baseActor.name,
slug: baseActor.slug,
network_id: null,
batch_id: batchId,
};
return {
name: baseActor.name,
slug: baseActor.slug,
network_id: null,
batch_id: batchId,
};
}
function curateActorEntries(baseActors, batchId) {
return baseActors.map(baseActor => curateActorEntry(baseActor, batchId));
return baseActors.map(baseActor => curateActorEntry(baseActor, batchId));
}
async function scrapeProfiles() {
async function scrapeActors(actorNames) {
const baseActors = toBaseActors(actorNames);
const sources = argv.sources || config.profiles || Object.keys(scrapers.actors);
const siteSlugs = sources.flat();
const [networks, sites, existingActorEntries] = await Promise.all([
knex('networks').whereIn('slug', siteSlugs),
knex('sites').whereIn('slug', siteSlugs),
knex('actors')
.select(['id', 'name', 'slug'])
.whereIn('slug', baseActors.map(baseActor => baseActor.slug))
.whereNull('network_id'),
]);
const existingActorEntriesBySlug = existingActorEntries.reduce((acc, actorEntry) => ({ ...acc, [actorEntry.slug]: actorEntry }), {});
const networksBySlug = networks.reduce((acc, network) => ({ ...acc, [network.slug]: { ...network, isNetwork: true } }), {});
const sitesBySlug = sites.reduce((acc, site) => ({ ...acc, [site.slug]: site }), {});
const newBaseActors = baseActors.filter(baseActor => !existingActorEntriesBySlug[baseActor.slug]);
const [batchId] = newBaseActors.length > 0 ? await knex('batches').insert({ comment: null }).returning('id') : [null];
const curatedActorEntries = batchId && curateActorEntries(newBaseActors, batchId);
const newActorEntries = batchId && await knex('actors').insert(curatedActorEntries).returning(['id', 'name', 'slug']);
const actorEntries = existingActorEntries.concat(Array.isArray(newActorEntries) ? newActorEntries : []);
console.log(actorEntries, newActorEntries, actorEntries);
}
async function getOrCreateActors(baseActors, batchId) {
const existingActors = await knex('actors')
.select('id', 'alias_for', 'name', 'slug', 'network_id')
.whereIn('slug', baseActors.map(baseActor => baseActor.slug))
.whereNull('network_id')
.orWhereIn(['slug', 'network_id'], baseActors.map(baseActor => [baseActor.slug, baseActor.network.id]));
const existingActors = await knex('actors')
.select('id', 'alias_for', 'name', 'slug', 'network_id')
.whereIn('slug', baseActors.map(baseActor => baseActor.slug))
.whereNull('network_id')
.orWhereIn(['slug', 'network_id'], baseActors.map(baseActor => [baseActor.slug, baseActor.network.id]));
// const existingActorSlugs = new Set(existingActors.map(actor => actor.slug));
const existingActorSlugs = existingActors.reduce((acc, actor) => ({
...acc,
[actor.network_id]: {
...acc[actor.network_id],
[actor.slug]: true,
},
}), {});
// const existingActorSlugs = new Set(existingActors.map(actor => actor.slug));
const existingActorSlugs = existingActors.reduce((acc, actor) => ({
...acc,
[actor.network_id]: {
...acc[actor.network_id],
[actor.slug]: true,
},
}), {});
const uniqueBaseActors = baseActors.filter(baseActor => !existingActorSlugs[baseActor.network.id]?.[baseActor.slug] && !existingActorSlugs.null?.[baseActor.slug]);
const uniqueBaseActors = baseActors.filter(baseActor => !existingActorSlugs[baseActor.network.id]?.[baseActor.slug] && !existingActorSlugs.null?.[baseActor.slug]);
const curatedActorEntries = curateActorEntries(uniqueBaseActors, batchId);
const newActors = await knex('actors').insert(curatedActorEntries, ['id', 'alias_for', 'name', 'slug', 'network_id']);
const curatedActorEntries = curateActorEntries(uniqueBaseActors, batchId);
const newActors = await knex('actors').insert(curatedActorEntries, ['id', 'alias_for', 'name', 'slug', 'network_id']);
if (Array.isArray(newActors)) {
return newActors.concat(existingActors);
}
if (Array.isArray(newActors)) {
return newActors.concat(existingActors);
}
return existingActors;
return existingActors;
}
async function associateActors(releases, batchId) {
const baseActorsByReleaseId = releases.reduce((acc, release) => {
if (release.actors) {
acc[release.id] = toBaseActors(release.actors, release);
}
const baseActorsByReleaseId = releases.reduce((acc, release) => {
if (release.actors) {
acc[release.id] = toBaseActors(release.actors, release);
}
return acc;
}, {});
return acc;
}, {});
const baseActors = Object.values(baseActorsByReleaseId).flat();
const baseActors = Object.values(baseActorsByReleaseId).flat();
if (baseActors.length === 0) {
return;
}
if (baseActors.length === 0) {
return;
}
const baseActorsBySlugAndNetworkId = baseActors.reduce((acc, baseActor) => ({
...acc,
[baseActor.slug]: {
...acc[baseActor.slug],
[baseActor.network.id]: baseActor,
},
}), {});
const baseActorsBySlugAndNetworkId = baseActors.reduce((acc, baseActor) => ({
...acc,
[baseActor.slug]: {
...acc[baseActor.slug],
[baseActor.network.id]: baseActor,
},
}), {});
const uniqueBaseActors = Object.values(baseActorsBySlugAndNetworkId).map(baseActorsByNetworkId => Object.values(baseActorsByNetworkId)).flat();
const uniqueBaseActors = Object.values(baseActorsBySlugAndNetworkId).map(baseActorsByNetworkId => Object.values(baseActorsByNetworkId)).flat();
const actors = await getOrCreateActors(uniqueBaseActors, batchId);
console.log(actors);
const actorIdsBySlugAndNetworkId = actors.reduce((acc, actor) => ({
...acc,
[actor.network_id]: {
...acc[actor.network_id],
[actor.slug]: actor.alias_for || actor.id,
},
}), {});
const actors = await getOrCreateActors(uniqueBaseActors, batchId);
console.log(actorIdsBySlugAndNetworkId);
const actorIdsBySlugAndNetworkId = actors.reduce((acc, actor) => ({
...acc,
[actor.network_id]: {
...acc[actor.network_id],
[actor.slug]: actor.alias_for || actor.id,
},
}), {});
const releaseActorAssociations = Object.entries(baseActorsByReleaseId)
.map(([releaseId, releaseActors]) => releaseActors
.map(releaseActor => ({
release_id: releaseId,
actor_id: actorIdsBySlugAndNetworkId[releaseActor.network.id]?.[releaseActor.slug] || actorIdsBySlugAndNetworkId.null[releaseActor.slug],
})))
.flat();
const releaseActorAssociations = Object.entries(baseActorsByReleaseId)
.map(([releaseId, releaseActors]) => releaseActors
.map(releaseActor => ({
release_id: releaseId,
actor_id: actorIdsBySlugAndNetworkId[releaseActor.network.id]?.[releaseActor.slug] || actorIdsBySlugAndNetworkId.null[releaseActor.slug],
})))
.flat();
await knex.raw(`${knex('releases_actors').insert(releaseActorAssociations).toString()} ON CONFLICT DO NOTHING;`);
await knex.raw(`${knex('releases_actors').insert(releaseActorAssociations).toString()} ON CONFLICT DO NOTHING;`);
}
module.exports = {
associateActors,
associateActors,
scrapeActors,
};

View File

@ -7,39 +7,39 @@ const knex = require('./knex');
const fetchUpdates = require('./updates');
const { fetchScenes, fetchMovies } = require('./deep');
const { storeReleases, updateReleasesSearch } = require('./store-releases');
const { scrapeActors } = require('./actors-legacy');
const { scrapeActors } = require('./actors');
async function init() {
if (argv.server) {
await initServer();
return;
}
if (argv.server) {
await initServer();
return;
}
if (argv.updateSearch) {
await updateReleasesSearch();
}
if (argv.updateSearch) {
await updateReleasesSearch();
}
if (argv.actors) {
await scrapeActors(argv.actors);
}
if (argv.actors) {
await scrapeActors(argv.actors);
}
const updateBaseScenes = (argv.scrape || argv.sites || argv.networks) && await fetchUpdates();
const updateBaseScenes = (argv.scrape || argv.sites || argv.networks) && await fetchUpdates();
const deepScenes = argv.deep
? await fetchScenes([...(argv.scenes || []), ...(updateBaseScenes || [])])
: updateBaseScenes;
const deepScenes = argv.deep
? await fetchScenes([...(argv.scenes || []), ...(updateBaseScenes || [])])
: updateBaseScenes;
const sceneMovies = deepScenes && argv.sceneMovies && deepScenes.map(scene => scene.movie).filter(Boolean);
const deepMovies = await fetchMovies([...(argv.movies || []), ...(sceneMovies || [])]);
const sceneMovies = deepScenes && argv.sceneMovies && deepScenes.map(scene => scene.movie).filter(Boolean);
const deepMovies = await fetchMovies([...(argv.movies || []), ...(sceneMovies || [])]);
if (argv.save) {
await storeReleases([
...(deepScenes || []),
...(deepMovies || []),
]);
}
if (argv.save) {
await storeReleases([
...(deepScenes || []),
...(deepMovies || []),
]);
}
knex.destroy();
knex.destroy();
}
module.exports = init;

View File

@ -4,188 +4,188 @@ const config = require('config');
const yargs = require('yargs');
const { argv } = yargs
.command('npm start')
.option('server', {
describe: 'Start web server',
type: 'boolean',
alias: 'web',
})
.option('scrape', {
describe: 'Scrape sites and networks defined in configuration',
type: 'boolean',
})
.option('networks', {
describe: 'Networks to scrape (overrides configuration)',
type: 'array',
alias: 'network',
})
.option('sites', {
describe: 'Sites to scrape (overrides configuration)',
type: 'array',
alias: 'site',
})
.option('actors', {
describe: 'Scrape actors by name or slug',
type: 'array',
alias: 'actor',
})
.option('actor-scenes', {
describe: 'Fetch all scenes for an actor',
type: 'boolean',
alias: 'with-releases',
default: false,
})
.option('movie-scenes', {
describe: 'Fetch all scenes for a movie',
type: 'boolean',
alias: 'with-releases',
default: false,
})
.option('scene-movies', {
describe: 'Fetch movies for scenes',
type: 'boolean',
default: true,
})
.option('profiles', {
describe: 'Scrape profiles for new actors after fetching scenes',
type: 'boolean',
alias: 'bios',
default: false,
})
.option('scene', {
describe: 'Scrape scene info from URL',
type: 'array',
alias: 'scenes',
})
.option('movie', {
describe: 'Scrape movie info from URL',
type: 'array',
alias: 'movies',
})
.option('sources', {
describe: 'Use these scrapers for actor data',
type: 'array',
alias: 'source',
})
.option('deep', {
describe: 'Fetch details for all releases',
type: 'boolean',
default: true,
})
.option('latest', {
describe: 'Scrape latest releases if available',
type: 'boolean',
default: true,
})
.option('upcoming', {
describe: 'Scrape upcoming releases if available',
type: 'boolean',
default: true,
})
.option('redownload', {
describe: 'Don\'t ignore duplicates, update existing entries',
type: 'boolean',
alias: 'force',
})
.option('after', {
describe: 'Don\'t fetch scenes older than',
type: 'string',
default: config.fetchAfter.join(' '),
})
.option('last', {
describe: 'Get the latest x releases, no matter the date range',
type: 'number',
})
.option('null-date-limit', {
describe: 'Limit amount of scenes when dates are missing.',
type: 'number',
default: config.nullDateLimit,
alias: 'limit',
})
.option('page', {
describe: 'Page to start scraping at',
type: 'number',
default: 1,
})
.option('save', {
describe: 'Save fetched releases to database',
type: 'boolean',
default: true,
})
.option('media', {
describe: 'Include any release media',
type: 'boolean',
default: true,
})
.option('media-limit', {
describe: 'Maximum amount of assets of each type per release',
type: 'number',
default: config.media.limit,
})
.option('images', {
describe: 'Include any photos, posters or covers',
type: 'boolean',
default: true,
alias: 'pics',
})
.option('videos', {
describe: 'Include any trailers or teasers',
type: 'boolean',
default: true,
})
.option('posters', {
describe: 'Include release posters',
type: 'boolean',
default: true,
alias: 'poster',
})
.option('covers', {
describe: 'Include release covers',
type: 'boolean',
default: true,
alias: 'cover',
})
.option('photos', {
describe: 'Include release photos',
type: 'boolean',
default: true,
})
.option('trailers', {
describe: 'Include release trailers',
type: 'boolean',
default: true,
alias: 'trailer',
})
.option('teasers', {
describe: 'Include release teasers',
type: 'boolean',
default: true,
alias: 'teaser',
})
.option('avatars', {
describe: 'Include actor avatars',
type: 'boolean',
default: true,
})
.option('inspect', {
describe: 'Show data in console.',
type: 'boolean',
default: false,
})
.option('level', {
describe: 'Log level',
type: 'string',
default: process.env.NODE_ENV === 'development' ? 'silly' : 'info',
})
.option('debug', {
describe: 'Show error stack traces',
type: 'boolean',
default: process.env.NODE_ENV === 'development',
})
.option('update-search', {
describe: 'Update search documents for all releases.',
type: 'boolean',
default: false,
});
.command('npm start')
.option('server', {
describe: 'Start web server',
type: 'boolean',
alias: 'web',
})
.option('scrape', {
describe: 'Scrape sites and networks defined in configuration',
type: 'boolean',
})
.option('networks', {
describe: 'Networks to scrape (overrides configuration)',
type: 'array',
alias: 'network',
})
.option('sites', {
describe: 'Sites to scrape (overrides configuration)',
type: 'array',
alias: 'site',
})
.option('actors', {
describe: 'Scrape actors by name or slug',
type: 'array',
alias: 'actor',
})
.option('actor-scenes', {
describe: 'Fetch all scenes for an actor',
type: 'boolean',
alias: 'with-releases',
default: false,
})
.option('movie-scenes', {
describe: 'Fetch all scenes for a movie',
type: 'boolean',
alias: 'with-releases',
default: false,
})
.option('scene-movies', {
describe: 'Fetch movies for scenes',
type: 'boolean',
default: true,
})
.option('profiles', {
describe: 'Scrape profiles for new actors after fetching scenes',
type: 'boolean',
alias: 'bios',
default: false,
})
.option('scene', {
describe: 'Scrape scene info from URL',
type: 'array',
alias: 'scenes',
})
.option('movie', {
describe: 'Scrape movie info from URL',
type: 'array',
alias: 'movies',
})
.option('sources', {
describe: 'Use these scrapers for actor data',
type: 'array',
alias: 'source',
})
.option('deep', {
describe: 'Fetch details for all releases',
type: 'boolean',
default: true,
})
.option('latest', {
describe: 'Scrape latest releases if available',
type: 'boolean',
default: true,
})
.option('upcoming', {
describe: 'Scrape upcoming releases if available',
type: 'boolean',
default: true,
})
.option('redownload', {
describe: 'Don\'t ignore duplicates, update existing entries',
type: 'boolean',
alias: 'force',
})
.option('after', {
describe: 'Don\'t fetch scenes older than',
type: 'string',
default: config.fetchAfter.join(' '),
})
.option('last', {
describe: 'Get the latest x releases, no matter the date range',
type: 'number',
})
.option('null-date-limit', {
describe: 'Limit amount of scenes when dates are missing.',
type: 'number',
default: config.nullDateLimit,
alias: 'limit',
})
.option('page', {
describe: 'Page to start scraping at',
type: 'number',
default: 1,
})
.option('save', {
describe: 'Save fetched releases to database',
type: 'boolean',
default: true,
})
.option('media', {
describe: 'Include any release media',
type: 'boolean',
default: true,
})
.option('media-limit', {
describe: 'Maximum amount of assets of each type per release',
type: 'number',
default: config.media.limit,
})
.option('images', {
describe: 'Include any photos, posters or covers',
type: 'boolean',
default: true,
alias: 'pics',
})
.option('videos', {
describe: 'Include any trailers or teasers',
type: 'boolean',
default: true,
})
.option('posters', {
describe: 'Include release posters',
type: 'boolean',
default: true,
alias: 'poster',
})
.option('covers', {
describe: 'Include release covers',
type: 'boolean',
default: true,
alias: 'cover',
})
.option('photos', {
describe: 'Include release photos',
type: 'boolean',
default: true,
})
.option('trailers', {
describe: 'Include release trailers',
type: 'boolean',
default: true,
alias: 'trailer',
})
.option('teasers', {
describe: 'Include release teasers',
type: 'boolean',
default: true,
alias: 'teaser',
})
.option('avatars', {
describe: 'Include actor avatars',
type: 'boolean',
default: true,
})
.option('inspect', {
describe: 'Show data in console.',
type: 'boolean',
default: false,
})
.option('level', {
describe: 'Log level',
type: 'string',
default: process.env.NODE_ENV === 'development' ? 'silly' : 'info',
})
.option('debug', {
describe: 'Show error stack traces',
type: 'boolean',
default: process.env.NODE_ENV === 'development',
})
.option('update-search', {
describe: 'Update search documents for all releases.',
type: 'boolean',
default: false,
});
module.exports = argv;

View File

@ -11,159 +11,160 @@ const { curateSites } = require('./sites');
const { curateNetworks } = require('./networks');
function urlToSiteSlug(url) {
try {
const slug = new URL(url)
.hostname
.match(/([\w-]+)\.\w+$/)?.[1];
try {
const slug = new URL(url)
.hostname
.match(/([\w-]+)\.\w+$/)?.[1];
return slug;
} catch (error) {
logger.warn(`Failed to derive site slug from '${url}': ${error.message}`);
return slug;
} catch (error) {
logger.warn(`Failed to derive site slug from '${url}': ${error.message}`);
return null;
}
return null;
}
}
async function findSites(baseReleases) {
const baseReleasesWithoutSite = baseReleases.filter(release => release.url && !release.site);
const baseReleasesWithoutSite = baseReleases.filter(release => release.url && !release.site);
const siteSlugs = Array.from(new Set(
baseReleasesWithoutSite
.map(baseRelease => urlToSiteSlug(baseRelease.url))
.filter(Boolean),
));
const siteSlugs = Array.from(new Set(
baseReleasesWithoutSite
.map(baseRelease => urlToSiteSlug(baseRelease.url))
.filter(Boolean),
));
const siteEntries = await knex('sites')
.leftJoin('networks', 'networks.id', 'sites.network_id')
.select('sites.*', 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.parameters as network_parameters', 'networks.description as network_description')
.whereIn('sites.slug', siteSlugs);
const siteEntries = await knex('sites')
.leftJoin('networks', 'networks.id', 'sites.network_id')
.select('sites.*', 'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.parameters as network_parameters', 'networks.description as network_description')
.whereIn('sites.slug', siteSlugs);
const networkEntries = await knex('networks').whereIn('slug', siteSlugs);
const networkEntries = await knex('networks').whereIn('slug', siteSlugs);
const sites = await curateSites(siteEntries, true, false);
const networks = await curateNetworks(networkEntries, true, false, false);
const markedNetworks = networks.map(network => ({ ...network, isFallback: true }));
const sites = await curateSites(siteEntries, true, false);
const networks = await curateNetworks(networkEntries, true, false, false);
const markedNetworks = networks.map(network => ({ ...network, isNetwork: true }));
const sitesBySlug = []
.concat(markedNetworks, sites)
.reduce((accSites, site) => ({ ...accSites, [site.slug]: site }), {});
const sitesBySlug = []
.concat(markedNetworks, sites)
.reduce((accSites, site) => ({ ...accSites, [site.slug]: site }), {});
return sitesBySlug;
return sitesBySlug;
}
function toBaseReleases(baseReleasesOrUrls) {
return baseReleasesOrUrls
.map((baseReleaseOrUrl) => {
if (baseReleaseOrUrl.url) {
// base release with URL
return {
...baseReleaseOrUrl,
deep: false,
};
}
return baseReleasesOrUrls
.map((baseReleaseOrUrl) => {
if (baseReleaseOrUrl.url) {
// base release with URL
return {
...baseReleaseOrUrl,
deep: false,
};
}
if (/^http/.test(baseReleaseOrUrl)) {
// URL
return {
url: baseReleaseOrUrl,
deep: false,
};
}
if (/^http/.test(baseReleaseOrUrl)) {
// URL
return {
url: baseReleaseOrUrl,
deep: false,
};
}
if (typeof baseReleaseOrUrl === 'object' && !Array.isArray(baseReleaseOrUrl)) {
// base release without URL, prepare for passthrough
return {
...baseReleaseOrUrl,
deep: false,
};
}
if (typeof baseReleaseOrUrl === 'object' && !Array.isArray(baseReleaseOrUrl)) {
// base release without URL, prepare for passthrough
return {
...baseReleaseOrUrl,
deep: false,
};
}
logger.warn(`Malformed base release, discarding '${baseReleaseOrUrl}'`);
return null;
})
.filter(Boolean);
logger.warn(`Malformed base release, discarding '${baseReleaseOrUrl}'`);
return null;
})
.filter(Boolean);
}
async function scrapeRelease(baseRelease, sites, type = 'scene') {
const site = baseRelease.site || sites[urlToSiteSlug(baseRelease.url)];
const site = baseRelease.site || sites[urlToSiteSlug(baseRelease.url)];
const siteWithFallbackNetwork = site.isNetwork ? { ...site, network: site } : site; // make site.network available, even when site is network fallback
if (!site) {
logger.warn(`No site available for ${baseRelease.url}`);
return baseRelease;
}
if (!site) {
logger.warn(`No site available for ${baseRelease.url}`);
return baseRelease;
}
if ((!baseRelease.url && !baseRelease.path) || !argv.deep) {
return {
...baseRelease,
site,
};
}
if ((!baseRelease.url && !baseRelease.path) || !argv.deep) {
return {
...baseRelease,
site,
};
}
const scraper = scrapers.releases[site.slug] || scrapers.releases[site.network.slug];
const scraper = scrapers.releases[site.slug] || scrapers.releases[site.network.slug];
if (!scraper) {
logger.warn(`Could not find scraper for ${baseRelease.url}`);
return baseRelease;
}
if (!scraper) {
logger.warn(`Could not find scraper for ${baseRelease.url}`);
return baseRelease;
}
if ((type === 'scene' && !scraper.fetchScene) || (type === 'movie' && !scraper.fetchMovie)) {
logger.warn(`The '${site.name}'-scraper cannot fetch individual ${type}s`);
return baseRelease;
}
if ((type === 'scene' && !scraper.fetchScene) || (type === 'movie' && !scraper.fetchMovie)) {
logger.warn(`The '${site.name}'-scraper cannot fetch individual ${type}s`);
return baseRelease;
}
try {
logger.verbose(`Fetching ${type} ${baseRelease.url}`);
try {
logger.verbose(`Fetching ${type} ${baseRelease.url}`);
const scrapedRelease = type === 'scene'
? await scraper.fetchScene(baseRelease.url, site, baseRelease, null, include)
: await scraper.fetchMovie(baseRelease.url, site, baseRelease, null, include);
const scrapedRelease = type === 'scene'
? await scraper.fetchScene(baseRelease.url, siteWithFallbackNetwork, baseRelease, null, include)
: await scraper.fetchMovie(baseRelease.url, siteWithFallbackNetwork, baseRelease, null, include);
const mergedRelease = {
...baseRelease,
...scrapedRelease,
deep: !!scrapedRelease,
site,
};
const mergedRelease = {
...baseRelease,
...scrapedRelease,
deep: !!scrapedRelease,
site,
};
if (scrapedRelease && baseRelease?.tags) {
// accumulate all available tags
mergedRelease.tags = baseRelease.tags.concat(scrapedRelease.tags);
}
if (scrapedRelease && baseRelease?.tags) {
// accumulate all available tags
mergedRelease.tags = baseRelease.tags.concat(scrapedRelease.tags);
}
return mergedRelease;
} catch (error) {
logger.error(`Deep scrape failed for ${baseRelease.url}: ${error.message}`);
return baseRelease;
}
return mergedRelease;
} catch (error) {
logger.error(`Deep scrape failed for ${baseRelease.url}: ${error.message}`);
return baseRelease;
}
}
async function scrapeReleases(baseReleases, sites, type) {
return Promise.map(
baseReleases,
async baseRelease => scrapeRelease(baseRelease, sites, type),
{ concurrency: 10 },
);
return Promise.map(
baseReleases,
async baseRelease => scrapeRelease(baseRelease, sites, type),
{ concurrency: 10 },
);
}
async function fetchReleases(baseReleasesOrUrls, type = 'scene') {
const baseReleases = toBaseReleases(baseReleasesOrUrls);
const sites = await findSites(baseReleases);
const baseReleases = toBaseReleases(baseReleasesOrUrls);
const sites = await findSites(baseReleases);
const deepReleases = await scrapeReleases(baseReleases, sites, type);
const deepReleases = await scrapeReleases(baseReleases, sites, type);
return deepReleases;
return deepReleases;
}
async function fetchScenes(baseReleasesOrUrls) {
return fetchReleases(baseReleasesOrUrls, 'scene');
return fetchReleases(baseReleasesOrUrls, 'scene');
}
async function fetchMovies(baseReleasesOrUrls) {
return fetchReleases(baseReleasesOrUrls, 'movie');
return fetchReleases(baseReleasesOrUrls, 'movie');
}
module.exports = {
fetchReleases,
fetchScenes,
fetchMovies,
fetchReleases,
fetchScenes,
fetchMovies,
};

View File

@ -4,8 +4,8 @@ const config = require('config');
const knex = require('knex');
module.exports = knex({
client: 'pg',
connection: config.database,
// performance overhead, don't use asyncStackTraces in production
asyncStackTraces: process.env.NODE_ENV === 'development',
client: 'pg',
connection: config.database,
// performance overhead, don't use asyncStackTraces in production
asyncStackTraces: process.env.NODE_ENV === 'development',
});

View File

@ -9,31 +9,31 @@ require('winston-daily-rotate-file');
const args = require('./argv');
function logger(filepath) {
const root = filepath.match(/src\/|dist\//);
const filename = filepath.slice(root.index + root[0].length)
.replace(path.extname(filepath), '');
const root = filepath.match(/src\/|dist\//);
const filename = filepath.slice(root.index + root[0].length)
.replace(path.extname(filepath), '');
return winston.createLogger({
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format(info => (info instanceof Error
? { ...info, message: info.stack }
: { ...info, message: typeof info.message === 'string' ? info.message : util.inspect(info.message) }))(),
winston.format.colorize(),
winston.format.printf(({ level, timestamp, label, message }) => `${timestamp} ${level} [${label || filename}] ${message}`),
),
transports: [
new winston.transports.Console({
level: args.level,
timestamp: true,
}),
new winston.transports.DailyRotateFile({
datePattern: 'YYYY-MM-DD',
filename: 'log/%DATE%.log',
level: 'silly',
}),
],
});
return winston.createLogger({
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format(info => (info instanceof Error
? { ...info, message: info.stack }
: { ...info, message: typeof info.message === 'string' ? info.message : util.inspect(info.message) }))(),
winston.format.colorize(),
winston.format.printf(({ level, timestamp, label, message }) => `${timestamp} ${level} [${label || filename}] ${message}`),
),
transports: [
new winston.transports.Console({
level: args.level,
timestamp: true,
}),
new winston.transports.DailyRotateFile({
datePattern: 'YYYY-MM-DD',
filename: 'log/%DATE%.log',
level: 'silly',
}),
],
});
}
module.exports = logger;

File diff suppressed because it is too large Load Diff

View File

@ -5,77 +5,77 @@ const whereOr = require('./utils/where-or');
const { fetchSites } = require('./sites');
async function curateNetwork(network, includeParameters = false, includeSites = true, includeStudios = false) {
const curatedNetwork = {
id: network.id,
name: network.name,
url: network.url,
description: network.description,
slug: network.slug,
parameters: includeParameters ? network.parameters : null,
};
const curatedNetwork = {
id: network.id,
name: network.name,
url: network.url,
description: network.description,
slug: network.slug,
parameters: includeParameters ? network.parameters : null,
};
if (includeSites) {
curatedNetwork.sites = await fetchSites({ network_id: network.id });
}
if (includeSites) {
curatedNetwork.sites = await fetchSites({ network_id: network.id });
}
if (includeStudios) {
const studios = await knex('studios').where({ network_id: network.id });
if (includeStudios) {
const studios = await knex('studios').where({ network_id: network.id });
curatedNetwork.studios = studios.map(studio => ({
id: studio.id,
name: studio.name,
url: studio.url,
description: studio.description,
slug: studio.slug,
}));
}
curatedNetwork.studios = studios.map(studio => ({
id: studio.id,
name: studio.name,
url: studio.url,
description: studio.description,
slug: studio.slug,
}));
}
return curatedNetwork;
return curatedNetwork;
}
function curateNetworks(releases) {
return Promise.all(releases.map(async release => curateNetwork(release)));
return Promise.all(releases.map(async release => curateNetwork(release)));
}
async function findNetworkByUrl(url) {
const { hostname } = new URL(url);
const domain = hostname.replace(/^www./, '');
const { hostname } = new URL(url);
const domain = hostname.replace(/^www./, '');
const network = await knex('networks')
.where('networks.url', 'like', `%${domain}`)
.orWhere('networks.url', url)
.first();
const network = await knex('networks')
.where('networks.url', 'like', `%${domain}`)
.orWhere('networks.url', url)
.first();
if (network) {
return curateNetwork(network, true);
}
if (network) {
return curateNetwork(network, true);
}
return null;
return null;
}
async function fetchNetworks(queryObject) {
const releases = await knex('networks')
.where(builder => whereOr(queryObject, 'networks', builder))
.limit(100);
const releases = await knex('networks')
.where(builder => whereOr(queryObject, 'networks', builder))
.limit(100);
return curateNetworks(releases);
return curateNetworks(releases);
}
async function fetchNetworksFromReleases() {
const releases = await knex('releases')
.select('site_id', '')
.leftJoin('sites', 'sites.id', 'releases.site_id')
.leftJoin('networks', 'networks.id', 'sites.network_id')
.groupBy('networks.id')
.limit(100);
const releases = await knex('releases')
.select('site_id', '')
.leftJoin('sites', 'sites.id', 'releases.site_id')
.leftJoin('networks', 'networks.id', 'sites.network_id')
.groupBy('networks.id')
.limit(100);
return curateNetworks(releases);
return curateNetworks(releases);
}
module.exports = {
curateNetwork,
curateNetworks,
fetchNetworks,
fetchNetworksFromReleases,
findNetworkByUrl,
curateNetwork,
curateNetworks,
fetchNetworks,
fetchNetworksFromReleases,
findNetworkByUrl,
};

View File

@ -11,356 +11,356 @@ const whereOr = require('./utils/where-or');
const { associateTags } = require('./tags');
const { associateActors, scrapeBasicActors } = require('./actors');
const {
pluckItems,
storeMedia,
associateMedia,
pluckItems,
storeMedia,
associateMedia,
} = require('./media');
const { fetchSites } = require('./sites');
const slugify = require('./utils/slugify');
const capitalize = require('./utils/capitalize');
function commonQuery(queryBuilder, {
filter = [],
after = new Date(0), // January 1970
before = new Date(2 ** 44), // May 2109
limit = 100,
filter = [],
after = new Date(0), // January 1970
before = new Date(2 ** 44), // May 2109
limit = 100,
}) {
const finalFilter = [].concat(filter); // ensure filter is array
const finalFilter = [].concat(filter); // ensure filter is array
queryBuilder
.leftJoin('sites', 'releases.site_id', 'sites.id')
.leftJoin('studios', 'releases.studio_id', 'studios.id')
.leftJoin('networks', 'sites.network_id', 'networks.id')
.select(
'releases.*',
'sites.name as site_name', 'sites.slug as site_slug', 'sites.url as site_url', 'sites.network_id', 'sites.parameters as site_parameters',
'studios.name as studio_name', 'sites.slug as site_slug', 'studios.url as studio_url',
'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description',
)
.whereNotExists((builder) => {
// apply tag filters
builder
.select('*')
.from('tags_associated')
.leftJoin('tags', 'tags_associated.tag_id', 'tags.id')
.whereIn('tags.slug', finalFilter)
.where('tags_associated.domain', 'releases')
.whereRaw('tags_associated.target_id = releases.id');
})
.andWhere('releases.date', '>', after)
.andWhere('releases.date', '<=', before)
.orderBy([{ column: 'date', order: 'desc' }, { column: 'created_at', order: 'desc' }])
.limit(limit);
queryBuilder
.leftJoin('sites', 'releases.site_id', 'sites.id')
.leftJoin('studios', 'releases.studio_id', 'studios.id')
.leftJoin('networks', 'sites.network_id', 'networks.id')
.select(
'releases.*',
'sites.name as site_name', 'sites.slug as site_slug', 'sites.url as site_url', 'sites.network_id', 'sites.parameters as site_parameters',
'studios.name as studio_name', 'sites.slug as site_slug', 'studios.url as studio_url',
'networks.name as network_name', 'networks.slug as network_slug', 'networks.url as network_url', 'networks.description as network_description',
)
.whereNotExists((builder) => {
// apply tag filters
builder
.select('*')
.from('tags_associated')
.leftJoin('tags', 'tags_associated.tag_id', 'tags.id')
.whereIn('tags.slug', finalFilter)
.where('tags_associated.domain', 'releases')
.whereRaw('tags_associated.target_id = releases.id');
})
.andWhere('releases.date', '>', after)
.andWhere('releases.date', '<=', before)
.orderBy([{ column: 'date', order: 'desc' }, { column: 'created_at', order: 'desc' }])
.limit(limit);
}
async function curateRelease(release) {
const [actors, tags, media] = await Promise.all([
knex('actors_associated')
.select(
'actors.id', 'actors.name', 'actors.gender', 'actors.slug', 'actors.birthdate',
'birth_countries.alpha2 as birth_country_alpha2', 'birth_countries.name as birth_country_name', 'birth_countries.alias as birth_country_alias',
'media.thumbnail as avatar',
)
.where({ release_id: release.id })
.leftJoin('actors', 'actors.id', 'actors_associated.actor_id')
.leftJoin('countries as birth_countries', 'actors.birth_country_alpha2', 'birth_countries.alpha2')
.leftJoin('media', (builder) => {
builder
.on('media.target_id', 'actors.id')
.andOnVal('media.domain', 'actors')
.andOnVal('media.index', '0');
})
.orderBy('actors.gender'),
knex('tags_associated')
.select('tags.name', 'tags.slug')
.where({
domain: 'releases',
target_id: release.id,
})
.leftJoin('tags', 'tags.id', 'tags_associated.tag_id')
.orderBy('tags.priority', 'desc'),
knex('media')
.where({
target_id: release.id,
domain: 'releases',
})
.orderBy(['role', 'index']),
]);
const [actors, tags, media] = await Promise.all([
knex('actors_associated')
.select(
'actors.id', 'actors.name', 'actors.gender', 'actors.slug', 'actors.birthdate',
'birth_countries.alpha2 as birth_country_alpha2', 'birth_countries.name as birth_country_name', 'birth_countries.alias as birth_country_alias',
'media.thumbnail as avatar',
)
.where({ release_id: release.id })
.leftJoin('actors', 'actors.id', 'actors_associated.actor_id')
.leftJoin('countries as birth_countries', 'actors.birth_country_alpha2', 'birth_countries.alpha2')
.leftJoin('media', (builder) => {
builder
.on('media.target_id', 'actors.id')
.andOnVal('media.domain', 'actors')
.andOnVal('media.index', '0');
})
.orderBy('actors.gender'),
knex('tags_associated')
.select('tags.name', 'tags.slug')
.where({
domain: 'releases',
target_id: release.id,
})
.leftJoin('tags', 'tags.id', 'tags_associated.tag_id')
.orderBy('tags.priority', 'desc'),
knex('media')
.where({
target_id: release.id,
domain: 'releases',
})
.orderBy(['role', 'index']),
]);
const curatedRelease = {
id: release.id,
type: release.type,
title: release.title,
date: release.date,
dateAdded: release.created_at,
description: release.description,
url: release.url,
shootId: release.shoot_id,
entryId: release.entry_id,
actors: actors.map(actor => ({
id: actor.id,
slug: actor.slug,
name: actor.name,
gender: actor.gender,
birthdate: actor.birthdate,
age: moment().diff(actor.birthdate, 'years'),
ageThen: moment(release.date).diff(actor.birthdate, 'years'),
avatar: actor.avatar,
origin: actor.birth_country_alpha2
? {
country: {
name: actor.birth_country_alias,
alpha2: actor.birth_country_alpha2,
},
}
: null,
})),
director: release.director,
tags,
duration: release.duration,
photos: media.filter(item => item.role === 'photo'),
poster: media.filter(item => item.role === 'poster')[0],
covers: media.filter(item => item.role === 'cover'),
trailer: media.filter(item => item.role === 'trailer')[0],
site: {
id: release.site_id,
name: release.site_name,
independent: !!release.site_parameters?.independent,
slug: release.site_slug,
url: release.site_url,
},
studio: release.studio_id
? {
id: release.studio_id,
name: release.studio_name,
slug: release.studio_slug,
url: release.studio_url,
}
: null,
network: {
id: release.network_id,
name: release.network_name,
description: release.network_description,
slug: release.network_slug,
url: release.network_url,
},
};
const curatedRelease = {
id: release.id,
type: release.type,
title: release.title,
date: release.date,
dateAdded: release.created_at,
description: release.description,
url: release.url,
shootId: release.shoot_id,
entryId: release.entry_id,
actors: actors.map(actor => ({
id: actor.id,
slug: actor.slug,
name: actor.name,
gender: actor.gender,
birthdate: actor.birthdate,
age: moment().diff(actor.birthdate, 'years'),
ageThen: moment(release.date).diff(actor.birthdate, 'years'),
avatar: actor.avatar,
origin: actor.birth_country_alpha2
? {
country: {
name: actor.birth_country_alias,
alpha2: actor.birth_country_alpha2,
},
}
: null,
})),
director: release.director,
tags,
duration: release.duration,
photos: media.filter(item => item.role === 'photo'),
poster: media.filter(item => item.role === 'poster')[0],
covers: media.filter(item => item.role === 'cover'),
trailer: media.filter(item => item.role === 'trailer')[0],
site: {
id: release.site_id,
name: release.site_name,
independent: !!release.site_parameters?.independent,
slug: release.site_slug,
url: release.site_url,
},
studio: release.studio_id
? {
id: release.studio_id,
name: release.studio_name,
slug: release.studio_slug,
url: release.studio_url,
}
: null,
network: {
id: release.network_id,
name: release.network_name,
description: release.network_description,
slug: release.network_slug,
url: release.network_url,
},
};
return curatedRelease;
return curatedRelease;
}
function curateReleases(releases) {
return Promise.all(releases.map(async release => curateRelease(release)));
return Promise.all(releases.map(async release => curateRelease(release)));
}
async function attachChannelSite(release) {
if (!release.site?.isFallback && !release.channel?.force) {
return release;
}
if (!release.site?.isFallback && !release.channel?.force) {
return release;
}
if (!release.channel) {
throw new Error(`Unable to derive channel site from generic URL: ${release.url}`);
}
if (!release.channel) {
throw new Error(`Unable to derive channel site from generic URL: ${release.url}`);
}
const [site] = await fetchSites({
name: release.channel.name || release.channel,
slug: release.channel.slug || release.channel,
});
const [site] = await fetchSites({
name: release.channel.name || release.channel,
slug: release.channel.slug || release.channel,
});
if (site) {
return {
...release,
site,
};
}
if (site) {
return {
...release,
site,
};
}
throw new Error(`Unable to match channel '${release.channel.slug || release.channel}' from generic URL: ${release.url}`);
throw new Error(`Unable to match channel '${release.channel.slug || release.channel}' from generic URL: ${release.url}`);
}
async function attachStudio(release) {
if (!release.studio) {
return release;
}
if (!release.studio) {
return release;
}
const studio = await knex('studios')
.where('name', release.studio)
.orWhere('slug', release.studio)
.orWhere('url', release.studio)
.first();
const studio = await knex('studios')
.where('name', release.studio)
.orWhere('slug', release.studio)
.orWhere('url', release.studio)
.first();
return {
...release,
studio,
};
return {
...release,
studio,
};
}
async function curateReleaseEntry(release, batchId, existingRelease) {
const slug = slugify(release.title, {
encode: true,
limit: config.titleSlugLength,
});
const slug = slugify(release.title, {
encode: true,
limit: config.titleSlugLength,
});
const curatedRelease = {
site_id: release.site.id,
studio_id: release.studio ? release.studio.id : null,
shoot_id: release.shootId || null,
entry_id: release.entryId || null,
type: release.type,
url: release.url,
title: release.title,
slug,
date: release.date,
description: release.description,
// director: release.director,
duration: release.duration,
// likes: release.rating && release.rating.likes,
// dislikes: release.rating && release.rating.dislikes,
// rating: release.rating && release.rating.stars && Math.floor(release.rating.stars),
deep: typeof release.deep === 'boolean' ? release.deep : false,
deep_url: release.deepUrl,
updated_batch_id: batchId,
...(!existingRelease && { created_batch_id: batchId }),
};
const curatedRelease = {
site_id: release.site.id,
studio_id: release.studio ? release.studio.id : null,
shoot_id: release.shootId || null,
entry_id: release.entryId || null,
type: release.type,
url: release.url,
title: release.title,
slug,
date: release.date,
description: release.description,
// director: release.director,
duration: release.duration,
// likes: release.rating && release.rating.likes,
// dislikes: release.rating && release.rating.dislikes,
// rating: release.rating && release.rating.stars && Math.floor(release.rating.stars),
deep: typeof release.deep === 'boolean' ? release.deep : false,
deep_url: release.deepUrl,
updated_batch_id: batchId,
...(!existingRelease && { created_batch_id: batchId }),
};
return curatedRelease;
return curatedRelease;
}
async function fetchReleases(queryObject = {}, options = {}) {
const releases = await knex('releases')
.modify(commonQuery, options)
.andWhere(builder => whereOr(queryObject, 'releases', builder));
const releases = await knex('releases')
.modify(commonQuery, options)
.andWhere(builder => whereOr(queryObject, 'releases', builder));
return curateReleases(releases);
return curateReleases(releases);
}
async function fetchSiteReleases(queryObject, options = {}) {
const releases = await knex('releases')
.modify(commonQuery, options)
.where(builder => whereOr(queryObject, 'sites', builder));
const releases = await knex('releases')
.modify(commonQuery, options)
.where(builder => whereOr(queryObject, 'sites', builder));
return curateReleases(releases);
return curateReleases(releases);
}
async function fetchNetworkReleases(queryObject, options = {}) {
const releases = await knex('releases')
.modify(commonQuery, options)
.where(builder => whereOr(queryObject, 'networks', builder));
const releases = await knex('releases')
.modify(commonQuery, options)
.where(builder => whereOr(queryObject, 'networks', builder));
return curateReleases(releases);
return curateReleases(releases);
}
async function fetchActorReleases(queryObject, options = {}) {
const releases = await knex('actors_associated')
.leftJoin('releases', 'actors_associated.release_id', 'releases.id')
.leftJoin('actors', 'actors_associated.actor_id', 'actors.id')
.select(
'actors.name as actor_name',
)
.modify(commonQuery, options)
.where(builder => whereOr(queryObject, 'actors', builder));
const releases = await knex('actors_associated')
.leftJoin('releases', 'actors_associated.release_id', 'releases.id')
.leftJoin('actors', 'actors_associated.actor_id', 'actors.id')
.select(
'actors.name as actor_name',
)
.modify(commonQuery, options)
.where(builder => whereOr(queryObject, 'actors', builder));
return curateReleases(releases);
return curateReleases(releases);
}
async function fetchTagReleases(queryObject, options = {}) {
const releases = await knex('tags_associated')
.leftJoin('releases', 'tags_associated.target_id', 'releases.id')
.leftJoin('tags', 'tags_associated.tag_id', 'tags.id')
.select(
'tags.name as tag_name',
)
.modify(commonQuery, options)
.where('tags_associated.domain', 'releases')
.where(builder => whereOr(queryObject, 'tags', builder));
const releases = await knex('tags_associated')
.leftJoin('releases', 'tags_associated.target_id', 'releases.id')
.leftJoin('tags', 'tags_associated.tag_id', 'tags.id')
.select(
'tags.name as tag_name',
)
.modify(commonQuery, options)
.where('tags_associated.domain', 'releases')
.where(builder => whereOr(queryObject, 'tags', builder));
return curateReleases(releases);
return curateReleases(releases);
}
function accumulateActors(releases) {
return releases.reduce((acc, release) => {
if (!Array.isArray(release.actors)) return acc;
return releases.reduce((acc, release) => {
if (!Array.isArray(release.actors)) return acc;
release.actors.forEach((actor) => {
const actorName = actor.name ? actor.name.trim() : actor.trim();
const actorSlug = slugify(actorName);
release.actors.forEach((actor) => {
const actorName = actor.name ? actor.name.trim() : actor.trim();
const actorSlug = slugify(actorName);
if (!actorSlug) return;
if (!actorSlug) return;
if (!acc[actorSlug]) {
acc[actorSlug] = {
name: actorName,
slug: actorSlug,
releaseIds: new Set(),
avatars: [],
};
}
if (!acc[actorSlug]) {
acc[actorSlug] = {
name: actorName,
slug: actorSlug,
releaseIds: new Set(),
avatars: [],
};
}
acc[actorSlug].releaseIds.add(release.id);
acc[actorSlug].releaseIds.add(release.id);
if (actor.name) acc[actorSlug] = { ...acc[actorSlug], ...actor }; // actor input contains profile info
if (actor.avatar) {
const avatar = Array.isArray(actor.avatar)
? actor.avatar.map(avatarX => ({
src: avatarX.src || avatarX,
copyright: avatarX.copyright === undefined ? capitalize(release.site?.network?.name) : avatarX.copyright,
}))
: {
src: actor.avatar.src || actor.avatar,
copyright: actor.avatar.copyright === undefined ? capitalize(release.site?.network?.name) : actor.avatar.copyright,
};
if (actor.name) acc[actorSlug] = { ...acc[actorSlug], ...actor }; // actor input contains profile info
if (actor.avatar) {
const avatar = Array.isArray(actor.avatar)
? actor.avatar.map(avatarX => ({
src: avatarX.src || avatarX,
copyright: avatarX.copyright === undefined ? capitalize(release.site?.network?.name) : avatarX.copyright,
}))
: {
src: actor.avatar.src || actor.avatar,
copyright: actor.avatar.copyright === undefined ? capitalize(release.site?.network?.name) : actor.avatar.copyright,
};
acc[actorSlug].avatars = acc[actorSlug].avatars.concat([avatar]); // don't flatten fallbacks
}
});
acc[actorSlug].avatars = acc[actorSlug].avatars.concat([avatar]); // don't flatten fallbacks
}
});
return acc;
}, {});
return acc;
}, {});
}
async function storeReleaseAssets(releases) {
if (!argv.media) {
return;
}
if (!argv.media) {
return;
}
const releasePostersById = releases.reduce((acc, release) => ({ ...acc, [release.id]: [release.poster] }), {});
const releaseCoversById = releases.reduce((acc, release) => ({ ...acc, [release.id]: release.covers }), {});
const releaseTrailersById = releases.reduce((acc, release) => ({ ...acc, [release.id]: [release.trailer] }), {});
const releaseTeasersById = releases.reduce((acc, release) => ({ ...acc, [release.id]: [release.teaser] }), {});
const releasePhotosById = releases.reduce((acc, release) => ({
...acc,
[release.id]: pluckItems(release.photos),
}), {});
const releasePostersById = releases.reduce((acc, release) => ({ ...acc, [release.id]: [release.poster] }), {});
const releaseCoversById = releases.reduce((acc, release) => ({ ...acc, [release.id]: release.covers }), {});
const releaseTrailersById = releases.reduce((acc, release) => ({ ...acc, [release.id]: [release.trailer] }), {});
const releaseTeasersById = releases.reduce((acc, release) => ({ ...acc, [release.id]: [release.teaser] }), {});
const releasePhotosById = releases.reduce((acc, release) => ({
...acc,
[release.id]: pluckItems(release.photos),
}), {});
if (argv.images && argv.posters) {
const posters = await storeMedia(Object.values(releasePostersById).flat(), 'release', 'poster');
if (posters) await associateMedia(releasePostersById, posters, 'release', 'poster');
}
if (argv.images && argv.posters) {
const posters = await storeMedia(Object.values(releasePostersById).flat(), 'release', 'poster');
if (posters) await associateMedia(releasePostersById, posters, 'release', 'poster');
}
if (argv.images && argv.covers) {
const covers = await storeMedia(Object.values(releaseCoversById).flat(), 'release', 'cover');
if (covers) await associateMedia(releaseCoversById, covers, 'release', 'cover');
}
if (argv.images && argv.covers) {
const covers = await storeMedia(Object.values(releaseCoversById).flat(), 'release', 'cover');
if (covers) await associateMedia(releaseCoversById, covers, 'release', 'cover');
}
if (argv.images && argv.photos) {
const photos = await storeMedia(Object.values(releasePhotosById).flat(), 'release', 'photo');
if (photos) await associateMedia(releasePhotosById, photos, 'release', 'photo');
}
if (argv.images && argv.photos) {
const photos = await storeMedia(Object.values(releasePhotosById).flat(), 'release', 'photo');
if (photos) await associateMedia(releasePhotosById, photos, 'release', 'photo');
}
if (argv.videos && argv.trailers) {
const trailers = await storeMedia(Object.values(releaseTrailersById).flat(), 'release', 'trailer');
if (trailers) await associateMedia(releaseTrailersById, trailers, 'release', 'trailer');
}
if (argv.videos && argv.trailers) {
const trailers = await storeMedia(Object.values(releaseTrailersById).flat(), 'release', 'trailer');
if (trailers) await associateMedia(releaseTrailersById, trailers, 'release', 'trailer');
}
if (argv.videos && argv.teasers) {
const teasers = await storeMedia(Object.values(releaseTeasersById).flat(), 'release', 'teaser');
if (teasers) await associateMedia(releaseTeasersById, teasers, 'release', 'teaser');
}
if (argv.videos && argv.teasers) {
const teasers = await storeMedia(Object.values(releaseTeasersById).flat(), 'release', 'teaser');
if (teasers) await associateMedia(releaseTeasersById, teasers, 'release', 'teaser');
}
}
async function updateReleasesSearch(releaseIds) {
logger.info(`Updating search documents for ${releaseIds ? releaseIds.length : 'all' } releases`);
logger.info(`Updating search documents for ${releaseIds ? releaseIds.length : 'all' } releases`);
const documents = await knex.raw(`
const documents = await knex.raw(`
SELECT
releases.id AS release_id,
TO_TSVECTOR(
@ -391,117 +391,117 @@ async function updateReleasesSearch(releaseIds) {
GROUP BY releases.id, sites.name, sites.slug, sites.alias, sites.url, networks.name, networks.slug, networks.url;
`, releaseIds && [releaseIds]);
if (documents.rows?.length > 0) {
const query = knex('releases_search').insert(documents.rows).toString();
await knex.raw(`${query} ON CONFLICT (release_id) DO UPDATE SET document = EXCLUDED.document`);
}
if (documents.rows?.length > 0) {
const query = knex('releases_search').insert(documents.rows).toString();
await knex.raw(`${query} ON CONFLICT (release_id) DO UPDATE SET document = EXCLUDED.document`);
}
}
async function storeRelease(release, batchId) {
if (!release.site) {
throw new Error(`Missing site, unable to store "${release.title}" (${release.url})`);
}
if (!release.site) {
throw new Error(`Missing site, unable to store "${release.title}" (${release.url})`);
}
if (!release.entryId) {
logger.warn(`Missing entry ID, unable to store "${release.title}" (${release.url})`);
return null;
}
if (!release.entryId) {
logger.warn(`Missing entry ID, unable to store "${release.title}" (${release.url})`);
return null;
}
const existingRelease = await knex('releases')
.where({
entry_id: release.entryId,
site_id: release.site.id,
})
.first();
const existingRelease = await knex('releases')
.where({
entry_id: release.entryId,
site_id: release.site.id,
})
.first();
const curatedRelease = await curateReleaseEntry(release, batchId, existingRelease);
const curatedRelease = await curateReleaseEntry(release, batchId, existingRelease);
if (existingRelease && !argv.redownload) {
return existingRelease;
}
if (existingRelease && !argv.redownload) {
return existingRelease;
}
if (existingRelease && argv.redownload) {
const [updatedRelease] = await knex('releases')
.where('id', existingRelease.id)
.update({
...existingRelease,
...curatedRelease,
})
.returning('*');
if (existingRelease && argv.redownload) {
const [updatedRelease] = await knex('releases')
.where('id', existingRelease.id)
.update({
...existingRelease,
...curatedRelease,
})
.returning('*');
if (updatedRelease) {
await associateTags(release, updatedRelease.id);
logger.info(`Updated release "${release.title}" (${existingRelease.id}, ${release.site.name})`);
}
if (updatedRelease) {
await associateTags(release, updatedRelease.id);
logger.info(`Updated release "${release.title}" (${existingRelease.id}, ${release.site.name})`);
}
await associateTags(release, existingRelease.id);
await associateTags(release, existingRelease.id);
return existingRelease;
}
return existingRelease;
}
const [releaseEntry] = await knex('releases')
.insert(curatedRelease)
.returning('*');
const [releaseEntry] = await knex('releases')
.insert(curatedRelease)
.returning('*');
await associateTags(release, releaseEntry.id);
await associateTags(release, releaseEntry.id);
logger.info(`Stored release "${release.title}" (${releaseEntry.id}, ${release.site.name})`);
logger.info(`Stored release "${release.title}" (${releaseEntry.id}, ${release.site.name})`);
return releaseEntry;
return releaseEntry;
}
async function storeReleases(releases) {
const [batchId] = await knex('batches').insert({ comment: null }).returning('id');
const [batchId] = await knex('batches').insert({ comment: null }).returning('id');
const storedReleases = await Promise.map(releases, async (release) => {
try {
const releaseWithChannelSite = await attachChannelSite(release);
const releaseWithStudio = await attachStudio(releaseWithChannelSite);
const storedRelease = await storeRelease(releaseWithStudio, batchId);
const storedReleases = await Promise.map(releases, async (release) => {
try {
const releaseWithChannelSite = await attachChannelSite(release);
const releaseWithStudio = await attachStudio(releaseWithChannelSite);
const storedRelease = await storeRelease(releaseWithStudio, batchId);
return storedRelease && {
id: storedRelease.id,
slug: storedRelease.slug,
...releaseWithChannelSite,
};
} catch (error) {
logger.error(error);
return storedRelease && {
id: storedRelease.id,
slug: storedRelease.slug,
...releaseWithChannelSite,
};
} catch (error) {
logger.error(error);
return null;
}
}, {
concurrency: 10,
}).filter(Boolean);
return null;
}
}, {
concurrency: 10,
}).filter(Boolean);
logger.info(`Stored ${storedReleases.length} new releases`);
logger.info(`Stored ${storedReleases.length} new releases`);
const actors = accumulateActors(storedReleases);
const actors = accumulateActors(storedReleases);
await associateActors(actors, storedReleases);
await associateActors(actors, storedReleases);
await Promise.all([
// actors need to be stored before generating search
updateReleasesSearch(storedReleases.map(release => release.id)),
storeReleaseAssets(storedReleases),
]);
await Promise.all([
// actors need to be stored before generating search
updateReleasesSearch(storedReleases.map(release => release.id)),
storeReleaseAssets(storedReleases),
]);
if (argv.withProfiles && Object.keys(actors).length > 0) {
await scrapeBasicActors();
}
if (argv.withProfiles && Object.keys(actors).length > 0) {
await scrapeBasicActors();
}
return {
releases: storedReleases,
actors,
};
return {
releases: storedReleases,
actors,
};
}
module.exports = {
fetchReleases,
fetchActorReleases,
fetchSiteReleases,
fetchNetworkReleases,
fetchTagReleases,
storeRelease,
storeReleases,
updateReleasesSearch,
fetchReleases,
fetchActorReleases,
fetchSiteReleases,
fetchNetworkReleases,
fetchTagReleases,
storeRelease,
storeReleases,
updateReleasesSearch,
};

View File

@ -3,18 +3,18 @@
const knex = require('./knex');
async function fetchReleases(limit = 100) {
const releases = await knex('releases').limit(limit);
const releases = await knex('releases').limit(limit);
return releases;
return releases;
}
async function searchReleases(query, limit = 100) {
const releases = await knex.raw('SELECT * FROM search_releases(?) LIMIT ?;', [query, limit]);
const releases = await knex.raw('SELECT * FROM search_releases(?) LIMIT ?;', [query, limit]);
return releases.rows;
return releases.rows;
}
module.exports = {
fetchReleases,
searchReleases,
fetchReleases,
searchReleases,
};

View File

@ -1,199 +0,0 @@
'use strict';
const config = require('config');
const Promise = require('bluebird');
const logger = require('./logger')(__filename);
const argv = require('./argv');
const include = require('./utils/argv-include')(argv);
const knex = require('./knex');
const scrapers = require('./scrapers/scrapers');
const { findSiteByUrl } = require('./sites');
const { findNetworkByUrl } = require('./networks');
const { storeReleases } = require('./releases');
async function findSite(url, release) {
if (release?.site) return release.site;
if (!url) return null;
const site = await findSiteByUrl(url);
if (site) {
return site;
}
const network = await findNetworkByUrl(url);
if (network) {
return {
...network,
network,
isFallback: true,
};
}
return null;
}
async function scrapeRelease(source, basicRelease = null, type = 'scene', beforeFetchLatest) {
// profile scraper may return either URLs or pre-scraped scenes
const sourceIsUrlOrEmpty = typeof source === 'string' || source === undefined;
const url = sourceIsUrlOrEmpty ? source : source?.url;
const release = sourceIsUrlOrEmpty ? basicRelease : source;
const site = basicRelease?.site || await findSite(url, release);
if (!site) {
throw new Error(`Could not find site for ${url} in database`);
}
if (!argv.deep && release) {
return {
...release,
site,
};
}
const scraper = scrapers.releases[site.slug] || scrapers.releases[site.network.slug];
if (!scraper) {
throw new Error(`Could not find scraper for ${url}`);
}
if ((type === 'scene' && !scraper.fetchScene) || (type === 'movie' && !scraper.fetchMovie)) {
if (release) {
logger.warn(`The '${site.name}'-scraper cannot fetch individual ${type}s`);
return null;
}
throw new Error(`The '${site.name}'-scraper cannot fetch individual ${type}s`);
}
if (!release) {
logger.info(`Scraping release from ${url}`);
}
const scrapedRelease = type === 'scene'
? await scraper.fetchScene(url, site, release, beforeFetchLatest, include)
: await scraper.fetchMovie(url, site, release, beforeFetchLatest, include);
return {
...release,
...scrapedRelease,
...(scrapedRelease && release?.tags && {
tags: release.tags.concat(scrapedRelease.tags),
}),
site,
};
}
async function accumulateMovies(releases) {
if (!argv.withMovies) return [];
const moviesByUrl = releases.reduce((acc, release) => {
if (!release.movie) return acc;
const movie = release.movie.url ? release.movie : { url: release.movie };
if (!acc[movie.url]) {
acc[movie.url] = {
...movie,
type: 'movie',
sceneIds: [],
};
}
acc[movie.url].sceneIds = acc[movie.url].sceneIds.concat(release.id);
return acc;
}, {});
const movies = await Promise.map(Object.values(moviesByUrl), async movie => scrapeRelease(movie, null, 'movie'));
const { releases: storedMovies } = await storeReleases(movies);
const movieAssociations = storedMovies.reduce((acc, movie) => acc.concat(movie.sceneIds.map(sceneId => ({
movie_id: movie.id,
scene_id: sceneId,
}))), []);
await knex('releases_movies').insert(movieAssociations);
// console.log(moviesByUrl);
return movies;
}
async function scrapeReleases(sources, type = 'scene') {
const scrapedReleases = await Promise.map(sources, async source => scrapeRelease(source, null, type), {
concurrency: 5,
}).filter(Boolean);
const curatedReleases = scrapedReleases.map(scrapedRelease => ({ ...scrapedRelease, type }));
if ((argv.scene || argv.movie) && argv.inspect) {
// only show when fetching from URL
}
if (argv.save) {
const { releases: storedReleases } = await storeReleases(curatedReleases);
await accumulateMovies(storedReleases);
if (storedReleases) {
logger.info(storedReleases.map(storedRelease => `\nhttp://${config.web.host}:${config.web.port}/scene/${storedRelease.id}/${storedRelease.slug}`).join(''));
}
return storedReleases;
}
return curatedReleases;
}
async function scrapeScenes(sources) {
return scrapeReleases(sources, 'scene');
}
async function scrapeMovies(sources) {
return scrapeReleases(sources, 'movie');
}
async function deepFetchReleases(baseReleases, beforeFetchLatest) {
const deepReleases = await Promise.map(baseReleases, async (release) => {
if (release.url || (release.path && release.site)) {
try {
const fullRelease = await scrapeRelease(release.url, release, 'scene', beforeFetchLatest);
if (fullRelease) {
return {
...release,
...fullRelease,
deep: true,
};
}
logger.warn(`Release scraper returned empty result for ${release.url}`);
return release;
} catch (error) {
logger.error(`Failed to scrape ${release.url}: ${error}`);
return {
...release,
deep: false,
};
}
}
return release;
}, {
concurrency: 2,
});
return deepReleases;
}
module.exports = {
deepFetchReleases,
scrapeMovies,
scrapeRelease,
scrapeReleases,
scrapeScenes,
};

View File

@ -1,184 +0,0 @@
'use strict';
const Promise = require('bluebird');
const moment = require('moment');
const argv = require('./argv');
const include = require('./utils/argv-include')(argv);
const logger = require('./logger')(__filename);
const knex = require('./knex');
const { fetchIncludedSites } = require('./sites');
const scrapers = require('./scrapers/scrapers');
const { deepFetchReleases } = require('./scrape-releases');
const { storeReleases } = require('./releases');
function getAfterDate() {
if (/\d{2,4}-\d{2}-\d{2,4}/.test(argv.after)) {
// using date
return moment
.utc(argv.after, ['YYYY-MM-DD', 'DD-MM-YYYY'])
.toDate();
}
// using time distance (e.g. "1 month")
return moment
.utc()
.subtract(...argv.after.split(' '))
.toDate();
}
async function findDuplicateReleaseIds(latestReleases, accReleases) {
const duplicateReleases = await knex('releases')
.whereIn('entry_id', latestReleases.map(({ entryId }) => entryId));
// include accumulated releases as duplicates to prevent an infinite
// loop when the next page contains the same releases as the previous
return new Set(duplicateReleases
.map(release => String(release.entry_id))
.concat(accReleases.map(release => String(release.entryId))));
}
async function scrapeUniqueReleases(scraper, site, beforeFetchLatest, accSiteReleases, afterDate = getAfterDate(), accReleases = [], page = argv.page) {
if (!argv.latest || !scraper.fetchLatest) {
return [];
}
const latestReleases = await scraper.fetchLatest(site, page, beforeFetchLatest, accSiteReleases, include);
if (!Array.isArray(latestReleases)) {
logger.warn(`Scraper returned ${latestReleases || 'null'} when fetching latest from '${site.name}' on '${site.network.name}'`);
return accReleases;
}
if (latestReleases.length === 0) {
return accReleases;
}
const latestReleasesWithSite = latestReleases.map(release => ({ ...release, site }));
const oldestReleaseOnPage = latestReleases.slice(-1)[0].date;
const duplicateReleaseIds = argv.redownload ? new Set() : await findDuplicateReleaseIds(latestReleases, accReleases);
const uniqueReleases = latestReleasesWithSite
.filter(release => !duplicateReleaseIds.has(String(release.entryId)) // release is already in database
&& (argv.last || !release.date || moment(release.date).isAfter(afterDate))); // release is older than specified date limit
logger.verbose(`${site.name}: Scraped page ${page}, ${uniqueReleases.length} unique recent releases`);
if (
uniqueReleases.length > 0
// && (oldestReleaseOnPage || page < argv.pages)
&& ((oldestReleaseOnPage
? moment(oldestReleaseOnPage).isAfter(afterDate)
: accReleases.length + uniqueReleases.length <= argv.nullDateLimit)
|| (argv.last && accReleases.length + uniqueReleases.length < argv.last))
) {
// oldest release on page is newer that specified date range, or latest count has not yet been met, fetch next page
return scrapeUniqueReleases(scraper, site, beforeFetchLatest, accSiteReleases, afterDate, accReleases.concat(uniqueReleases), page + 1);
}
if (argv.last && uniqueReleases.length >= argv.last) {
return accReleases.concat(uniqueReleases).slice(0, argv.last);
}
if (oldestReleaseOnPage) {
return accReleases.concat(uniqueReleases);
}
return accReleases.concat(uniqueReleases).slice(0, argv.nullDateLimit);
}
async function scrapeUpcomingReleases(scraper, site, beforeFetchLatest) {
if (argv.upcoming && scraper.fetchUpcoming) {
const upcomingReleases = await scraper.fetchUpcoming(site, 1, beforeFetchLatest, include);
return upcomingReleases
? upcomingReleases.map(release => ({ ...release, site, upcoming: true }))
: [];
}
return [];
}
async function scrapeSiteReleases(scraper, site, accSiteReleases) {
const beforeFetchLatest = await scraper.beforeFetchLatest?.(site, accSiteReleases);
const [newReleases, upcomingReleases] = await Promise.all([
scrapeUniqueReleases(scraper, site, beforeFetchLatest, accSiteReleases), // fetch basic release info from scene overview
scrapeUpcomingReleases(scraper, site, beforeFetchLatest, accSiteReleases), // fetch basic release info from upcoming overview
]);
if (argv.upcoming) {
logger.info(`${site.name}: ${argv.latest ? `Found ${newReleases.length}` : 'Ignoring'} latest releases,${argv.upcoming ? ' ' : ' ignoring '}${upcomingReleases.length || '0'} upcoming releases`);
}
const baseReleases = [...newReleases, ...upcomingReleases];
if (argv.deep) {
// follow URL for every release
return deepFetchReleases(baseReleases, beforeFetchLatest);
}
return baseReleases;
}
async function scrapeSite(site, network, accSiteReleases = []) {
if (site.parameters?.ignore) {
logger.warn(`Ignoring ${network.name}: ${site.name}`);
return [];
}
const scraper = scrapers.releases[site.slug] || scrapers.releases[site.network.slug];
if (!scraper) {
logger.warn(`No scraper found for '${site.name}' (${site.slug})`);
return [];
}
try {
const siteReleases = await scrapeSiteReleases(scraper, site, accSiteReleases);
return siteReleases.map(release => ({ ...release, site }));
} catch (error) {
logger.error(`${site.name}: Failed to scrape releases: ${error.message}`);
return [];
}
}
async function scrapeSites() {
const networks = await fetchIncludedSites();
const scrapedNetworks = await Promise.map(networks, async (network) => {
if (network.parameters?.sequential) {
logger.info(`Scraping '${network.name}' sequentially`);
return Promise.reduce(network.sites, async (acc, site) => {
const accSiteReleases = await acc;
const siteReleases = await scrapeSite(site, network, accSiteReleases);
return accSiteReleases.concat(siteReleases);
}, Promise.resolve([]));
}
return Promise.map(network.sites, async site => scrapeSite(site, network), {
concurrency: network.parameters?.concurrency || 2,
});
},
{
// 5 networks at a time
concurrency: 5,
});
const releases = scrapedNetworks.flat(2);
if (argv.inspect) {
console.log(releases);
}
if (argv.save) {
await storeReleases(releases);
}
}
module.exports = scrapeSites;

View File

@ -3,8 +3,8 @@
const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma');
module.exports = {
fetchLatest: fetchApiLatest,
fetchProfile: fetchApiProfile,
fetchUpcoming: fetchApiUpcoming,
fetchScene,
fetchLatest: fetchApiLatest,
fetchProfile: fetchApiProfile,
fetchUpcoming: fetchApiUpcoming,
fetchScene,
};

View File

@ -3,8 +3,8 @@
const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma');
module.exports = {
fetchLatest: fetchApiLatest,
fetchProfile: fetchApiProfile,
fetchUpcoming: fetchApiUpcoming,
fetchScene,
fetchLatest: fetchApiLatest,
fetchProfile: fetchApiProfile,
fetchUpcoming: fetchApiUpcoming,
fetchScene,
};

View File

@ -3,8 +3,8 @@
const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma');
module.exports = {
fetchLatest: fetchApiLatest,
fetchProfile: fetchApiProfile,
fetchUpcoming: fetchApiUpcoming,
fetchScene,
fetchLatest: fetchApiLatest,
fetchProfile: fetchApiProfile,
fetchUpcoming: fetchApiUpcoming,
fetchScene,
};

View File

@ -3,37 +3,37 @@
const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma');
function curateRelease(release, site) {
if (['bubblegumdungeon', 'ladygonzo'].includes(site.slug)) {
return {
...release,
title: release.title.split(/:|\|/)[1].trim(),
};
}
if (['bubblegumdungeon', 'ladygonzo'].includes(site.slug)) {
return {
...release,
title: release.title.split(/:|\|/)[1].trim(),
};
}
return release;
return release;
}
async function networkFetchScene(url, site, release) {
const scene = await fetchScene(url, site, release);
const scene = await fetchScene(url, site, release);
return curateRelease(scene, site);
return curateRelease(scene, site);
}
async function fetchLatest(site, page = 1) {
const releases = await fetchApiLatest(site, page, false);
const releases = await fetchApiLatest(site, page, false);
return releases.map(release => curateRelease(release, site));
return releases.map(release => curateRelease(release, site));
}
async function fetchUpcoming(site, page = 1) {
const releases = await fetchApiUpcoming(site, page, false);
const releases = await fetchApiUpcoming(site, page, false);
return releases.map(release => curateRelease(release, site));
return releases.map(release => curateRelease(release, site));
}
module.exports = {
fetchLatest,
fetchProfile: fetchApiProfile,
fetchScene: networkFetchScene,
fetchUpcoming,
fetchLatest,
fetchProfile: fetchApiProfile,
fetchScene: networkFetchScene,
fetchUpcoming,
};

View File

@ -3,47 +3,47 @@
const { fetchLatest, fetchScene } = require('./julesjordan');
function extractActors(scene) {
const release = scene;
const release = scene;
if (!scene.actors || scene.actors.length === 0) {
const introActorMatches = scene.title.match(/(?:presents|introduces|features|welcomes) (\w+ \w+)/i);
const introTwoActorMatches = scene.title.match(/(?:presents|introduces|features|welcomes) (?:(\w+)|(\w+ \w+)) and (\w+ \w+)/i);
const returnActorMatches = scene.title.match(/(?:(^\w+)|(\w+ \w+))(?:,| (?:return|visit|pov|give|suck|lick|milk|love|enjoy|service|is))/i);
const returnTwoActorMatches = scene.title.match(/(\w+ \w+) and (?:(\w+)|(\w+ \w+)) (?:return|visit|give|suck|lick|milk|love|enjoy|service|are)/i);
if (!scene.actors || scene.actors.length === 0) {
const introActorMatches = scene.title.match(/(?:presents|introduces|features|welcomes) (\w+ \w+)/i);
const introTwoActorMatches = scene.title.match(/(?:presents|introduces|features|welcomes) (?:(\w+)|(\w+ \w+)) and (\w+ \w+)/i);
const returnActorMatches = scene.title.match(/(?:(^\w+)|(\w+ \w+))(?:,| (?:return|visit|pov|give|suck|lick|milk|love|enjoy|service|is))/i);
const returnTwoActorMatches = scene.title.match(/(\w+ \w+) and (?:(\w+)|(\w+ \w+)) (?:return|visit|give|suck|lick|milk|love|enjoy|service|are)/i);
const rawActors = (introTwoActorMatches || introActorMatches || returnTwoActorMatches || returnActorMatches)?.slice(1);
const actors = rawActors?.filter((actor) => {
if (!actor) return false;
if (/swallow|\bcum|fuck|suck|give|giving|take|takes|taking|head|teen|babe|cute|beaut|naughty|teacher|nanny|adorable|brunette|blonde|bust|audition|from|\band\b|\bto\b/i.test(actor)) return false;
const rawActors = (introTwoActorMatches || introActorMatches || returnTwoActorMatches || returnActorMatches)?.slice(1);
const actors = rawActors?.filter((actor) => {
if (!actor) return false;
if (/swallow|\bcum|fuck|suck|give|giving|take|takes|taking|head|teen|babe|cute|beaut|naughty|teacher|nanny|adorable|brunette|blonde|bust|audition|from|\band\b|\bto\b/i.test(actor)) return false;
return true;
});
return true;
});
if (actors) {
release.actors = actors;
}
}
if (actors) {
release.actors = actors;
}
}
if (release.actors?.length > 1 || /threesome|threeway/.test(scene.title)) {
release.tags = scene.tags ? [...scene.tags, 'mff'] : ['mff'];
}
if (release.actors?.length > 1 || /threesome|threeway/.test(scene.title)) {
release.tags = scene.tags ? [...scene.tags, 'mff'] : ['mff'];
}
return release;
return release;
}
async function fetchLatestWrap(site, page = 1) {
const latest = await fetchLatest(site, page);
const latest = await fetchLatest(site, page);
return latest.map(scene => extractActors(scene));
return latest.map(scene => extractActors(scene));
}
async function fetchSceneWrap(url, site) {
const scene = await fetchScene(url, site);
const scene = await fetchScene(url, site);
return extractActors(scene);
return extractActors(scene);
}
module.exports = {
fetchLatest: fetchLatestWrap,
fetchScene: fetchSceneWrap,
fetchLatest: fetchLatestWrap,
fetchScene: fetchSceneWrap,
};

View File

@ -3,7 +3,7 @@
const { get, geta, ctxa } = require('../utils/q');
function extractActors(actorString) {
return actorString
return actorString
?.replace(/.*:|\(.*\)|\d+(-|\s)year(-|\s)old|nurses?|tangled/ig, '') // remove Patient:, (date) and other nonsense
.split(/\band\b|\bvs\b|\/|,|&/ig)
.map(actor => actor.trim())
@ -12,120 +12,120 @@ function extractActors(actorString) {
}
function matchActors(actorString, models) {
return models
.filter(model => new RegExp(model.name, 'i')
.test(actorString));
return models
.filter(model => new RegExp(model.name, 'i')
.test(actorString));
}
function scrapeLatest(scenes, site, models) {
return scenes.map(({ qu }) => {
const release = {};
return scenes.map(({ qu }) => {
const release = {};
const pathname = qu.url('a.itemimg').slice(1);
[release.entryId] = pathname.split('/').slice(-1);
release.url = `${site.url}${pathname}`;
const pathname = qu.url('a.itemimg').slice(1);
[release.entryId] = pathname.split('/').slice(-1);
release.url = `${site.url}${pathname}`;
release.title = qu.q('.itemimg img', 'alt') || qu.q('h4 a', true);
release.description = qu.q('.mas_longdescription', true);
release.date = qu.date('.movie_info2', 'MM/DD/YY', /\d{2}\/\d{2}\/\d{2}/);
release.title = qu.q('.itemimg img', 'alt') || qu.q('h4 a', true);
release.description = qu.q('.mas_longdescription', true);
release.date = qu.date('.movie_info2', 'MM/DD/YY', /\d{2}\/\d{2}\/\d{2}/);
const actorString = qu.q('.mas_description', true);
const actors = matchActors(actorString, models);
if (actors.length > 0) release.actors = actors;
else release.actors = extractActors(actorString);
const actorString = qu.q('.mas_description', true);
const actors = matchActors(actorString, models);
if (actors.length > 0) release.actors = actors;
else release.actors = extractActors(actorString);
const posterPath = qu.img('.itemimg img');
release.poster = `${site.url}/${posterPath}`;
const posterPath = qu.img('.itemimg img');
release.poster = `${site.url}/${posterPath}`;
return release;
});
return release;
});
}
function scrapeScene({ html, qu }, url, site, models) {
const release = { url };
const release = { url };
[release.entryId] = url.split('/').slice(-1);
release.title = qu.q('.mas_title', true);
release.description = qu.q('.mas_longdescription', true);
release.date = qu.date('.mas_description', 'MMMM DD, YYYY', /\w+ \d{1,2}, \d{4}/);
[release.entryId] = url.split('/').slice(-1);
release.title = qu.q('.mas_title', true);
release.description = qu.q('.mas_longdescription', true);
release.date = qu.date('.mas_description', 'MMMM DD, YYYY', /\w+ \d{1,2}, \d{4}/);
const actorString = qu.q('.mas_description', true).replace(/\w+ \d{1,2}, \d{4}/, '');
const actors = matchActors(actorString, models);
if (actors.length > 0) release.actors = actors;
else release.actors = extractActors(actorString);
const actorString = qu.q('.mas_description', true).replace(/\w+ \d{1,2}, \d{4}/, '');
const actors = matchActors(actorString, models);
if (actors.length > 0) release.actors = actors;
else release.actors = extractActors(actorString);
release.tags = qu.all('.tags a', true);
release.tags = qu.all('.tags a', true);
release.photos = qu.imgs('.stills img').map(photoPath => `${site.url}/${photoPath}`);
release.photos = qu.imgs('.stills img').map(photoPath => `${site.url}/${photoPath}`);
const posterIndex = 'splash:';
const poster = html.slice(html.indexOf('faceimages/', posterIndex), html.indexOf('.jpg', posterIndex) + 4);
if (poster) release.poster = `${site.url}/${poster}`;
const posterIndex = 'splash:';
const poster = html.slice(html.indexOf('faceimages/', posterIndex), html.indexOf('.jpg', posterIndex) + 4);
if (poster) release.poster = `${site.url}/${poster}`;
const trailerIndex = html.indexOf('video/mp4');
const trailer = html.slice(html.indexOf('/content', trailerIndex), html.indexOf('.mp4', trailerIndex) + 4);
if (trailer) release.trailer = { src: `${site.url}${trailer}` };
const trailerIndex = html.indexOf('video/mp4');
const trailer = html.slice(html.indexOf('/content', trailerIndex), html.indexOf('.mp4', trailerIndex) + 4);
if (trailer) release.trailer = { src: `${site.url}${trailer}` };
return release;
return release;
}
function extractModels({ el }, site) {
const models = ctxa(el, '.item');
const models = ctxa(el, '.item');
return models.map(({ qu }) => {
const actor = { gender: 'female' };
return models.map(({ qu }) => {
const actor = { gender: 'female' };
const avatar = qu.q('.itemimg img');
actor.avatar = `${site.url}/${avatar.src}`;
actor.name = avatar.alt
.split(':').slice(-1)[0]
.replace(/xtreme girl|nurse/ig, '')
.trim();
const avatar = qu.q('.itemimg img');
actor.avatar = `${site.url}/${avatar.src}`;
actor.name = avatar.alt
.split(':').slice(-1)[0]
.replace(/xtreme girl|nurse/ig, '')
.trim();
const actorPath = qu.url('.itemimg');
actor.url = `${site.url}${actorPath.slice(1)}`;
const actorPath = qu.url('.itemimg');
actor.url = `${site.url}${actorPath.slice(1)}`;
return actor;
});
return actor;
});
}
async function fetchModels(site, page = 1, accModels = []) {
const url = `${site.url}/?models/${page}`;
const res = await get(url);
const url = `${site.url}/?models/${page}`;
const res = await get(url);
if (res.ok) {
const models = extractModels(res.item, site);
const nextPage = res.item.qa('.pagenumbers', true)
.map(pageX => Number(pageX))
.filter(Boolean) // remove << and >>
.includes(page + 1);
if (res.ok) {
const models = extractModels(res.item, site);
const nextPage = res.item.qa('.pagenumbers', true)
.map(pageX => Number(pageX))
.filter(Boolean) // remove << and >>
.includes(page + 1);
if (nextPage) {
return fetchModels(site, page + 1, accModels.concat(models));
}
if (nextPage) {
return fetchModels(site, page + 1, accModels.concat(models));
}
return accModels.concat(models, { name: 'Dr. Gray' });
}
return accModels.concat(models, { name: 'Dr. Gray' });
}
return [];
return [];
}
async function fetchLatest(site, page = 1, models) {
const url = `${site.url}/show.php?a=${site.parameters.a}_${page}`;
const res = await geta(url, '.item');
const url = `${site.url}/show.php?a=${site.parameters.a}_${page}`;
const res = await geta(url, '.item');
return res.ok ? scrapeLatest(res.items, site, models) : res.status;
return res.ok ? scrapeLatest(res.items, site, models) : res.status;
}
async function fetchScene(url, site, release, beforeFetchLatest) {
const models = beforeFetchLatest || await fetchModels(site);
const res = await get(url);
const models = beforeFetchLatest || await fetchModels(site);
const res = await get(url);
return res.ok ? scrapeScene(res.item, url, site, models) : res.status;
return res.ok ? scrapeScene(res.item, url, site, models) : res.status;
}
module.exports = {
fetchLatest,
fetchScene,
beforeFetchLatest: fetchModels,
fetchLatest,
fetchScene,
beforeFetchLatest: fetchModels,
};

View File

@ -5,141 +5,141 @@ const { get, getAll, initAll, extractDate } = require('../utils/qu');
const { feetInchesToCm } = require('../utils/convert');
function getFallbacks(source) {
return [
source.replace('-1x.jpg', '-4x.jpg'),
source.replace('-1x.jpg', '-3x.jpg'),
source.replace('-1x.jpg', '-2x.jpg'),
source,
];
return [
source.replace('-1x.jpg', '-4x.jpg'),
source.replace('-1x.jpg', '-3x.jpg'),
source.replace('-1x.jpg', '-2x.jpg'),
source,
];
}
function scrapeAll(scenes, site) {
return scenes.map(({ qu }) => {
const release = {};
return scenes.map(({ qu }) => {
const release = {};
release.entryId = qu.q('.stdimage', 'id', true).match(/set-target-(\d+)/)[1];
release.url = qu.url('a');
release.entryId = qu.q('.stdimage', 'id', true).match(/set-target-(\d+)/)[1];
release.url = qu.url('a');
release.title = qu.q('h5 a', true);
release.date = qu.date('.icon-calendar + strong', 'MM/DD/YYYY');
release.title = qu.q('h5 a', true);
release.date = qu.date('.icon-calendar + strong', 'MM/DD/YYYY');
release.actors = qu.q('h3', true).replace(/featuring:\s?/i, '').split(', ');
release.actors = qu.q('h3', true).replace(/featuring:\s?/i, '').split(', ');
const photoCount = qu.q('.stdimage', 'cnt');
[release.poster, ...release.photos] = Array.from({ length: Number(photoCount) }, (value, index) => {
const source = qu.img('.stdimage', `src${index}_1x`, site.url);
const photoCount = qu.q('.stdimage', 'cnt');
[release.poster, ...release.photos] = Array.from({ length: Number(photoCount) }, (value, index) => {
const source = qu.img('.stdimage', `src${index}_1x`, site.url);
return getFallbacks(source);
});
return getFallbacks(source);
});
return release;
});
return release;
});
}
function scrapeScene({ html, qu }, url) {
const release = { url };
const release = { url };
release.entryId = qu.q('.stdimage', 'id', true).match(/set-target-(\d+)/)[1];
release.entryId = qu.q('.stdimage', 'id', true).match(/set-target-(\d+)/)[1];
release.title = qu.q('h2', true);
release.description = qu.q('p', true);
release.title = qu.q('h2', true);
release.description = qu.q('p', true);
release.date = extractDate(html, 'MM/DD/YYYY', /\b\d{2}\/\d{2}\/\d{4}\b/);
release.date = extractDate(html, 'MM/DD/YYYY', /\b\d{2}\/\d{2}\/\d{4}\b/);
release.actors = qu.all('h5:not(.video_categories) a').map(actor => ({
name: qu.q(actor, null, true),
url: qu.url(actor, null),
}));
release.actors = qu.all('h5:not(.video_categories) a').map(actor => ({
name: qu.q(actor, null, true),
url: qu.url(actor, null),
}));
release.tags = qu.all('.video_categories a', true);
release.tags = qu.all('.video_categories a', true);
release.duration = qu.dur('.video_categories + p');
release.duration = qu.dur('.video_categories + p');
const poster = qu.img('a img');
const poster = qu.img('a img');
release.poster = getFallbacks(poster);
release.photos = qu.imgs('.featured-video img', 'src0_1x').map(source => getFallbacks(source));
release.poster = getFallbacks(poster);
release.photos = qu.imgs('.featured-video img', 'src0_1x').map(source => getFallbacks(source));
return release;
return release;
}
function scrapeProfile({ el, qu }) {
const profile = {};
const profile = {};
const bio = Array.from(qu.q('.widget-content').childNodes).reduce((acc, node, index, nodes) => {
const nextNode = nodes[index + 1];
const bio = Array.from(qu.q('.widget-content').childNodes).reduce((acc, node, index, nodes) => {
const nextNode = nodes[index + 1];
if (node.tagName === 'STRONG' && nextNode?.nodeType === 3) {
acc[slugify(node.textContent, '_')] = nextNode.textContent.trim();
}
if (node.tagName === 'STRONG' && nextNode?.nodeType === 3) {
acc[slugify(node.textContent, '_')] = nextNode.textContent.trim();
}
return acc;
}, {});
return acc;
}, {});
if (bio.ethnicity) profile.ethnicity = bio.ethnicity;
if (bio.age) profile.age = Number(bio.age);
if (bio.ethnicity) profile.ethnicity = bio.ethnicity;
if (bio.age) profile.age = Number(bio.age);
if (bio.height && /\d{3}/.test(bio.height)) profile.height = Number(bio.height.match(/\d+/)[0]);
if (bio.height && /\d[;']\d/.test(bio.height)) profile.height = feetInchesToCm(bio.height);
if (bio.height && /\d{3}/.test(bio.height)) profile.height = Number(bio.height.match(/\d+/)[0]);
if (bio.height && /\d[;']\d/.test(bio.height)) profile.height = feetInchesToCm(bio.height);
if (bio.measurements) {
const [bust, waist, hip] = bio.measurements.split('-');
if (bio.measurements) {
const [bust, waist, hip] = bio.measurements.split('-');
if (bust && /\d+[a-zA-Z]+/.test(bust)) profile.bust = bust;
if (waist) profile.waist = Number(waist);
if (hip) profile.hip = Number(hip);
}
if (bust && /\d+[a-zA-Z]+/.test(bust)) profile.bust = bust;
if (waist) profile.waist = Number(waist);
if (hip) profile.hip = Number(hip);
}
if (bio.bust_size && !profile.bust) profile.bust = bio.bust_size.toUpperCase();
if (bio.bust_size && !profile.bust) profile.bust = bio.bust_size.toUpperCase();
if (bio.birth_location) profile.birthPlace = bio.birth_location;
if (bio.status_married_or_single) profile.relationship = bio.status_married_or_single;
if (bio.birth_location) profile.birthPlace = bio.birth_location;
if (bio.status_married_or_single) profile.relationship = bio.status_married_or_single;
if (bio.eye_color) profile.eyes = bio.eye_color;
if (bio.eye_color) profile.eyes = bio.eye_color;
const avatar = qu.img('.tac img');
profile.avatar = getFallbacks(avatar);
const avatar = qu.img('.tac img');
profile.avatar = getFallbacks(avatar);
profile.releases = scrapeAll(initAll(el, '.featured-video'));
profile.releases = scrapeAll(initAll(el, '.featured-video'));
return profile;
return profile;
}
async function fetchLatest(site, page) {
const url = `${site.url}/tour/categories/movies_${page}_d.html`;
const res = await getAll(url, '.featured-video');
const url = `${site.url}/tour/categories/movies_${page}_d.html`;
const res = await getAll(url, '.featured-video');
if (res.ok) {
return scrapeAll(res.items, site);
}
if (res.ok) {
return scrapeAll(res.items, site);
}
return res.status;
return res.status;
}
async function fetchScene(url, site) {
const res = await get(url, '.page-content .row');
const res = await get(url, '.page-content .row');
if (res.ok) {
return scrapeScene(res.item, url, site);
}
if (res.ok) {
return scrapeScene(res.item, url, site);
}
return res.status;
return res.status;
}
async function fetchProfile(actorName, scraperSlug, site) {
const actorSlug = slugify(actorName, '');
const url = `${site.url}/tour/models/${actorSlug}.html`;
const res = await get(url, '.page-content .row');
const actorSlug = slugify(actorName, '');
const url = `${site.url}/tour/models/${actorSlug}.html`;
const res = await get(url, '.page-content .row');
if (res.ok) {
return scrapeProfile(res.item);
}
if (res.ok) {
return scrapeProfile(res.item);
}
return res.status;
return res.status;
}
module.exports = {
fetchLatest,
fetchProfile,
fetchScene,
fetchLatest,
fetchProfile,
fetchScene,
};

View File

@ -3,11 +3,11 @@
const { fetchScene, fetchLatest, fetchProfile } = require('./mindgeek');
async function networkFetchProfile(actorName) {
return fetchProfile(actorName, 'babes');
return fetchProfile(actorName, 'babes');
}
module.exports = {
fetchLatest,
fetchProfile: networkFetchProfile,
fetchScene,
fetchLatest,
fetchProfile: networkFetchProfile,
fetchScene,
};

View File

@ -6,144 +6,144 @@ const slugify = require('../utils/slugify');
const { feetInchesToCm } = require('../utils/convert');
function scrapeAll(scenes, site) {
return scenes.map(({ qu }) => {
const release = {};
return scenes.map(({ qu }) => {
const release = {};
release.title = qu.q('h3 a', true);
release.url = qu.url('h3 a');
release.title = qu.q('h3 a', true);
release.url = qu.url('h3 a');
release.date = qu.date('.item-meta li', 'MMMM D, YYYY', /\w+ \d{1,2}, \d{4}/);
release.duration = qu.dur('.item-meta li:nth-child(2)');
release.description = qu.q('.description', true);
release.date = qu.date('.item-meta li', 'MMMM D, YYYY', /\w+ \d{1,2}, \d{4}/);
release.duration = qu.dur('.item-meta li:nth-child(2)');
release.description = qu.q('.description', true);
release.actors = qu.all('a[href*="/models"]', true);
if (/bts/i.test(release.title)) release.tags = ['behind the scenes'];
release.actors = qu.all('a[href*="/models"]', true);
if (/bts/i.test(release.title)) release.tags = ['behind the scenes'];
[release.poster, ...release.photos] = qu.all('.item-thumbs img')
.map(source => [
source.getAttribute('src0_3x'),
source.getAttribute('src0_2x'),
source.getAttribute('src0_1x'),
]
.filter(Boolean)
.map(fallback => (/^http/.test(fallback) ? fallback : `${site.url}${fallback}`)));
[release.poster, ...release.photos] = qu.all('.item-thumbs img')
.map(source => [
source.getAttribute('src0_3x'),
source.getAttribute('src0_2x'),
source.getAttribute('src0_1x'),
]
.filter(Boolean)
.map(fallback => (/^http/.test(fallback) ? fallback : `${site.url}${fallback}`)));
release.entryId = `${formatDate(release.date, 'YYYY-MM-DD')}-${slugify(release.title)}`;
release.entryId = `${formatDate(release.date, 'YYYY-MM-DD')}-${slugify(release.title)}`;
return release;
});
return release;
});
}
function scrapeScene({ html, qu }, url, site) {
const release = { url };
const release = { url };
release.title = qu.q('.item-episode h4 a', true);
release.date = qu.date('.item-meta li', 'MMMM D, YYYY', /\w+ \d{1,2}, \d{4}/);
release.duration = qu.dur('.item-meta li:nth-child(2)');
release.description = qu.q('.description', true);
release.title = qu.q('.item-episode h4 a', true);
release.date = qu.date('.item-meta li', 'MMMM D, YYYY', /\w+ \d{1,2}, \d{4}/);
release.duration = qu.dur('.item-meta li:nth-child(2)');
release.description = qu.q('.description', true);
release.actors = qu.all('.item-episode a[href*="/models"]', true);
if (/bts/i.test(release.title)) release.tags = ['behind the scenes'];
release.actors = qu.all('.item-episode a[href*="/models"]', true);
if (/bts/i.test(release.title)) release.tags = ['behind the scenes'];
const posterPath = html.match(/poster="(.*.jpg)"/)?.[1];
const trailerPath = html.match(/video src="(.*.mp4)"/)?.[1];
const posterPath = html.match(/poster="(.*.jpg)"/)?.[1];
const trailerPath = html.match(/video src="(.*.mp4)"/)?.[1];
if (posterPath) {
const poster = /^http/.test(posterPath) ? posterPath : `${site.url}${posterPath}`;
release.poster = [
poster.replace('-1x', '-3x'),
poster.replace('-1x', '-2x'),
poster,
];
}
if (posterPath) {
const poster = /^http/.test(posterPath) ? posterPath : `${site.url}${posterPath}`;
release.poster = [
poster.replace('-1x', '-3x'),
poster.replace('-1x', '-2x'),
poster,
];
}
if (trailerPath) {
const trailer = /^http/.test(trailerPath) ? trailerPath : `${site.url}${trailerPath}`;
release.trailer = { src: trailer };
}
if (trailerPath) {
const trailer = /^http/.test(trailerPath) ? trailerPath : `${site.url}${trailerPath}`;
release.trailer = { src: trailer };
}
release.entryId = `${formatDate(release.date, 'YYYY-MM-DD')}-${slugify(release.title)}`;
release.entryId = `${formatDate(release.date, 'YYYY-MM-DD')}-${slugify(release.title)}`;
return release;
return release;
}
async function fetchActorReleases(actorId, site, page = 1, accScenes = []) {
const url = `${site.url}/sets.php?id=${actorId}&page=${page}`;
const res = await get(url);
const url = `${site.url}/sets.php?id=${actorId}&page=${page}`;
const res = await get(url);
if (!res.ok) return [];
if (!res.ok) return [];
const quReleases = initAll(res.item.el, '.item-episode');
const releases = scrapeAll(quReleases, site);
const quReleases = initAll(res.item.el, '.item-episode');
const releases = scrapeAll(quReleases, site);
const nextPage = res.item.qu.q(`a[href*="page=${page + 1}"]`);
const nextPage = res.item.qu.q(`a[href*="page=${page + 1}"]`);
if (nextPage) {
return fetchActorReleases(actorId, site, page + 1, accScenes.concat(releases));
}
if (nextPage) {
return fetchActorReleases(actorId, site, page + 1, accScenes.concat(releases));
}
return accScenes.concat(releases);
return accScenes.concat(releases);
}
async function scrapeProfile({ qu }, site, withScenes) {
const profile = {};
const profile = {};
const bio = qu.all('.stats li', true).reduce((acc, row) => {
const [key, value] = row.split(':');
return { ...acc, [slugify(key, '_')]: value.trim() };
}, {});
const bio = qu.all('.stats li', true).reduce((acc, row) => {
const [key, value] = row.split(':');
return { ...acc, [slugify(key, '_')]: value.trim() };
}, {});
if (bio.height) profile.height = feetInchesToCm(bio.height);
if (bio.measurements) {
const [bust, waist, hip] = bio.measurements.split('-');
if (bio.height) profile.height = feetInchesToCm(bio.height);
if (bio.measurements) {
const [bust, waist, hip] = bio.measurements.split('-');
if (bust) profile.bust = bust;
if (waist) profile.waist = Number(waist);
if (hip) profile.hip = Number(hip);
}
if (bust) profile.bust = bust;
if (waist) profile.waist = Number(waist);
if (hip) profile.hip = Number(hip);
}
profile.avatar = [
qu.q('.profile-pic img', 'src0_3x'),
qu.q('.profile-pic img', 'src0_2x'),
qu.q('.profile-pic img', 'src0_1x'),
].filter(Boolean).map(source => (/^http/.test(source) ? source : `${site.url}${source}`));
profile.avatar = [
qu.q('.profile-pic img', 'src0_3x'),
qu.q('.profile-pic img', 'src0_2x'),
qu.q('.profile-pic img', 'src0_1x'),
].filter(Boolean).map(source => (/^http/.test(source) ? source : `${site.url}${source}`));
if (withScenes) {
const actorId = qu.q('.profile-pic img', 'id')?.match(/set-target-(\d+)/)?.[1];
if (withScenes) {
const actorId = qu.q('.profile-pic img', 'id')?.match(/set-target-(\d+)/)?.[1];
if (actorId) {
profile.releases = await fetchActorReleases(actorId, site);
}
}
if (actorId) {
profile.releases = await fetchActorReleases(actorId, site);
}
}
return profile;
return profile;
}
async function fetchLatest(site, page = 1) {
const url = `${site.url}/categories/movies/${page}/latest/`;
const res = await geta(url, '.item-episode');
const url = `${site.url}/categories/movies/${page}/latest/`;
const res = await geta(url, '.item-episode');
return res.ok ? scrapeAll(res.items, site) : res.status;
return res.ok ? scrapeAll(res.items, site) : res.status;
}
async function fetchScene(url, site) {
const res = await get(url);
const res = await get(url);
return res.ok ? scrapeScene(res.item, url, site) : res.status;
return res.ok ? scrapeScene(res.item, url, site) : res.status;
}
async function fetchProfile(actorName, scraperSlug, site, include) {
const actorSlugA = slugify(actorName, '');
const actorSlugB = slugify(actorName);
const actorSlugA = slugify(actorName, '');
const actorSlugB = slugify(actorName);
const resA = await get(`${site.url}/models/${actorSlugA}.html`);
const res = resA.ok ? resA : await get(`${site.url}/models/${actorSlugB}.html`);
const resA = await get(`${site.url}/models/${actorSlugA}.html`);
const res = resA.ok ? resA : await get(`${site.url}/models/${actorSlugB}.html`);
return res.ok ? scrapeProfile(res.item, site, include.scenes) : res.status;
return res.ok ? scrapeProfile(res.item, site, include.scenes) : res.status;
}
module.exports = {
fetchLatest,
fetchScene,
fetchProfile,
fetchLatest,
fetchScene,
fetchProfile,
};

View File

@ -8,99 +8,99 @@ const clusterId = '617fb597b659459bafe6472470d9073a';
const authKey = 'YmFuZy1yZWFkOktqVDN0RzJacmQ1TFNRazI=';
const genderMap = {
M: 'male',
F: 'female',
M: 'male',
F: 'female',
};
function getScreenUrl(item, scene) {
return `https://i.bang.com/screenshots/${scene.dvd.id}/movie/${scene.order}/${item.screenId}.jpg`;
return `https://i.bang.com/screenshots/${scene.dvd.id}/movie/${scene.order}/${item.screenId}.jpg`;
}
function encodeId(id) {
return Buffer
.from(id, 'hex')
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, ',');
return Buffer
.from(id, 'hex')
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, ',');
}
function decodeId(id) {
const restoredId = id
.replace(/-/g, '+')
.replace(/_/g, '/')
.replace(/,/g, '=');
const restoredId = id
.replace(/-/g, '+')
.replace(/_/g, '/')
.replace(/,/g, '=');
return Buffer
.from(restoredId, 'base64')
.toString('hex');
return Buffer
.from(restoredId, 'base64')
.toString('hex');
}
function scrapeScene(scene, site) {
const release = {
site,
entryId: scene.id,
title: scene.name,
description: scene.description,
tags: scene.genres.concat(scene.actions).map(genre => genre.name),
duration: scene.duration,
};
const release = {
site,
entryId: scene.id,
title: scene.name,
description: scene.description,
tags: scene.genres.concat(scene.actions).map(genre => genre.name),
duration: scene.duration,
};
const slug = slugify(release.title);
release.url = `https://www.bang.com/video/${encodeId(release.entryId)}/${slug}`;
const slug = slugify(release.title);
release.url = `https://www.bang.com/video/${encodeId(release.entryId)}/${slug}`;
const date = new Date(scene.releaseDate);
release.date = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
const date = new Date(scene.releaseDate);
release.date = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
release.actors = scene.actors.map(actor => ({ name: actor.name, gender: genderMap[actor.gender] }));
release.actors = scene.actors.map(actor => ({ name: actor.name, gender: genderMap[actor.gender] }));
if (scene.is4k) release.tags.push('4k');
if (scene.gay) release.tags.push('gay');
if (scene.is4k) release.tags.push('4k');
if (scene.gay) release.tags.push('gay');
const defaultPoster = scene.screenshots.find(photo => photo.default === true);
const photoset = scene.screenshots.filter(photo => photo.default === false);
const defaultPoster = scene.screenshots.find(photo => photo.default === true);
const photoset = scene.screenshots.filter(photo => photo.default === false);
const photos = defaultPoster ? photoset : photoset.slice(1);
const poster = defaultPoster || photoset[0];
const photos = defaultPoster ? photoset : photoset.slice(1);
const poster = defaultPoster || photoset[0];
release.poster = getScreenUrl(poster, scene);
release.photos = photos.map(photo => getScreenUrl(photo, scene));
release.poster = getScreenUrl(poster, scene);
release.photos = photos.map(photo => getScreenUrl(photo, scene));
release.trailer = {
src: `https://i.bang.com/v/${scene.dvd.id}/${scene.identifier}/preview.mp4`,
};
release.trailer = {
src: `https://i.bang.com/v/${scene.dvd.id}/${scene.identifier}/preview.mp4`,
};
release.channel = scene.series.name
.replace(/[! .]/g, '')
.replace('&', 'and');
release.channel = scene.series.name
.replace(/[! .]/g, '')
.replace('&', 'and');
return release;
return release;
}
function scrapeLatest(scenes, site) {
return scenes.map(({ _source: scene }) => scrapeScene(scene, site));
return scenes.map(({ _source: scene }) => scrapeScene(scene, site));
}
async function fetchLatest(site, page = 1) {
const res = await bhttp.post(`https://${clusterId}.us-east-1.aws.found.io/videos/video/_search`, {
size: 50,
from: (page - 1) * 50,
query: {
bool: {
must: [
{
match: {
status: 'ok',
},
},
{
range: {
releaseDate: {
lte: 'now',
},
},
},
/*
const res = await bhttp.post(`https://${clusterId}.us-east-1.aws.found.io/videos/video/_search`, {
size: 50,
from: (page - 1) * 50,
query: {
bool: {
must: [
{
match: {
status: 'ok',
},
},
{
range: {
releaseDate: {
lte: 'now',
},
},
},
/*
* global fetch
{
nested: {
@ -122,66 +122,66 @@ async function fetchLatest(site, page = 1) {
},
},
*/
{
nested: {
path: 'series',
query: {
bool: {
must: [
{
match: {
'series.id': {
operator: 'AND',
query: site.parameters.siteId,
},
},
},
],
},
},
},
},
],
must_not: [
{
match: {
type: 'trailer',
},
},
],
},
},
sort: [
{
releaseDate: {
order: 'desc',
},
},
],
}, {
encodeJSON: true,
headers: {
Authorization: `Basic ${authKey}`,
},
});
{
nested: {
path: 'series',
query: {
bool: {
must: [
{
match: {
'series.id': {
operator: 'AND',
query: site.parameters.siteId,
},
},
},
],
},
},
},
},
],
must_not: [
{
match: {
type: 'trailer',
},
},
],
},
},
sort: [
{
releaseDate: {
order: 'desc',
},
},
],
}, {
encodeJSON: true,
headers: {
Authorization: `Basic ${authKey}`,
},
});
return scrapeLatest(res.body.hits.hits, site);
return scrapeLatest(res.body.hits.hits, site);
}
async function fetchScene(url, site) {
const encodedId = new URL(url).pathname.split('/')[2];
const entryId = decodeId(encodedId);
const encodedId = new URL(url).pathname.split('/')[2];
const entryId = decodeId(encodedId);
const res = await bhttp.get(`https://${clusterId}.us-east-1.aws.found.io/videos/video/${entryId}`, {
headers: {
Authorization: `Basic ${authKey}`,
},
});
const res = await bhttp.get(`https://${clusterId}.us-east-1.aws.found.io/videos/video/${entryId}`, {
headers: {
Authorization: `Basic ${authKey}`,
},
});
return scrapeScene(res.body._source, site); // eslint-disable-line no-underscore-dangle
return scrapeScene(res.body._source, site); // eslint-disable-line no-underscore-dangle
}
module.exports = {
fetchLatest,
fetchScene,
fetchLatest,
fetchScene,
};

View File

@ -10,44 +10,44 @@ const slugify = require('../utils/slugify');
const { ex } = require('../utils/q');
function scrape(html, site) {
const $ = cheerio.load(html, { normalizeWhitespace: true });
const sceneElements = $('.echThumb').toArray();
const $ = cheerio.load(html, { normalizeWhitespace: true });
const sceneElements = $('.echThumb').toArray();
return sceneElements.map((element) => {
const sceneLinkElement = $(element).find('.thmb_lnk');
const title = sceneLinkElement.attr('title');
const url = `https://bangbros.com${sceneLinkElement.attr('href')}`;
const shootId = sceneLinkElement.attr('id') && sceneLinkElement.attr('id').split('-')[1];
const entryId = url.split('/')[3].slice(5);
return sceneElements.map((element) => {
const sceneLinkElement = $(element).find('.thmb_lnk');
const title = sceneLinkElement.attr('title');
const url = `https://bangbros.com${sceneLinkElement.attr('href')}`;
const shootId = sceneLinkElement.attr('id') && sceneLinkElement.attr('id').split('-')[1];
const entryId = url.split('/')[3].slice(5);
const date = moment.utc($(element).find('.thmb_mr_2 span.faTxt').text(), 'MMM D, YYYY').toDate();
const actors = $(element).find('.cast-wrapper a.cast').map((actorIndex, actorElement) => $(actorElement).text().trim()).toArray();
const date = moment.utc($(element).find('.thmb_mr_2 span.faTxt').text(), 'MMM D, YYYY').toDate();
const actors = $(element).find('.cast-wrapper a.cast').map((actorIndex, actorElement) => $(actorElement).text().trim()).toArray();
const photoElement = $(element).find('.rollover-image');
const poster = `https:${photoElement.attr('data-original')}`;
const photoElement = $(element).find('.rollover-image');
const poster = `https:${photoElement.attr('data-original')}`;
const photosUrl = photoElement.attr('data-rollover-url');
const photosMaxIndex = photoElement.attr('data-rollover-max-index');
const photos = Array.from({ length: photosMaxIndex }, (val, index) => `https:${photosUrl}big${index + 1}.jpg`);
const photosUrl = photoElement.attr('data-rollover-url');
const photosMaxIndex = photoElement.attr('data-rollover-max-index');
const photos = Array.from({ length: photosMaxIndex }, (val, index) => `https:${photosUrl}big${index + 1}.jpg`);
const duration = moment.duration(`0:${$(element).find('.thmb_pic b.tTm').text()}`).asSeconds();
const channel = $(element).find('a[href*="/websites"]').attr('href').split('/').slice(-1)[0];
const duration = moment.duration(`0:${$(element).find('.thmb_pic b.tTm').text()}`).asSeconds();
const channel = $(element).find('a[href*="/websites"]').attr('href').split('/').slice(-1)[0];
return {
url,
entryId,
shootId,
title,
actors,
date,
duration,
poster,
photos,
rating: null,
site,
channel,
};
});
return {
url,
entryId,
shootId,
title,
actors,
date,
duration,
poster,
photos,
rating: null,
site,
channel,
};
});
}
/* no dates available, breaks database
@ -80,63 +80,63 @@ function scrapeUpcoming(html, site) {
*/
function scrapeScene(html, url, _site) {
const { qu } = ex(html, '.playerSection');
const release = {};
const { qu } = ex(html, '.playerSection');
const release = {};
[release.shootId] = qu.q('.vdoTags + .vdoCast', true).match(/\w+$/);
[release.entryId] = url.split('/')[3].match(/\d+$/);
release.title = qu.q('.ps-vdoHdd h1', true);
release.description = qu.q('.vdoDesc', true);
[release.shootId] = qu.q('.vdoTags + .vdoCast', true).match(/\w+$/);
[release.entryId] = url.split('/')[3].match(/\d+$/);
release.title = qu.q('.ps-vdoHdd h1', true);
release.description = qu.q('.vdoDesc', true);
release.actors = qu.all('a[href*="/model"]', true);
release.tags = qu.all('.vdoTags a', true);
release.actors = qu.all('a[href*="/model"]', true);
release.tags = qu.all('.vdoTags a', true);
release.stars = Number(qu.q('div[class*="like"]', true).match(/^\d+/)[0]) / 20;
release.stars = Number(qu.q('div[class*="like"]', true).match(/^\d+/)[0]) / 20;
const poster = qu.img('img#player-overlay-image');
release.poster = [
poster,
poster.replace('/big_trailer', '/members/450x340'), // load error fallback
];
const poster = qu.img('img#player-overlay-image');
release.poster = [
poster,
poster.replace('/big_trailer', '/members/450x340'), // load error fallback
];
release.trailer = { src: qu.trailer() };
release.trailer = { src: qu.trailer() };
// all scenes seem to have 12 album photos available, not always included on the page
const firstPhotoUrl = ex(html).qu.img('img[data-slider-index="1"]');
release.photos = Array.from({ length: 12 }, (val, index) => firstPhotoUrl.replace(/big\d+/, `big${index + 1}`));
// all scenes seem to have 12 album photos available, not always included on the page
const firstPhotoUrl = ex(html).qu.img('img[data-slider-index="1"]');
release.photos = Array.from({ length: 12 }, (val, index) => firstPhotoUrl.replace(/big\d+/, `big${index + 1}`));
const [channel] = qu.url('a[href*="/websites"]').match(/\w+$/);
const [channel] = qu.url('a[href*="/websites"]').match(/\w+$/);
if (channel === 'bangcasting') release.channel = 'bangbroscasting';
if (channel === 'remaster') release.channel = 'bangbrosremastered';
else release.channel = channel;
if (channel === 'bangcasting') release.channel = 'bangbroscasting';
if (channel === 'remaster') release.channel = 'bangbrosremastered';
else release.channel = channel;
return release;
return release;
}
function scrapeProfile(html) {
const { q } = ex(html);
const profile = {};
const { q } = ex(html);
const profile = {};
const avatar = q('.profilePic img', 'src');
if (avatar) profile.avatar = `https:${avatar}`;
const avatar = q('.profilePic img', 'src');
if (avatar) profile.avatar = `https:${avatar}`;
profile.releases = scrape(html);
profile.releases = scrape(html);
return profile;
return profile;
}
function scrapeProfileSearch(html, actorName) {
const { qu } = ex(html);
const actorLink = qu.url(`a[title="${actorName}" i][href*="model"]`);
const { qu } = ex(html);
const actorLink = qu.url(`a[title="${actorName}" i][href*="model"]`);
return actorLink ? `https://bangbros.com${actorLink}` : null;
return actorLink ? `https://bangbros.com${actorLink}` : null;
}
async function fetchLatest(site, page = 1) {
const res = await bhttp.get(`${site.url}/${page}`);
const res = await bhttp.get(`${site.url}/${page}`);
return scrape(res.body.toString(), site);
return scrape(res.body.toString(), site);
}
/*
@ -148,43 +148,43 @@ async function fetchUpcoming(site) {
*/
async function fetchScene(url, site, release) {
if (!release?.date) {
logger.warn(`Scraping Bang Bros scene from URL without release date: ${url}`);
}
if (!release?.date) {
logger.warn(`Scraping Bang Bros scene from URL without release date: ${url}`);
}
const { origin } = new URL(url);
const res = await bhttp.get(url);
const { origin } = new URL(url);
const res = await bhttp.get(url);
if (!/https?:\/\/(www.)?bangbros.com\/?$/.test(origin)) {
throw new Error('Cannot fetch from this URL. Please find the scene on https://bangbros.com and try again.');
}
if (!/https?:\/\/(www.)?bangbros.com\/?$/.test(origin)) {
throw new Error('Cannot fetch from this URL. Please find the scene on https://bangbros.com and try again.');
}
return scrapeScene(res.body.toString(), url, site);
return scrapeScene(res.body.toString(), url, site);
}
async function fetchProfile(actorName) {
const actorSlug = slugify(actorName);
const url = `https://bangbros.com/search/${actorSlug}`;
const res = await bhttp.get(url);
const actorSlug = slugify(actorName);
const url = `https://bangbros.com/search/${actorSlug}`;
const res = await bhttp.get(url);
if (res.statusCode === 200) {
const actorUrl = scrapeProfileSearch(res.body.toString(), actorName);
if (res.statusCode === 200) {
const actorUrl = scrapeProfileSearch(res.body.toString(), actorName);
if (actorUrl) {
const actorRes = await bhttp.get(actorUrl);
if (actorUrl) {
const actorRes = await bhttp.get(actorUrl);
if (actorRes.statusCode === 200) {
return scrapeProfile(actorRes.body.toString());
}
}
}
if (actorRes.statusCode === 200) {
return scrapeProfile(actorRes.body.toString());
}
}
}
return null;
return null;
}
module.exports = {
fetchLatest,
fetchScene,
fetchProfile,
// fetchUpcoming, no dates available
fetchLatest,
fetchScene,
fetchProfile,
// fetchUpcoming, no dates available
};

View File

@ -5,33 +5,33 @@
const { fetchScene, fetchLatest, fetchUpcoming, fetchProfile } = require('./gamma');
async function fetchSceneWrapper(url, site, baseRelease) {
const release = await fetchScene(url, site, baseRelease);
const release = await fetchScene(url, site, baseRelease);
if (site.isFallback && release.channel) {
const channelUrl = url.replace('blowpass.com', `${release.channel}.com`);
if (site.isNetwork && release.channel) {
const channelUrl = url.replace('blowpass.com', `${release.channel}.com`);
if (['onlyteenblowjobs', 'mommyblowsbest'].includes(release.channel)) {
release.url = channelUrl.replace(/video\/\w+\//, 'scene/');
return release;
}
if (['onlyteenblowjobs', 'mommyblowsbest'].includes(release.channel)) {
release.url = channelUrl.replace(/video\/\w+\//, 'scene/');
return release;
}
release.url = channelUrl.replace(/video\/\w+\//, 'video/');
}
release.url = channelUrl.replace(/video\/\w+\//, 'video/');
}
return release;
return release;
}
function getActorReleasesUrl(actorPath, page = 1) {
return `https://www.blowpass.com/en/videos/blowpass/latest/All-Categories/0${actorPath}/${page}`;
return `https://www.blowpass.com/en/videos/blowpass/latest/All-Categories/0${actorPath}/${page}`;
}
async function networkFetchProfile(actorName, scraperSlug, site, include) {
return fetchProfile(actorName, scraperSlug, null, getActorReleasesUrl, include);
return fetchProfile(actorName, scraperSlug, null, getActorReleasesUrl, include);
}
module.exports = {
fetchLatest,
fetchProfile: networkFetchProfile,
fetchUpcoming,
fetchScene: fetchSceneWrapper,
fetchLatest,
fetchProfile: networkFetchProfile,
fetchUpcoming,
fetchScene: fetchSceneWrapper,
};

View File

@ -5,90 +5,90 @@ const bhttp = require('bhttp');
const { ex } = require('../utils/q');
function scrapeProfile(html) {
const { qu } = ex(html); /* eslint-disable-line object-curly-newline */
const profile = {};
const { qu } = ex(html); /* eslint-disable-line object-curly-newline */
const profile = {};
const bio = qu.all('.infobox tr[valign="top"]')
.map(detail => qu.all(detail, 'td', true))
.reduce((acc, [key, value]) => ({ ...acc, [key.slice(0, -1).replace(/[\s+|/]/g, '_')]: value }), {});
const bio = qu.all('.infobox tr[valign="top"]')
.map(detail => qu.all(detail, 'td', true))
.reduce((acc, [key, value]) => ({ ...acc, [key.slice(0, -1).replace(/[\s+|/]/g, '_')]: value }), {});
/* unreliable, see: Syren De Mer
/* unreliable, see: Syren De Mer
const catlinks = qa('#mw-normal-catlinks a', true);
const isTrans = catlinks.some(link => link.match(/shemale|transgender/i));
profile.gender = isTrans ? 'transsexual' : 'female';
*/
profile.birthdate = qu.date('.bday', 'YYYY-MM-DD');
profile.birthdate = qu.date('.bday', 'YYYY-MM-DD');
profile.description = qu.q('#mw-content-text > p', true);
profile.description = qu.q('#mw-content-text > p', true);
if (bio.Born) profile.birthPlace = bio.Born.slice(bio.Born.lastIndexOf(')') + 1);
if (bio.Ethnicity) profile.ethnicity = bio.Ethnicity;
if (bio.Born) profile.birthPlace = bio.Born.slice(bio.Born.lastIndexOf(')') + 1);
if (bio.Ethnicity) profile.ethnicity = bio.Ethnicity;
if (bio.Measurements) {
const measurements = bio.Measurements
.match(/\d+(\w+)?-\d+-\d+/g)
if (bio.Measurements) {
const measurements = bio.Measurements
.match(/\d+(\w+)?-\d+-\d+/g)
?.slice(-1)[0] // allow for both '34C-25-36' and '86-64-94 cm / 34-25-37 in'
.split('-');
// account for measuemrents being just e.g. '32EE'
if (measurements) {
const [bust, waist, hip] = measurements;
// account for measuemrents being just e.g. '32EE'
if (measurements) {
const [bust, waist, hip] = measurements;
if (/[a-zA-Z]/.test(bust)) profile.bust = bust; // only use bust if cup size is included
if (/[a-zA-Z]/.test(bust)) profile.bust = bust; // only use bust if cup size is included
profile.waist = Number(waist);
profile.hip = Number(hip);
}
profile.waist = Number(waist);
profile.hip = Number(hip);
}
if (/^\d+\w+$/.test(bio.Measurements)) profile.bust = bio.Measurements;
}
if (/^\d+\w+$/.test(bio.Measurements)) profile.bust = bio.Measurements;
}
if (bio.Bra_cup_size) {
const bust = bio.Bra_cup_size.match(/^\d+\w+/);
if (bust) [profile.bust] = bust;
}
if (bio.Bra_cup_size) {
const bust = bio.Bra_cup_size.match(/^\d+\w+/);
if (bust) [profile.bust] = bust;
}
if (bio.Boobs === 'Enhanced') profile.naturalBoobs = false;
if (bio.Boobs === 'Natural') profile.naturalBoobs = true;
if (bio.Boobs === 'Enhanced') profile.naturalBoobs = false;
if (bio.Boobs === 'Natural') profile.naturalBoobs = true;
if (bio.Height) profile.height = Number(bio.Height.match(/\d+\.\d+/g).slice(-1)[0]) * 100;
if (bio.Weight) profile.weight = Number(bio.Weight.match(/\d+/g)[1]);
if (bio.Height) profile.height = Number(bio.Height.match(/\d+\.\d+/g).slice(-1)[0]) * 100;
if (bio.Weight) profile.weight = Number(bio.Weight.match(/\d+/g)[1]);
if (bio.Eye_color) profile.eyes = bio.Eye_color;
if (bio.Hair) [profile.hair] = bio.Hair.split(',');
if (bio.Eye_color) profile.eyes = bio.Eye_color;
if (bio.Hair) [profile.hair] = bio.Hair.split(',');
if (bio.Blood_group) profile.blood = bio.Blood_group;
if (bio.Also_known_as) profile.aliases = bio.Also_known_as.split(', ');
if (bio.Blood_group) profile.blood = bio.Blood_group;
if (bio.Also_known_as) profile.aliases = bio.Also_known_as.split(', ');
const avatarThumbPath = qu.img('.image img');
const avatarThumbPath = qu.img('.image img');
if (avatarThumbPath && !/NoImageAvailable/.test(avatarThumbPath)) {
const avatarPath = avatarThumbPath.slice(0, avatarThumbPath.lastIndexOf('/')).replace('thumb/', '');
if (avatarThumbPath && !/NoImageAvailable/.test(avatarThumbPath)) {
const avatarPath = avatarThumbPath.slice(0, avatarThumbPath.lastIndexOf('/')).replace('thumb/', '');
profile.avatar = {
src: `http://www.boobpedia.com${avatarPath}`,
copyright: null,
};
}
profile.avatar = {
src: `http://www.boobpedia.com${avatarPath}`,
copyright: null,
};
}
profile.social = qu.urls('.infobox a.external');
profile.social = qu.urls('.infobox a.external');
return profile;
return profile;
}
async function fetchProfile(actorName) {
const actorSlug = actorName.replace(/\s+/, '_');
const res = await bhttp.get(`http://www.boobpedia.com/boobs/${actorSlug}`);
const actorSlug = actorName.replace(/\s+/, '_');
const res = await bhttp.get(`http://www.boobpedia.com/boobs/${actorSlug}`);
if (res.statusCode === 200) {
return scrapeProfile(res.body.toString());
}
if (res.statusCode === 200) {
return scrapeProfile(res.body.toString());
}
return null;
return null;
}
module.exports = {
fetchProfile,
fetchProfile,
};

View File

@ -11,216 +11,216 @@ const slugify = require('../utils/slugify');
const { heightToCm, lbsToKg } = require('../utils/convert');
const hairMap = {
Blonde: 'blonde',
Brunette: 'brown',
'Black Hair': 'black',
Redhead: 'red',
Blonde: 'blonde',
Brunette: 'brown',
'Black Hair': 'black',
Redhead: 'red',
};
function scrapeAll(html, site, upcoming) {
const $ = cheerio.load(html, { normalizeWhitespace: true });
const sceneElements = $('.release-card.scene').toArray();
const $ = cheerio.load(html, { normalizeWhitespace: true });
const sceneElements = $('.release-card.scene').toArray();
return sceneElements.reduce((acc, element) => {
const isUpcoming = $(element).find('.icon-upcoming.active').length === 1;
return sceneElements.reduce((acc, element) => {
const isUpcoming = $(element).find('.icon-upcoming.active').length === 1;
if ((upcoming && !isUpcoming) || (!upcoming && isUpcoming)) {
return acc;
}
if ((upcoming && !isUpcoming) || (!upcoming && isUpcoming)) {
return acc;
}
const sceneLinkElement = $(element).find('a');
const sceneLinkElement = $(element).find('a');
const url = `https://www.brazzers.com${sceneLinkElement.attr('href')}`;
const title = sceneLinkElement.attr('title');
const entryId = url.split('/').slice(-3, -2)[0];
const url = `https://www.brazzers.com${sceneLinkElement.attr('href')}`;
const title = sceneLinkElement.attr('title');
const entryId = url.split('/').slice(-3, -2)[0];
const date = moment.utc($(element).find('time').text(), 'MMMM DD, YYYY').toDate();
const actors = $(element).find('.model-names a').map((actorIndex, actorElement) => $(actorElement).attr('title')).toArray();
const date = moment.utc($(element).find('time').text(), 'MMMM DD, YYYY').toDate();
const actors = $(element).find('.model-names a').map((actorIndex, actorElement) => $(actorElement).attr('title')).toArray();
const likes = Number($(element).find('.label-rating .like-amount').text());
const dislikes = Number($(element).find('.label-rating .dislike-amount').text());
const likes = Number($(element).find('.label-rating .like-amount').text());
const dislikes = Number($(element).find('.label-rating .dislike-amount').text());
const poster = `https:${$(element).find('.card-main-img').attr('data-src')}`;
const photos = $(element).find('.card-overlay .image-under').map((photoIndex, photoElement) => `https:${$(photoElement).attr('data-src')}`).toArray();
const poster = `https:${$(element).find('.card-main-img').attr('data-src')}`;
const photos = $(element).find('.card-overlay .image-under').map((photoIndex, photoElement) => `https:${$(photoElement).attr('data-src')}`).toArray();
const channel = slugify($(element).find('.collection').attr('title'), '');
const channel = slugify($(element).find('.collection').attr('title'), '');
return acc.concat({
url,
entryId,
title,
actors,
date,
poster,
photos,
rating: {
likes,
dislikes,
},
channel,
site,
});
}, []);
return acc.concat({
url,
entryId,
title,
actors,
date,
poster,
photos,
rating: {
likes,
dislikes,
},
channel,
site,
});
}, []);
}
async function scrapeScene(html, url, _site) {
const $ = cheerio.load(html, { normalizeWhitespace: true });
const release = {};
const $ = cheerio.load(html, { normalizeWhitespace: true });
const release = {};
const videoJson = $('script:contains("window.videoUiOptions")').html();
const videoString = videoJson.slice(videoJson.indexOf('{"stream_info":'), videoJson.lastIndexOf('},') + 1);
const videoData = JSON.parse(videoString);
const videoJson = $('script:contains("window.videoUiOptions")').html();
const videoString = videoJson.slice(videoJson.indexOf('{"stream_info":'), videoJson.lastIndexOf('},') + 1);
const videoData = JSON.parse(videoString);
[release.entryId] = url.split('/').slice(-3, -2);
release.title = $('.scene-title[itemprop="name"]').text();
[release.entryId] = url.split('/').slice(-3, -2);
release.title = $('.scene-title[itemprop="name"]').text();
release.description = $('#scene-description p[itemprop="description"]')
.contents()
.first()
.text()
.trim();
release.description = $('#scene-description p[itemprop="description"]')
.contents()
.first()
.text()
.trim();
release.date = moment.utc($('.more-scene-info .scene-date').text(), 'MMMM DD, YYYY').toDate();
release.duration = Number($('.scene-length[itemprop="duration"]').attr('content').slice(1, -1)) * 60;
release.date = moment.utc($('.more-scene-info .scene-date').text(), 'MMMM DD, YYYY').toDate();
release.duration = Number($('.scene-length[itemprop="duration"]').attr('content').slice(1, -1)) * 60;
const actorsFromCards = $('.featured-model .card-image a').map((actorIndex, actorElement) => {
const avatar = `https:${$(actorElement).find('img').attr('data-src')}`;
const actorsFromCards = $('.featured-model .card-image a').map((actorIndex, actorElement) => {
const avatar = `https:${$(actorElement).find('img').attr('data-src')}`;
return {
name: $(actorElement).attr('title'),
avatar: [avatar.replace('medium.jpg', 'large.jpg'), avatar],
};
}).toArray();
return {
name: $(actorElement).attr('title'),
avatar: [avatar.replace('medium.jpg', 'large.jpg'), avatar],
};
}).toArray();
release.actors = actorsFromCards || $('.related-model a').map((actorIndex, actorElement) => $(actorElement).text()).toArray();
release.actors = actorsFromCards || $('.related-model a').map((actorIndex, actorElement) => $(actorElement).text()).toArray();
release.likes = Number($('.label-rating .like').text());
release.dislikes = Number($('.label-rating .dislike').text());
release.likes = Number($('.label-rating .like').text());
release.dislikes = Number($('.label-rating .dislike').text());
const siteElement = $('.niche-site-logo');
// const siteUrl = `https://www.brazzers.com${siteElement.attr('href').slice(0, -1)}`;
const siteName = siteElement.attr('title');
release.channel = siteName.replace(/\s+/g, '').toLowerCase();
const siteElement = $('.niche-site-logo');
// const siteUrl = `https://www.brazzers.com${siteElement.attr('href').slice(0, -1)}`;
const siteName = siteElement.attr('title');
release.channel = siteName.replace(/\s+/g, '').toLowerCase();
release.tags = $('.tag-card-container a').map((tagIndex, tagElement) => $(tagElement).text()).toArray();
release.photos = $('.carousel-thumb a').map((photoIndex, photoElement) => `https:${$(photoElement).attr('href')}`).toArray();
release.tags = $('.tag-card-container a').map((tagIndex, tagElement) => $(tagElement).text()).toArray();
release.photos = $('.carousel-thumb a').map((photoIndex, photoElement) => `https:${$(photoElement).attr('href')}`).toArray();
const posterPath = videoData?.poster || $('meta[itemprop="thumbnailUrl"]').attr('content') || $('#trailer-player-container').attr('data-player-img');
if (posterPath) release.poster = `https:${posterPath}`;
const posterPath = videoData?.poster || $('meta[itemprop="thumbnailUrl"]').attr('content') || $('#trailer-player-container').attr('data-player-img');
if (posterPath) release.poster = `https:${posterPath}`;
if (videoData) {
release.trailer = Object.entries(videoData.stream_info.http.paths).map(([quality, path]) => ({
src: `https:${path}`,
quality: Number(quality.match(/\d{3,}/)[0]),
}));
}
if (videoData) {
release.trailer = Object.entries(videoData.stream_info.http.paths).map(([quality, path]) => ({
src: `https:${path}`,
quality: Number(quality.match(/\d{3,}/)[0]),
}));
}
return release;
return release;
}
function scrapeActorSearch(html, url, actorName) {
const { document } = new JSDOM(html).window;
const actorLink = document.querySelector(`a[title="${actorName}" i]`);
const { document } = new JSDOM(html).window;
const actorLink = document.querySelector(`a[title="${actorName}" i]`);
return actorLink ? actorLink.href : null;
return actorLink ? actorLink.href : null;
}
async function fetchActorReleases({ qu, html }, accReleases = []) {
const releases = scrapeAll(html);
const next = qu.url('.pagination .next a');
const releases = scrapeAll(html);
const next = qu.url('.pagination .next a');
if (next) {
const url = `https://www.brazzers.com${next}`;
const res = await get(url);
if (next) {
const url = `https://www.brazzers.com${next}`;
const res = await get(url);
if (res.ok) {
return fetchActorReleases(res.item, accReleases.concat(releases));
}
}
if (res.ok) {
return fetchActorReleases(res.item, accReleases.concat(releases));
}
}
return accReleases.concat(releases);
return accReleases.concat(releases);
}
async function scrapeProfile(html, url, actorName) {
const qProfile = ex(html);
const { q, qa } = qProfile;
const qProfile = ex(html);
const { q, qa } = qProfile;
const bioKeys = qa('.profile-spec-list label', true).map(key => key.replace(/\n+|\s{2,}/g, '').trim());
const bioValues = qa('.profile-spec-list var', true).map(value => value.replace(/\n+|\s{2,}/g, '').trim());
const bioKeys = qa('.profile-spec-list label', true).map(key => key.replace(/\n+|\s{2,}/g, '').trim());
const bioValues = qa('.profile-spec-list var', true).map(value => value.replace(/\n+|\s{2,}/g, '').trim());
const bio = bioKeys.reduce((acc, key, index) => ({ ...acc, [key]: bioValues[index] }), {});
const bio = bioKeys.reduce((acc, key, index) => ({ ...acc, [key]: bioValues[index] }), {});
const profile = {
name: actorName,
};
const profile = {
name: actorName,
};
profile.description = q('.model-profile-specs p', true);
profile.description = q('.model-profile-specs p', true);
if (bio.Ethnicity) profile.ethnicity = bio.Ethnicity;
if (bio.Measurements && bio.Measurements.match(/\d+[A-Z]+-\d+-\d+/)) [profile.bust, profile.waist, profile.hip] = bio.Measurements.split('-');
if (bio['Date of Birth'] && bio['Date of Birth'] !== 'Unknown') profile.birthdate = moment.utc(bio['Date of Birth'], 'MMMM DD, YYYY').toDate();
if (bio['Birth Location']) profile.birthPlace = bio['Birth Location'];
if (bio['Pussy Type']) profile.pussy = bio['Pussy Type'].split(',').slice(-1)[0].toLowerCase();
if (bio.Ethnicity) profile.ethnicity = bio.Ethnicity;
if (bio.Measurements && bio.Measurements.match(/\d+[A-Z]+-\d+-\d+/)) [profile.bust, profile.waist, profile.hip] = bio.Measurements.split('-');
if (bio['Date of Birth'] && bio['Date of Birth'] !== 'Unknown') profile.birthdate = moment.utc(bio['Date of Birth'], 'MMMM DD, YYYY').toDate();
if (bio['Birth Location']) profile.birthPlace = bio['Birth Location'];
if (bio['Pussy Type']) profile.pussy = bio['Pussy Type'].split(',').slice(-1)[0].toLowerCase();
if (bio.Height) profile.height = heightToCm(bio.Height);
if (bio.Weight) profile.weight = lbsToKg(bio.Weight.match(/\d+/)[0]);
if (bio['Hair Color']) profile.hair = hairMap[bio['Hair Color']] || bio['Hair Color'].toLowerCase();
if (bio.Height) profile.height = heightToCm(bio.Height);
if (bio.Weight) profile.weight = lbsToKg(bio.Weight.match(/\d+/)[0]);
if (bio['Hair Color']) profile.hair = hairMap[bio['Hair Color']] || bio['Hair Color'].toLowerCase();
if (bio['Tits Type'] && bio['Tits Type'].match('Natural')) profile.naturalBoobs = true;
if (bio['Tits Type'] && bio['Tits Type'].match('Enhanced')) profile.naturalBoobs = false;
if (bio['Tits Type'] && bio['Tits Type'].match('Natural')) profile.naturalBoobs = true;
if (bio['Tits Type'] && bio['Tits Type'].match('Enhanced')) profile.naturalBoobs = false;
if (bio['Body Art'] && bio['Body Art'].match('Tattoo')) profile.hasTattoos = true;
if (bio['Body Art'] && bio['Body Art'].match('Piercing')) profile.hasPiercings = true;
if (bio['Body Art'] && bio['Body Art'].match('Tattoo')) profile.hasTattoos = true;
if (bio['Body Art'] && bio['Body Art'].match('Piercing')) profile.hasPiercings = true;
const avatarEl = q('.big-pic-model-container img');
if (avatarEl) profile.avatar = `https:${avatarEl.src}`;
const avatarEl = q('.big-pic-model-container img');
if (avatarEl) profile.avatar = `https:${avatarEl.src}`;
profile.releases = await fetchActorReleases(qProfile);
profile.releases = await fetchActorReleases(qProfile);
return profile;
return profile;
}
async function fetchLatest(site, page = 1) {
const res = await bhttp.get(`${site.url}/page/${page}/`);
const res = await bhttp.get(`${site.url}/page/${page}/`);
return scrapeAll(res.body.toString(), site, false);
return scrapeAll(res.body.toString(), site, false);
}
async function fetchUpcoming(site) {
const res = await bhttp.get(`${site.url}/`);
const res = await bhttp.get(`${site.url}/`);
return scrapeAll(res.body.toString(), site, true);
return scrapeAll(res.body.toString(), site, true);
}
async function fetchScene(url, site) {
const res = await bhttp.get(url);
const res = await bhttp.get(url);
return scrapeScene(res.body.toString(), url, site);
return scrapeScene(res.body.toString(), url, site);
}
async function fetchProfile(actorName) {
const searchUrl = 'https://brazzers.com/pornstars-search/';
const searchRes = await bhttp.get(searchUrl, {
headers: {
Cookie: `textSearch=${encodeURIComponent(actorName)};`,
},
});
const searchUrl = 'https://brazzers.com/pornstars-search/';
const searchRes = await bhttp.get(searchUrl, {
headers: {
Cookie: `textSearch=${encodeURIComponent(actorName)};`,
},
});
const actorLink = scrapeActorSearch(searchRes.body.toString(), searchUrl, actorName);
const actorLink = scrapeActorSearch(searchRes.body.toString(), searchUrl, actorName);
if (actorLink) {
const url = `https://brazzers.com${actorLink}`;
const res = await bhttp.get(url);
if (actorLink) {
const url = `https://brazzers.com${actorLink}`;
const res = await bhttp.get(url);
return scrapeProfile(res.body.toString(), url, actorName);
}
return scrapeProfile(res.body.toString(), url, actorName);
}
return null;
return null;
}
module.exports = {
fetchLatest,
fetchProfile,
fetchScene,
fetchUpcoming,
fetchLatest,
fetchProfile,
fetchScene,
fetchUpcoming,
};

View File

@ -3,8 +3,8 @@
const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma');
module.exports = {
fetchLatest: fetchApiLatest,
fetchProfile: fetchApiProfile,
fetchScene,
fetchUpcoming: fetchApiUpcoming,
fetchLatest: fetchApiLatest,
fetchProfile: fetchApiProfile,
fetchScene,
fetchUpcoming: fetchApiUpcoming,
};

View File

@ -4,139 +4,139 @@ const { get, geta, ctxa, ed } = require('../utils/q');
const slugify = require('../utils/slugify');
function scrapeAll(scenes, site) {
return scenes.map(({ qu }) => {
const url = qu.url('.text-thumb a');
const { pathname } = new URL(url);
const channelUrl = qu.url('.badge');
return scenes.map(({ qu }) => {
const url = qu.url('.text-thumb a');
const { pathname } = new URL(url);
const channelUrl = qu.url('.badge');
if (site?.parameters?.extract && qu.q('.badge', true) !== site.name) {
return null;
}
if (site?.parameters?.extract && qu.q('.badge', true) !== site.name) {
return null;
}
const release = {};
const release = {};
release.url = channelUrl ? `${channelUrl}${pathname}` : url;
release.entryId = pathname.match(/\/\d+/)[0].slice(1);
release.title = qu.q('.text-thumb a', true);
release.url = channelUrl ? `${channelUrl}${pathname}` : url;
release.entryId = pathname.match(/\/\d+/)[0].slice(1);
release.title = qu.q('.text-thumb a', true);
release.date = qu.date('.date', 'YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/);
release.duration = qu.dur('.date', /(\d{2}:)?\d{2}:\d{2}/);
release.date = qu.date('.date', 'YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/);
release.duration = qu.dur('.date', /(\d{2}:)?\d{2}:\d{2}/);
release.actors = qu.all('.category a', true);
release.actors = qu.all('.category a', true);
release.poster = qu.img('img.video_placeholder, .video-images img');
release.teaser = { src: qu.trailer() };
release.poster = qu.img('img.video_placeholder, .video-images img');
release.teaser = { src: qu.trailer() };
return release;
}).filter(Boolean);
return release;
}).filter(Boolean);
}
function scrapeScene({ q, qd, qa }, url, _site, baseRelease) {
const release = { url };
const release = { url };
const { pathname } = new URL(url);
release.entryId = pathname.match(/\/\d+/)[0].slice(1);
const { pathname } = new URL(url);
release.entryId = pathname.match(/\/\d+/)[0].slice(1);
release.title = q('.trailer-block_title', true);
release.description = q('.info-block:nth-child(3) .text', true);
release.date = qd('.info-block_data .text', 'MMMM D, YYYY', /\w+ \d{1,2}, \d{4}/);
release.title = q('.trailer-block_title', true);
release.description = q('.info-block:nth-child(3) .text', true);
release.date = qd('.info-block_data .text', 'MMMM D, YYYY', /\w+ \d{1,2}, \d{4}/);
const duration = baseRelease?.duration || Number(q('.info-block_data .text', true).match(/(\d+)\s+min/)?.[1]) * 60;
if (duration) release.duration = duration;
const duration = baseRelease?.duration || Number(q('.info-block_data .text', true).match(/(\d+)\s+min/)?.[1]) * 60;
if (duration) release.duration = duration;
release.actors = qa('.info-block_data a[href*="/models"]', true);
release.tags = qa('.info-block a[href*="/categories"]', true);
release.actors = qa('.info-block_data a[href*="/models"]', true);
release.tags = qa('.info-block a[href*="/categories"]', true);
const posterEl = q('.update_thumb');
const poster = posterEl.getAttribute('src0_3x') || posterEl.getAttribute('src0_2x') || posterEl.dataset.src;
const posterEl = q('.update_thumb');
const poster = posterEl.getAttribute('src0_3x') || posterEl.getAttribute('src0_2x') || posterEl.dataset.src;
if (poster && baseRelease?.poster) release.photos = [poster];
else if (poster) release.poster = poster;
if (poster && baseRelease?.poster) release.photos = [poster];
else if (poster) release.poster = poster;
return release;
return release;
}
function scrapeProfile({ q, qa, qtx }) {
const profile = {};
const profile = {};
const keys = qa('.model-descr_line:not(.model-descr_rait) p.text span', true);
const values = qa('.model-descr_line:not(.model-descr_rait) p.text').map(el => qtx(el));
const bio = keys.reduce((acc, key, index) => ({ ...acc, [slugify(key, '_')]: values[index] }), {});
const keys = qa('.model-descr_line:not(.model-descr_rait) p.text span', true);
const values = qa('.model-descr_line:not(.model-descr_rait) p.text').map(el => qtx(el));
const bio = keys.reduce((acc, key, index) => ({ ...acc, [slugify(key, '_')]: values[index] }), {});
if (bio.height) profile.height = Number(bio.height.match(/\((\d+)cm\)/)[1]);
if (bio.weight) profile.weight = Number(bio.weight.match(/\((\d+)kg\)/)[1]);
if (bio.race) profile.ethnicity = bio.race;
if (bio.height) profile.height = Number(bio.height.match(/\((\d+)cm\)/)[1]);
if (bio.weight) profile.weight = Number(bio.weight.match(/\((\d+)kg\)/)[1]);
if (bio.race) profile.ethnicity = bio.race;
if (bio.date_of_birth) profile.birthdate = ed(bio.date_of_birth, 'MMMM D, YYYY');
if (bio.birthplace) profile.birthPlace = bio.birthplace;
if (bio.date_of_birth) profile.birthdate = ed(bio.date_of_birth, 'MMMM D, YYYY');
if (bio.birthplace) profile.birthPlace = bio.birthplace;
if (bio.measurements) {
const [bust, waist, hip] = bio.measurements.split('-');
if (!/\?/.test(bust)) profile.bust = bust;
if (!/\?/.test(waist)) profile.waist = waist;
if (!/\?/.test(hip)) profile.hip = hip;
}
if (bio.measurements) {
const [bust, waist, hip] = bio.measurements.split('-');
if (!/\?/.test(bust)) profile.bust = bust;
if (!/\?/.test(waist)) profile.waist = waist;
if (!/\?/.test(hip)) profile.hip = hip;
}
if (bio.hair) profile.hair = bio.hair;
if (bio.eyes) profile.eyes = bio.eyes;
if (bio.hair) profile.hair = bio.hair;
if (bio.eyes) profile.eyes = bio.eyes;
if (/various/i.test(bio.tattoos)) profile.hasTattoos = true;
else if (/none/i.test(bio.tattoos)) profile.hasTattoos = false;
else if (bio.tattoos) {
profile.hasTattoos = true;
profile.tattoos = bio.tattoos;
}
if (/various/i.test(bio.tattoos)) profile.hasTattoos = true;
else if (/none/i.test(bio.tattoos)) profile.hasTattoos = false;
else if (bio.tattoos) {
profile.hasTattoos = true;
profile.tattoos = bio.tattoos;
}
if (/various/i.test(bio.piercings)) profile.hasPiercings = true;
else if (/none/i.test(bio.piercings)) profile.hasPiercings = false;
else if (bio.piercings) {
profile.hasPiercings = true;
profile.piercings = bio.piercings;
}
if (/various/i.test(bio.piercings)) profile.hasPiercings = true;
else if (/none/i.test(bio.piercings)) profile.hasPiercings = false;
else if (bio.piercings) {
profile.hasPiercings = true;
profile.piercings = bio.piercings;
}
if (bio.aliases) profile.aliases = bio.aliases.split(',').map(alias => alias.trim());
if (bio.aliases) profile.aliases = bio.aliases.split(',').map(alias => alias.trim());
const avatar = q('.model-img img');
profile.avatar = avatar.getAttribute('src0_3x') || avatar.getAttribute('src0_2x') || avatar.dataset.src;
const avatar = q('.model-img img');
profile.avatar = avatar.getAttribute('src0_3x') || avatar.getAttribute('src0_2x') || avatar.dataset.src;
const releases = qa('.video-thumb');
profile.releases = scrapeAll(ctxa(releases));
const releases = qa('.video-thumb');
profile.releases = scrapeAll(ctxa(releases));
return profile;
return profile;
}
async function fetchLatest(site, page = 1) {
const url = site.parameters?.extract
? `https://cherrypimps.com/categories/movies_${page}.html`
: `${site.url}/categories/movies_${page}.html`;
const res = await geta(url, 'div.video-thumb');
const url = site.parameters?.extract
? `https://cherrypimps.com/categories/movies_${page}.html`
: `${site.url}/categories/movies_${page}.html`;
const res = await geta(url, 'div.video-thumb');
return res.ok ? scrapeAll(res.items, site) : res.status;
return res.ok ? scrapeAll(res.items, site) : res.status;
}
async function fetchScene(url, site, release) {
const res = await get(url);
const res = await get(url);
return res.ok ? scrapeScene(res.item, url, site, release) : res.status;
return res.ok ? scrapeScene(res.item, url, site, release) : res.status;
}
async function fetchProfile(actorName, scraperSlug) {
const actorSlug = slugify(actorName);
const actorSlug2 = slugify(actorName, '');
const actorSlug = slugify(actorName);
const actorSlug2 = slugify(actorName, '');
const [url, url2] = ['cherrypimps', 'wildoncam'].includes(scraperSlug)
? [`https://${scraperSlug}.com/models/${actorSlug}.html`, `https://${scraperSlug}.com/models/${actorSlug2}.html`]
: [`https://${scraperSlug.replace('xxx', '')}.xxx/models/${actorSlug}.html`, `https://${scraperSlug.replace('xxx', '')}.xxx/models/${actorSlug2}.html`];
const [url, url2] = ['cherrypimps', 'wildoncam'].includes(scraperSlug)
? [`https://${scraperSlug}.com/models/${actorSlug}.html`, `https://${scraperSlug}.com/models/${actorSlug2}.html`]
: [`https://${scraperSlug.replace('xxx', '')}.xxx/models/${actorSlug}.html`, `https://${scraperSlug.replace('xxx', '')}.xxx/models/${actorSlug2}.html`];
const res = await get(url);
if (res.ok) return scrapeProfile(res.item);
const res = await get(url);
if (res.ok) return scrapeProfile(res.item);
const res2 = await get(url2);
return res2.ok ? scrapeProfile(res2.item) : res2.status;
const res2 = await get(url2);
return res2.ok ? scrapeProfile(res2.item) : res2.status;
}
module.exports = {
fetchLatest,
fetchScene,
fetchProfile,
fetchLatest,
fetchScene,
fetchProfile,
};

View File

@ -7,182 +7,182 @@ const slugify = require('../utils/slugify');
/* eslint-disable newline-per-chained-call */
function scrapeAll(html, site, origin) {
return exa(html, '.card.m-1:not(.pornstar-card)').map(({ q, qa, qd }) => {
const release = {};
return exa(html, '.card.m-1:not(.pornstar-card)').map(({ q, qa, qd }) => {
const release = {};
release.title = q('a', 'title');
release.url = `${site?.url || origin || 'https://ddfnetwork.com'}${q('a', 'href')}`;
[release.entryId] = release.url.split('/').slice(-1);
release.title = q('a', 'title');
release.url = `${site?.url || origin || 'https://ddfnetwork.com'}${q('a', 'href')}`;
[release.entryId] = release.url.split('/').slice(-1);
release.date = qd('small[datetime]', 'YYYY-MM-DD HH:mm:ss', null, 'datetime');
release.actors = qa('.card-subtitle a', true).filter(Boolean);
release.date = qd('small[datetime]', 'YYYY-MM-DD HH:mm:ss', null, 'datetime');
release.actors = qa('.card-subtitle a', true).filter(Boolean);
const duration = parseInt(q('.card-info div:nth-child(2) .card-text', true), 10) * 60;
if (duration) release.duration = duration;
const duration = parseInt(q('.card-info div:nth-child(2) .card-text', true), 10) * 60;
if (duration) release.duration = duration;
release.poster = q('img').dataset.src;
release.poster = q('img').dataset.src;
return release;
});
return release;
});
}
async function scrapeScene(html, url, _site) {
const { qu } = ex(html);
const release = {};
const { qu } = ex(html);
const release = {};
[release.entryId] = url.split('/').slice(-1);
[release.entryId] = url.split('/').slice(-1);
release.title = qu.meta('itemprop=name');
release.description = qu.q('.descr-box p', true);
release.date = qu.date('meta[itemprop=uploadDate]', 'YYYY-MM-DD', null, 'content')
release.title = qu.meta('itemprop=name');
release.description = qu.q('.descr-box p', true);
release.date = qu.date('meta[itemprop=uploadDate]', 'YYYY-MM-DD', null, 'content')
|| qu.date('.title-border:nth-child(2) p', 'MM.DD.YYYY');
release.actors = qu.all('.pornstar-card > a', 'title');
release.tags = qu.all('.tags-tab .tags a', true);
release.actors = qu.all('.pornstar-card > a', 'title');
release.tags = qu.all('.tags-tab .tags a', true);
release.duration = parseInt(qu.q('.icon-video-red + span', true), 10) * 60;
release.likes = Number(qu.q('.icon-like-red + span', true));
release.duration = parseInt(qu.q('.icon-video-red + span', true), 10) * 60;
release.likes = Number(qu.q('.icon-like-red + span', true));
release.poster = qu.poster();
release.photos = qu.urls('.photo-slider-guest .card a');
release.poster = qu.poster();
release.photos = qu.urls('.photo-slider-guest .card a');
release.trailer = qu.all('source[type="video/mp4"]').map(trailer => ({
src: trailer.src,
quality: Number(trailer.attributes.res.value),
}));
release.trailer = qu.all('source[type="video/mp4"]').map(trailer => ({
src: trailer.src,
quality: Number(trailer.attributes.res.value),
}));
return release;
return release;
}
async function fetchActorReleases(urls) {
// DDF Network and DDF Network Stream list all scenes, exclude
const sources = urls.filter(url => !/ddfnetwork/.test(url));
// DDF Network and DDF Network Stream list all scenes, exclude
const sources = urls.filter(url => !/ddfnetwork/.test(url));
const releases = await Promise.all(sources.map(async (url) => {
const { html } = await get(url);
const releases = await Promise.all(sources.map(async (url) => {
const { html } = await get(url);
return scrapeAll(html, null, new URL(url).origin);
}));
return scrapeAll(html, null, new URL(url).origin);
}));
// DDF cross-releases scenes between sites, filter duplicates by entryId
return Object.values(releases
.flat()
.sort((releaseA, releaseB) => releaseB.date - releaseA.date) // sort by date so earliest scene remains
.reduce((acc, release) => ({ ...acc, [release.entryId]: release }), {}));
// DDF cross-releases scenes between sites, filter duplicates by entryId
return Object.values(releases
.flat()
.sort((releaseA, releaseB) => releaseB.date - releaseA.date) // sort by date so earliest scene remains
.reduce((acc, release) => ({ ...acc, [release.entryId]: release }), {}));
}
async function scrapeProfile(html, _url, actorName) {
const { qu } = ex(html);
const { qu } = ex(html);
const keys = qu.all('.about-title', true).map(key => slugify(key, '_'));
const values = qu.all('.about-info').map((el) => {
if (el.children.length > 0) {
return Array.from(el.children, child => child.textContent.trim()).join(', ');
}
const keys = qu.all('.about-title', true).map(key => slugify(key, '_'));
const values = qu.all('.about-info').map((el) => {
if (el.children.length > 0) {
return Array.from(el.children, child => child.textContent.trim()).join(', ');
}
return el.textContent.trim();
});
return el.textContent.trim();
});
const bio = keys.reduce((acc, key, index) => {
if (values[index] === '-') return acc;
const bio = keys.reduce((acc, key, index) => {
if (values[index] === '-') return acc;
return {
...acc,
[key]: values[index],
};
}, {});
return {
...acc,
[key]: values[index],
};
}, {});
const profile = {
name: actorName,
};
const profile = {
name: actorName,
};
profile.description = qu.q('.description-box', true);
profile.birthdate = ed(bio.birthday, 'MMMM DD, YYYY');
profile.description = qu.q('.description-box', true);
profile.birthdate = ed(bio.birthday, 'MMMM DD, YYYY');
if (bio.nationality) profile.nationality = bio.nationality;
if (bio.nationality) profile.nationality = bio.nationality;
if (bio.bra_size) [profile.bust] = bio.bra_size.match(/\d+\w+/);
if (bio.waist) profile.waist = Number(bio.waist.match(/\d+/)[0]);
if (bio.hips) profile.hip = Number(bio.hips.match(/\d+/)[0]);
if (bio.bra_size) [profile.bust] = bio.bra_size.match(/\d+\w+/);
if (bio.waist) profile.waist = Number(bio.waist.match(/\d+/)[0]);
if (bio.hips) profile.hip = Number(bio.hips.match(/\d+/)[0]);
if (bio.height) profile.height = Number(bio.height.match(/\d{2,}/)[0]);
if (bio.height) profile.height = Number(bio.height.match(/\d{2,}/)[0]);
if (bio.tit_style && /Enhanced/.test(bio.tit_style)) profile.naturalBoobs = false;
if (bio.tit_style && /Natural/.test(bio.tit_style)) profile.naturalBoobs = true;
if (bio.tit_style && /Enhanced/.test(bio.tit_style)) profile.naturalBoobs = false;
if (bio.tit_style && /Natural/.test(bio.tit_style)) profile.naturalBoobs = true;
if (bio.body_art && /Tattoo/.test(bio.body_art)) profile.hasTattoos = true;
if (bio.body_art && /Piercing/.test(bio.body_art)) profile.hasPiercings = true;
if (bio.body_art && /Tattoo/.test(bio.body_art)) profile.hasTattoos = true;
if (bio.body_art && /Piercing/.test(bio.body_art)) profile.hasPiercings = true;
if (bio.hair_style) profile.hair = bio.hair_style.split(',')[0].trim().toLowerCase();
if (bio.eye_color) profile.eyes = bio.eye_color.match(/\w+/)[0].toLowerCase();
if (bio.hair_style) profile.hair = bio.hair_style.split(',')[0].trim().toLowerCase();
if (bio.eye_color) profile.eyes = bio.eye_color.match(/\w+/)[0].toLowerCase();
if (bio.shoe_size) profile.shoes = Number(bio.shoe_size.split('|')[1]);
if (bio.shoe_size) profile.shoes = Number(bio.shoe_size.split('|')[1]);
const avatarEl = qu.q('.pornstar-details .card-img-top');
if (avatarEl && avatarEl.dataset.src.match('^//')) profile.avatar = `https:${avatarEl.dataset.src}`;
const avatarEl = qu.q('.pornstar-details .card-img-top');
if (avatarEl && avatarEl.dataset.src.match('^//')) profile.avatar = `https:${avatarEl.dataset.src}`;
profile.releases = await fetchActorReleases(qu.urls('.find-me-tab li a'));
profile.releases = await fetchActorReleases(qu.urls('.find-me-tab li a'));
return profile;
return profile;
}
async function fetchLatest(site, page = 1) {
const url = site.parameters?.native
? `${site.url}/videos/search/latest/ever/allsite/-/${page}`
: `https://ddfnetwork.com/videos/search/latest/ever/${new URL(site.url).hostname}/-/${page}`;
const url = site.parameters?.native
? `${site.url}/videos/search/latest/ever/allsite/-/${page}`
: `https://ddfnetwork.com/videos/search/latest/ever/${new URL(site.url).hostname}/-/${page}`;
const res = await bhttp.get(url);
const res = await bhttp.get(url);
if (res.statusCode === 200) {
return scrapeAll(res.body.toString(), site);
}
if (res.statusCode === 200) {
return scrapeAll(res.body.toString(), site);
}
return res.statusCode;
return res.statusCode;
}
async function fetchScene(url, site) {
// DDF's main site moved to Porn World
// const res = await bhttp.get(`https://ddfnetwork.com${new URL(url).pathname}`);
const res = await bhttp.get(url);
// DDF's main site moved to Porn World
// const res = await bhttp.get(`https://ddfnetwork.com${new URL(url).pathname}`);
const res = await bhttp.get(url);
return scrapeScene(res.body.toString(), url, site);
return scrapeScene(res.body.toString(), url, site);
}
async function fetchProfile(actorName) {
const resSearch = await bhttp.post('https://ddfnetwork.com/search/ajax',
{
type: 'hints',
word: actorName,
},
{
decodeJSON: true,
headers: {
'x-requested-with': 'XMLHttpRequest',
},
});
const resSearch = await bhttp.post('https://ddfnetwork.com/search/ajax',
{
type: 'hints',
word: actorName,
},
{
decodeJSON: true,
headers: {
'x-requested-with': 'XMLHttpRequest',
},
});
if (resSearch.statusCode !== 200 || Array.isArray(resSearch.body.list)) {
return null;
}
if (resSearch.statusCode !== 200 || Array.isArray(resSearch.body.list)) {
return null;
}
if (!resSearch.body.list.pornstarsName || resSearch.body.list.pornstarsName.length === 0) {
return null;
}
if (!resSearch.body.list.pornstarsName || resSearch.body.list.pornstarsName.length === 0) {
return null;
}
const [actor] = resSearch.body.list.pornstarsName;
const url = `https://ddfnetwork.com${actor.href}`;
const [actor] = resSearch.body.list.pornstarsName;
const url = `https://ddfnetwork.com${actor.href}`;
const resActor = await bhttp.get(url);
const resActor = await bhttp.get(url);
if (resActor.statusCode !== 200) {
return null;
}
if (resActor.statusCode !== 200) {
return null;
}
return scrapeProfile(resActor.body.toString(), url, actorName);
return scrapeProfile(resActor.body.toString(), url, actorName);
}
module.exports = {
fetchLatest,
fetchProfile,
fetchScene,
fetchLatest,
fetchProfile,
fetchScene,
};

View File

@ -3,11 +3,11 @@
const { fetchScene, fetchLatest, fetchProfile } = require('./mindgeek');
async function networkFetchProfile(actorName) {
return fetchProfile(actorName, 'digitalplayground', 'modelprofile');
return fetchProfile(actorName, 'digitalplayground', 'modelprofile');
}
module.exports = {
fetchLatest,
fetchProfile: networkFetchProfile,
fetchScene,
fetchLatest,
fetchProfile: networkFetchProfile,
fetchScene,
};

View File

@ -7,136 +7,136 @@ const { JSDOM } = require('jsdom');
const moment = require('moment');
async function getPhotos(albumUrl) {
const res = await bhttp.get(albumUrl);
const html = res.body.toString();
const { document } = new JSDOM(html).window;
const res = await bhttp.get(albumUrl);
const html = res.body.toString();
const { document } = new JSDOM(html).window;
const lastPhotoPage = Array.from(document.querySelectorAll('.preview-image-container a')).slice(-1)[0].href;
const lastPhotoIndex = parseInt(lastPhotoPage.match(/\d+.jpg/)[0], 10);
const lastPhotoPage = Array.from(document.querySelectorAll('.preview-image-container a')).slice(-1)[0].href;
const lastPhotoIndex = parseInt(lastPhotoPage.match(/\d+.jpg/)[0], 10);
const photoUrls = Array.from({ length: lastPhotoIndex }, (value, index) => {
const pageUrl = `https://blacksonblondes.com${lastPhotoPage.replace(/\d+.jpg/, `${(index + 1).toString().padStart(3, '0')}.jpg`)}`;
const photoUrls = Array.from({ length: lastPhotoIndex }, (value, index) => {
const pageUrl = `https://blacksonblondes.com${lastPhotoPage.replace(/\d+.jpg/, `${(index + 1).toString().padStart(3, '0')}.jpg`)}`;
return {
url: pageUrl,
extract: ({ qu }) => qu.q('.scenes-module img', 'src'),
};
});
return {
url: pageUrl,
extract: ({ qu }) => qu.q('.scenes-module img', 'src'),
};
});
return photoUrls;
return photoUrls;
}
function scrapeLatest(html, site) {
const { document } = new JSDOM(html).window;
const sceneElements = Array.from(document.querySelectorAll('.recent-updates'));
const { document } = new JSDOM(html).window;
const sceneElements = Array.from(document.querySelectorAll('.recent-updates'));
return sceneElements.reduce((acc, element) => {
const siteUrl = element.querySelector('.help-block').textContent;
return sceneElements.reduce((acc, element) => {
const siteUrl = element.querySelector('.help-block').textContent;
if (`www.${siteUrl.toLowerCase()}` !== new URL(site.url).host) {
// different dogfart site
return acc;
}
if (`www.${siteUrl.toLowerCase()}` !== new URL(site.url).host) {
// different dogfart site
return acc;
}
const sceneLinkElement = element.querySelector('.thumbnail');
const url = `https://dogfartnetwork.com${sceneLinkElement.href}`;
const { pathname } = new URL(url);
const entryId = `${site.slug}_${pathname.split('/')[4]}`;
const sceneLinkElement = element.querySelector('.thumbnail');
const url = `https://dogfartnetwork.com${sceneLinkElement.href}`;
const { pathname } = new URL(url);
const entryId = `${site.slug}_${pathname.split('/')[4]}`;
const title = element.querySelector('.scene-title').textContent;
const actors = title.split(/[,&]|\band\b/).map(actor => actor.trim());
const title = element.querySelector('.scene-title').textContent;
const actors = title.split(/[,&]|\band\b/).map(actor => actor.trim());
const poster = `https:${element.querySelector('img').src}`;
const teaser = sceneLinkElement.dataset.preview_clip_url;
const poster = `https:${element.querySelector('img').src}`;
const teaser = sceneLinkElement.dataset.preview_clip_url;
return [
...acc,
{
url,
entryId,
title,
actors,
poster,
teaser: {
src: teaser,
},
site,
},
];
}, []);
return [
...acc,
{
url,
entryId,
title,
actors,
poster,
teaser: {
src: teaser,
},
site,
},
];
}, []);
}
async function scrapeScene(html, url, site) {
const { document } = new JSDOM(html).window;
const { document } = new JSDOM(html).window;
const title = document.querySelector('.description-title').textContent;
const actors = Array.from(document.querySelectorAll('.more-scenes a')).map(({ textContent }) => textContent);
const metaDescription = document.querySelector('meta[itemprop="description"]').content;
const description = metaDescription
? metaDescription.content
: document.querySelector('.description')
.textContent
.replace(/[ \t\n]{2,}/g, ' ')
.replace('...read more', '')
.trim();
const title = document.querySelector('.description-title').textContent;
const actors = Array.from(document.querySelectorAll('.more-scenes a')).map(({ textContent }) => textContent);
const metaDescription = document.querySelector('meta[itemprop="description"]').content;
const description = metaDescription
? metaDescription.content
: document.querySelector('.description')
.textContent
.replace(/[ \t\n]{2,}/g, ' ')
.replace('...read more', '')
.trim();
const channel = document.querySelector('.site-name').textContent.split('.')[0].toLowerCase();
const { origin, pathname } = new URL(url);
const entryId = `${channel}_${pathname.split('/').slice(-2)[0]}`;
const channel = document.querySelector('.site-name').textContent.split('.')[0].toLowerCase();
const { origin, pathname } = new URL(url);
const entryId = `${channel}_${pathname.split('/').slice(-2)[0]}`;
const date = new Date(document.querySelector('meta[itemprop="uploadDate"]').content);
const duration = moment
.duration(`00:${document
.querySelectorAll('.extra-info p')[1]
.textContent
.match(/\d+:\d+$/)[0]}`)
.asSeconds();
const date = new Date(document.querySelector('meta[itemprop="uploadDate"]').content);
const duration = moment
.duration(`00:${document
.querySelectorAll('.extra-info p')[1]
.textContent
.match(/\d+:\d+$/)[0]}`)
.asSeconds();
const trailerElement = document.querySelector('.html5-video');
const poster = `https:${trailerElement.dataset.poster}`;
const { trailer } = trailerElement.dataset;
const trailerElement = document.querySelector('.html5-video');
const poster = `https:${trailerElement.dataset.poster}`;
const { trailer } = trailerElement.dataset;
const lastPhotosUrl = Array.from(document.querySelectorAll('.pagination a')).slice(-1)[0].href;
const photos = await getPhotos(`${origin}${pathname}${lastPhotosUrl}`, site, url);
const lastPhotosUrl = Array.from(document.querySelectorAll('.pagination a')).slice(-1)[0].href;
const photos = await getPhotos(`${origin}${pathname}${lastPhotosUrl}`, site, url);
const stars = Math.floor(Number(document.querySelector('span[itemprop="average"]')?.textContent || document.querySelector('span[itemprop="ratingValue"]')?.textContent) / 2);
const tags = Array.from(document.querySelectorAll('.scene-details .categories a')).map(({ textContent }) => textContent);
const stars = Math.floor(Number(document.querySelector('span[itemprop="average"]')?.textContent || document.querySelector('span[itemprop="ratingValue"]')?.textContent) / 2);
const tags = Array.from(document.querySelectorAll('.scene-details .categories a')).map(({ textContent }) => textContent);
return {
entryId,
url: `${origin}${pathname}`,
title,
description,
actors,
date,
duration,
poster,
photos,
trailer: {
src: trailer,
},
tags,
rating: {
stars,
},
site,
channel,
};
return {
entryId,
url: `${origin}${pathname}`,
title,
description,
actors,
date,
duration,
poster,
photos,
trailer: {
src: trailer,
},
tags,
rating: {
stars,
},
site,
channel,
};
}
async function fetchLatest(site, page = 1) {
const res = await bhttp.get(`https://dogfartnetwork.com/tour/scenes/?p=${page}`);
const res = await bhttp.get(`https://dogfartnetwork.com/tour/scenes/?p=${page}`);
return scrapeLatest(res.body.toString(), site);
return scrapeLatest(res.body.toString(), site);
}
async function fetchScene(url, site) {
const res = await bhttp.get(url);
const res = await bhttp.get(url);
return scrapeScene(res.body.toString(), url, site);
return scrapeScene(res.body.toString(), url, site);
}
module.exports = {
fetchLatest,
fetchScene,
fetchLatest,
fetchScene,
};

View File

@ -3,8 +3,8 @@
const { fetchApiLatest, fetchApiUpcoming, fetchScene, fetchApiProfile } = require('./gamma');
module.exports = {
fetchLatest: fetchApiLatest,
fetchProfile: fetchApiProfile,
fetchScene,
fetchUpcoming: fetchApiUpcoming,
fetchLatest: fetchApiLatest,
fetchProfile: fetchApiProfile,
fetchScene,
fetchUpcoming: fetchApiUpcoming,
};

View File

@ -3,11 +3,11 @@
const { fetchScene, fetchLatest, fetchProfile } = require('./mindgeek');
async function networkFetchProfile(actorName) {
return fetchProfile(actorName, 'fakehub', 'modelprofile');
return fetchProfile(actorName, 'fakehub', 'modelprofile');
}
module.exports = {
fetchLatest,
fetchProfile: networkFetchProfile,
fetchScene,
fetchLatest,
fetchProfile: networkFetchProfile,
fetchScene,
};

View File

@ -1,115 +1,115 @@
'use strict';
const {
fetchLatest,
fetchApiLatest,
fetchUpcoming,
fetchApiUpcoming,
fetchScene,
fetchProfile,
fetchApiProfile,
scrapeAll,
fetchLatest,
fetchApiLatest,
fetchUpcoming,
fetchApiUpcoming,
fetchScene,
fetchProfile,
fetchApiProfile,
scrapeAll,
} = require('./gamma');
const { get } = require('../utils/q');
const slugify = require('../utils/slugify');
function extractLowArtActors(release) {
const actors = release.title
.replace(/solo/i, '')
.split(/,|\band\b/ig)
.map(actor => actor.trim());
const actors = release.title
.replace(/solo/i, '')
.split(/,|\band\b/ig)
.map(actor => actor.trim());
return {
...release,
actors,
};
return {
...release,
actors,
};
}
async function networkFetchLatest(site, page = 1) {
if (site.parameters?.api) return fetchApiLatest(site, page, false);
if (site.parameters?.api) return fetchApiLatest(site, page, false);
const releases = await fetchLatest(site, page);
const releases = await fetchLatest(site, page);
if (site.slug === 'lowartfilms') {
return releases.map(release => extractLowArtActors(release));
}
if (site.slug === 'lowartfilms') {
return releases.map(release => extractLowArtActors(release));
}
return releases;
return releases;
}
async function networkFetchScene(url, site) {
const release = await fetchScene(url, site);
const release = await fetchScene(url, site);
if (site.slug === 'lowartfilms') {
return extractLowArtActors(release);
}
if (site.slug === 'lowartfilms') {
return extractLowArtActors(release);
}
return release;
return release;
}
async function networkFetchUpcoming(site, page = 1) {
if (site.parameters?.api) return fetchApiUpcoming(site, page, true);
if (site.parameters?.api) return fetchApiUpcoming(site, page, true);
return fetchUpcoming(site, page);
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}`;
return `https://www.peternorth.com/en/videos/All-Categories/0${actorPath}/All-Dvds/0/latest/${page}`;
}
async function fetchClassicProfile(actorName, siteSlug) {
const actorSlug = slugify(actorName);
const actorSlug = slugify(actorName);
const url = `https://${siteSlug}.com/en/pornstars`;
const pornstarsRes = await get(url);
const url = `https://${siteSlug}.com/en/pornstars`;
const pornstarsRes = await get(url);
if (!pornstarsRes.ok) return null;
if (!pornstarsRes.ok) return null;
const actorPath = pornstarsRes.item.qa('option[value*="/pornstar"]')
.find(el => slugify(el.textContent) === actorSlug)
const actorPath = pornstarsRes.item.qa('option[value*="/pornstar"]')
.find(el => slugify(el.textContent) === actorSlug)
?.value;
if (actorPath) {
const actorUrl = `https://${siteSlug}.com${actorPath}`;
const res = await get(actorUrl);
if (actorPath) {
const actorUrl = `https://${siteSlug}.com${actorPath}`;
const res = await get(actorUrl);
if (res.ok) {
const releases = scrapeAll(res.item, null, `https://www.${siteSlug}.com`, false);
if (res.ok) {
const releases = scrapeAll(res.item, null, `https://www.${siteSlug}.com`, false);
return { releases };
}
}
return { releases };
}
}
return null;
return null;
}
async function networkFetchProfile(actorName, scraperSlug, site, include) {
// not all Fame Digital sites offer Gamma actors
const [devils, rocco, peter, silvia] = await Promise.all([
fetchApiProfile(actorName, 'devilsfilm', true),
fetchApiProfile(actorName, 'roccosiffredi'),
include.scenes ? fetchProfile(actorName, 'peternorth', true, getActorReleasesUrl, include) : [],
include.scenes ? fetchClassicProfile(actorName, 'silviasaint') : [],
include.scenes ? fetchClassicProfile(actorName, 'silverstonedvd') : [],
]);
// not all Fame Digital sites offer Gamma actors
const [devils, rocco, peter, silvia] = await Promise.all([
fetchApiProfile(actorName, 'devilsfilm', true),
fetchApiProfile(actorName, 'roccosiffredi'),
include.scenes ? fetchProfile(actorName, 'peternorth', true, getActorReleasesUrl, include) : [],
include.scenes ? fetchClassicProfile(actorName, 'silviasaint') : [],
include.scenes ? fetchClassicProfile(actorName, 'silverstonedvd') : [],
]);
if (devils || rocco || peter) {
const releases = [].concat(devils?.releases || [], rocco?.releases || [], peter?.releases || [], silvia?.releases || []);
if (devils || rocco || peter) {
const releases = [].concat(devils?.releases || [], rocco?.releases || [], peter?.releases || [], silvia?.releases || []);
return {
...peter,
...rocco,
...devils,
releases,
};
}
return {
...peter,
...rocco,
...devils,
releases,
};
}
return null;
return null;
}
module.exports = {
fetchLatest: networkFetchLatest,
fetchProfile: networkFetchProfile,
fetchScene: networkFetchScene,
fetchUpcoming: networkFetchUpcoming,
fetchLatest: networkFetchLatest,
fetchProfile: networkFetchProfile,
fetchScene: networkFetchScene,
fetchUpcoming: networkFetchUpcoming,
};

View File

@ -4,7 +4,7 @@ const { fetchLatest, fetchUpcoming, fetchScene } = require('./gamma');
module.exports = {
fetchLatest,
fetchScene,
fetchUpcoming,
fetchLatest,
fetchScene,
fetchUpcoming,
};

View File

@ -5,89 +5,89 @@ const { JSDOM } = require('jsdom');
const moment = require('moment');
function scrapeProfile(html, actorName) {
const { document } = new JSDOM(html).window;
const profile = { name: actorName };
const { document } = new JSDOM(html).window;
const profile = { name: actorName };
const bio = Array.from(document.querySelectorAll('a[href^="/babes"]'), el => decodeURI(el.href)).reduce((acc, item) => {
const keyMatch = item.match(/\[\w+\]/);
const bio = Array.from(document.querySelectorAll('a[href^="/babes"]'), el => decodeURI(el.href)).reduce((acc, item) => {
const keyMatch = item.match(/\[\w+\]/);
if (keyMatch) {
const key = keyMatch[0].slice(1, -1);
const [, value] = item.split('=');
if (keyMatch) {
const key = keyMatch[0].slice(1, -1);
const [, value] = item.split('=');
// both hip and waist link to 'waist', assume biggest value is hip
if (key === 'waist' && acc.waist) {
if (acc.waist > value) {
acc.hip = acc.waist;
acc.waist = value;
// both hip and waist link to 'waist', assume biggest value is hip
if (key === 'waist' && acc.waist) {
if (acc.waist > value) {
acc.hip = acc.waist;
acc.waist = value;
return acc;
}
return acc;
}
acc.hip = value;
acc.hip = value;
return acc;
}
return acc;
}
acc[key] = value;
}
acc[key] = value;
}
return acc;
}, {});
return acc;
}, {});
if (bio.dateOfBirth) profile.birthdate = moment.utc(bio.dateOfBirth, 'YYYY-MM-DD').toDate();
if (bio.dateOfBirth) profile.birthdate = moment.utc(bio.dateOfBirth, 'YYYY-MM-DD').toDate();
if (profile.placeOfBirth || bio.country) profile.birthPlace = `${bio.placeOfBirth}, ${bio.country}`;
profile.eyes = bio.eyeColor;
profile.hair = bio.hairColor;
profile.ethnicity = bio.ethnicity;
if (profile.placeOfBirth || bio.country) profile.birthPlace = `${bio.placeOfBirth}, ${bio.country}`;
profile.eyes = bio.eyeColor;
profile.hair = bio.hairColor;
profile.ethnicity = bio.ethnicity;
profile.bust = bio.bra;
if (bio.waist) profile.waist = Number(bio.waist.split(',')[0]);
if (bio.hip) profile.hip = Number(bio.hip.split(',')[0]);
profile.bust = bio.bra;
if (bio.waist) profile.waist = Number(bio.waist.split(',')[0]);
if (bio.hip) profile.hip = Number(bio.hip.split(',')[0]);
if (bio.height) profile.height = Number(bio.height.split(',')[0]);
if (bio.weight) profile.weight = Number(bio.weight.split(',')[0]);
if (bio.height) profile.height = Number(bio.height.split(',')[0]);
if (bio.weight) profile.weight = Number(bio.weight.split(',')[0]);
profile.social = Array.from(document.querySelectorAll('.profile-meta-item a.social-icons'), el => el.href);
profile.social = Array.from(document.querySelectorAll('.profile-meta-item a.social-icons'), el => el.href);
const avatar = document.querySelector('.profile-image-large img').src;
if (!avatar.match('placeholder')) profile.avatar = { src: avatar, copyright: null };
const avatar = document.querySelector('.profile-image-large img').src;
if (!avatar.match('placeholder')) profile.avatar = { src: avatar, copyright: null };
return profile;
return profile;
}
function scrapeSearch(html) {
const { document } = new JSDOM(html).window;
const { document } = new JSDOM(html).window;
return document.querySelector('a.image-link')?.href || null;
return document.querySelector('a.image-link')?.href || null;
}
async function fetchProfile(actorName) {
const actorSlug = actorName.toLowerCase().replace(/\s+/g, '-');
const actorSlug = actorName.toLowerCase().replace(/\s+/g, '-');
const res = await bhttp.get(`https://freeones.nl/${actorSlug}/profile`);
const res = await bhttp.get(`https://freeones.nl/${actorSlug}/profile`);
if (res.statusCode === 200) {
return scrapeProfile(res.body.toString(), actorName);
}
if (res.statusCode === 200) {
return scrapeProfile(res.body.toString(), actorName);
}
const searchRes = await bhttp.get(`https://freeones.nl/babes?q=${actorName}`);
const actorPath = scrapeSearch(searchRes.body.toString());
const searchRes = await bhttp.get(`https://freeones.nl/babes?q=${actorName}`);
const actorPath = scrapeSearch(searchRes.body.toString());
if (actorPath) {
const actorRes = await bhttp.get(`https://freeones.nl${actorPath}/profile`);
if (actorPath) {
const actorRes = await bhttp.get(`https://freeones.nl${actorPath}/profile`);
if (actorRes.statusCode === 200) {
return scrapeProfile(actorRes.body.toString(), actorName);
}
if (actorRes.statusCode === 200) {
return scrapeProfile(actorRes.body.toString(), actorName);
}
return null;
}
return null;
}
return null;
return null;
}
module.exports = {
fetchProfile,
fetchProfile,
};

View File

@ -6,135 +6,135 @@ const { JSDOM } = require('jsdom');
const moment = require('moment');
async function scrapeProfileFrontpage(html, url, name) {
const { document } = new JSDOM(html).window;
const bioEl = document.querySelector('.dashboard-bio-list');
const { document } = new JSDOM(html).window;
const bioEl = document.querySelector('.dashboard-bio-list');
const bioUrl = `https:${document.querySelector('.seemore a').href}`;
const bioUrl = `https:${document.querySelector('.seemore a').href}`;
const keys = Array.from(bioEl.querySelectorAll('dt'), el => el.textContent.trim());
const values = Array.from(bioEl.querySelectorAll('dd'), el => el.textContent.trim());
const keys = Array.from(bioEl.querySelectorAll('dt'), el => el.textContent.trim());
const values = Array.from(bioEl.querySelectorAll('dd'), el => el.textContent.trim());
const bio = keys.reduce((acc, key, index) => ({ ...acc, [key]: values[index] }), {});
const bio = keys.reduce((acc, key, index) => ({ ...acc, [key]: values[index] }), {});
const profile = {
name,
gender: 'female',
};
const profile = {
name,
gender: 'female',
};
const birthdateString = bio['Date of Birth:'];
const measurementsString = bio['Measurements:'];
const birthdateString = bio['Date of Birth:'];
const measurementsString = bio['Measurements:'];
const birthCityString = bio['Place of Birth:'];
const birthCity = birthCityString !== undefined && birthCityString !== 'Unknown' && birthCityString !== 'Unknown (add)' && birthCityString;
const birthCityString = bio['Place of Birth:'];
const birthCity = birthCityString !== undefined && birthCityString !== 'Unknown' && birthCityString !== 'Unknown (add)' && birthCityString;
const birthCountryString = bio['Country of Origin:'];
const birthCountry = birthCountryString !== undefined && birthCountryString !== 'Unknown' && birthCountryString !== 'Unknown (add)' && birthCountryString;
const birthCountryString = bio['Country of Origin:'];
const birthCountry = birthCountryString !== undefined && birthCountryString !== 'Unknown' && birthCountryString !== 'Unknown (add)' && birthCountryString;
const piercingsString = bio['Piercings:'];
const tattoosString = bio['Tattoos:'];
const piercingsString = bio['Piercings:'];
const tattoosString = bio['Tattoos:'];
if (birthdateString && birthdateString !== 'Unknown (add)') profile.birthdate = moment.utc(birthdateString.slice(0, birthdateString.indexOf(' (')), 'MMMM D, YYYY').toDate();
if (measurementsString) [profile.bust, profile.waist, profile.hip] = measurementsString.split('-').map(measurement => (measurement === '??' ? null : measurement));
if (birthdateString && birthdateString !== 'Unknown (add)') profile.birthdate = moment.utc(birthdateString.slice(0, birthdateString.indexOf(' (')), 'MMMM D, YYYY').toDate();
if (measurementsString) [profile.bust, profile.waist, profile.hip] = measurementsString.split('-').map(measurement => (measurement === '??' ? null : measurement));
if (bio['Fake Boobs:']) profile.naturalBoobs = bio['Fake Boobs:'] === 'No';
profile.birthPlace = `${birthCity || ''}${birthCity ? ', ' : ''}${birthCountry || ''}`;
if (bio['Fake Boobs:']) profile.naturalBoobs = bio['Fake Boobs:'] === 'No';
profile.birthPlace = `${birthCity || ''}${birthCity ? ', ' : ''}${birthCountry || ''}`;
profile.hair = bio['Hair Color:'].toLowerCase();
profile.eyes = bio['Eye Color:'].toLowerCase();
profile.hair = bio['Hair Color:'].toLowerCase();
profile.eyes = bio['Eye Color:'].toLowerCase();
if (piercingsString) profile.hasPiercings = !!(piercingsString !== 'Unknown (add)' && piercingsString !== 'None');
if (tattoosString) profile.hasTattoos = !!(tattoosString !== 'Unknown (add)' && tattoosString !== 'None');
if (piercingsString) profile.hasPiercings = !!(piercingsString !== 'Unknown (add)' && piercingsString !== 'None');
if (tattoosString) profile.hasTattoos = !!(tattoosString !== 'Unknown (add)' && tattoosString !== 'None');
if (profile.hasPiercings && piercingsString !== 'various') profile.piercings = piercingsString;
if (profile.hasTattoos && tattoosString !== 'various') profile.tattoos = tattoosString;
if (profile.hasPiercings && piercingsString !== 'various') profile.piercings = piercingsString;
if (profile.hasTattoos && tattoosString !== 'various') profile.tattoos = tattoosString;
profile.social = Array.from(bioEl.querySelectorAll('.dashboard-socialmedia a'), el => el.href);
profile.social = Array.from(bioEl.querySelectorAll('.dashboard-socialmedia a'), el => el.href);
return {
profile,
url: bioUrl,
};
return {
profile,
url: bioUrl,
};
}
async function scrapeProfileBio(html, frontpageProfile, url, name) {
const { document } = new JSDOM(html).window;
const bioEl = document.querySelector('#biographyTable');
const { document } = new JSDOM(html).window;
const bioEl = document.querySelector('#biographyTable');
const keys = Array.from(bioEl.querySelectorAll('td:nth-child(1)'), el => el.textContent.trim());
const values = Array.from(bioEl.querySelectorAll('td:nth-child(2)'), el => el.textContent.trim());
const keys = Array.from(bioEl.querySelectorAll('td:nth-child(1)'), el => el.textContent.trim());
const values = Array.from(bioEl.querySelectorAll('td:nth-child(2)'), el => el.textContent.trim());
const bio = keys.reduce((acc, key, index) => ({ ...acc, [key]: values[index] }), {});
const bio = keys.reduce((acc, key, index) => ({ ...acc, [key]: values[index] }), {});
const profile = {
...frontpageProfile,
name,
gender: 'female',
};
const profile = {
...frontpageProfile,
name,
gender: 'female',
};
const birthdateString = bio['Date of Birth:'];
const measurementsString = bio['Measurements:'];
const birthdateString = bio['Date of Birth:'];
const measurementsString = bio['Measurements:'];
const birthCityString = bio['Place of Birth:'];
const birthCity = birthCityString !== undefined && birthCityString !== 'Unknown' && birthCityString !== 'Unknown (add)' && birthCityString;
const birthCityString = bio['Place of Birth:'];
const birthCity = birthCityString !== undefined && birthCityString !== 'Unknown' && birthCityString !== 'Unknown (add)' && birthCityString;
const birthCountryString = bio['Country of Origin:'];
const birthCountry = birthCountryString !== undefined && birthCountryString !== 'Unknown' && birthCountryString !== 'Unknown (add)' && birthCountryString;
const birthCountryString = bio['Country of Origin:'];
const birthCountry = birthCountryString !== undefined && birthCountryString !== 'Unknown' && birthCountryString !== 'Unknown (add)' && birthCountryString;
const piercingsString = bio['Piercings:'];
const tattoosString = bio['Tattoos:'];
const piercingsString = bio['Piercings:'];
const tattoosString = bio['Tattoos:'];
if (birthdateString && birthdateString !== 'Unknown') profile.birthdate = moment.utc(birthdateString.slice(0, birthdateString.indexOf(' (')), 'MMMM D, YYYY').toDate();
if (measurementsString) [profile.bust, profile.waist, profile.hip] = measurementsString.split('-').map(measurement => (measurement === '??' ? null : measurement));
if (birthdateString && birthdateString !== 'Unknown') profile.birthdate = moment.utc(birthdateString.slice(0, birthdateString.indexOf(' (')), 'MMMM D, YYYY').toDate();
if (measurementsString) [profile.bust, profile.waist, profile.hip] = measurementsString.split('-').map(measurement => (measurement === '??' ? null : measurement));
if (bio['Fake boobs']) profile.naturalBoobs = bio['Fake boobs:'] === 'No';
profile.ethnicity = bio['Ethnicity:'];
if (bio['Fake boobs']) profile.naturalBoobs = bio['Fake boobs:'] === 'No';
profile.ethnicity = bio['Ethnicity:'];
profile.birthPlace = `${birthCity || ''}${birthCity ? ', ' : ''}${birthCountry || ''}`;
profile.birthPlace = `${birthCity || ''}${birthCity ? ', ' : ''}${birthCountry || ''}`;
profile.hair = bio['Hair Color:'].toLowerCase();
profile.eyes = bio['Eye Color:'].toLowerCase();
profile.height = Number(bio['Height:'].match(/\d+/)[0]);
profile.weight = Number(bio['Weight:'].match(/\d+/)[0]);
profile.hair = bio['Hair Color:'].toLowerCase();
profile.eyes = bio['Eye Color:'].toLowerCase();
profile.height = Number(bio['Height:'].match(/\d+/)[0]);
profile.weight = Number(bio['Weight:'].match(/\d+/)[0]);
if (piercingsString) profile.hasPiercings = !!(piercingsString !== 'Unknown (add)' && piercingsString !== 'None');
if (tattoosString) profile.hasTattoos = !!(tattoosString !== 'Unknown (add)' && tattoosString !== 'None');
if (piercingsString) profile.hasPiercings = !!(piercingsString !== 'Unknown (add)' && piercingsString !== 'None');
if (tattoosString) profile.hasTattoos = !!(tattoosString !== 'Unknown (add)' && tattoosString !== 'None');
if (profile.hasPiercings && piercingsString !== 'various') profile.piercings = piercingsString;
if (profile.hasTattoos && tattoosString !== 'various') profile.tattoos = tattoosString;
if (profile.hasPiercings && piercingsString !== 'various') profile.piercings = piercingsString;
if (profile.hasTattoos && tattoosString !== 'various') profile.tattoos = tattoosString;
profile.social = Array.from(bioEl.querySelectorAll('#socialmedia a'), el => el.href);
profile.social = Array.from(bioEl.querySelectorAll('#socialmedia a'), el => el.href);
return profile;
return profile;
}
async function fetchProfile(actorName) {
const slug = actorName.replace(' ', '_');
const frontpageUrl = `https://www.freeones.com/html/v_links/${slug}`;
const slug = actorName.replace(' ', '_');
const frontpageUrl = `https://www.freeones.com/html/v_links/${slug}`;
const resFrontpage = await bhttp.get(frontpageUrl);
const resFrontpage = await bhttp.get(frontpageUrl);
if (resFrontpage.statusCode === 200) {
const { url, bio } = await scrapeProfileFrontpage(resFrontpage.body.toString(), frontpageUrl, actorName);
const resBio = await bhttp.get(url);
if (resFrontpage.statusCode === 200) {
const { url, bio } = await scrapeProfileFrontpage(resFrontpage.body.toString(), frontpageUrl, actorName);
const resBio = await bhttp.get(url);
return scrapeProfileBio(resBio.body.toString(), bio, url, actorName);
}
return scrapeProfileBio(resBio.body.toString(), bio, url, actorName);
}
// apparently some actors are appended 'Babe' as their surname...
const fallbackSlug = `${slug}_Babe`;
const fallbackUrl = `https://www.freeones.com/html/s_links/${fallbackSlug}`;
const resFallback = await bhttp.get(fallbackUrl);
// apparently some actors are appended 'Babe' as their surname...
const fallbackSlug = `${slug}_Babe`;
const fallbackUrl = `https://www.freeones.com/html/s_links/${fallbackSlug}`;
const resFallback = await bhttp.get(fallbackUrl);
if (resFallback.statusCode === 200) {
const { url, profile } = await scrapeProfileFrontpage(resFallback.body.toString(), fallbackUrl, actorName);
const resBio = await bhttp.get(url);
if (resFallback.statusCode === 200) {
const { url, profile } = await scrapeProfileFrontpage(resFallback.body.toString(), fallbackUrl, actorName);
const resBio = await bhttp.get(url);
return scrapeProfileBio(resBio.body.toString(), profile, url, actorName);
}
return scrapeProfileBio(resBio.body.toString(), profile, url, actorName);
}
return null;
return null;
}
module.exports = {
fetchProfile,
fetchProfile,
};

View File

@ -4,93 +4,93 @@ const { get, geta, ctxa } = require('../utils/q');
const slugify = require('../utils/slugify');
function scrapeAll(scenes) {
return scenes.map(({ el, qu }) => {
const release = {};
return scenes.map(({ el, qu }) => {
const release = {};
release.entryId = el.dataset.setid || qu.q('.update_thumb', 'id').match(/\w+-\w+-(\d+)-\d+/)[1];
release.url = qu.url('.title');
release.entryId = el.dataset.setid || qu.q('.update_thumb', 'id').match(/\w+-\w+-(\d+)-\d+/)[1];
release.url = qu.url('.title');
release.title = qu.q('.title', true);
release.description = qu.q('.title', 'title');
release.title = qu.q('.title', true);
release.description = qu.q('.title', 'title');
release.date = qu.date('.video-data > span:last-child', 'YYYY-MM-DD');
release.duration = qu.dur('.video-data > span');
release.date = qu.date('.video-data > span:last-child', 'YYYY-MM-DD');
release.duration = qu.dur('.video-data > span');
release.actors = qu.all('.update_models a', true);
release.actors = qu.all('.update_models a', true);
const poster = qu.q('.update_thumb', 'src0_1x');
release.poster = [
poster.replace('-1x', '-2x'),
poster,
];
const poster = qu.q('.update_thumb', 'src0_1x');
release.poster = [
poster.replace('-1x', '-2x'),
poster,
];
return release;
});
return release;
});
}
function scrapeScene({ q, qa, qd, qtx }, url, _site) {
const release = { url };
const release = { url };
release.entryId = q('#image_parent img', 'id').match(/\w+-\w+-(\d+)-\d+/)[1];
release.entryId = q('#image_parent img', 'id').match(/\w+-\w+-(\d+)-\d+/)[1];
release.title = q('.trailer_title', true);
release.description = qtx('.text p');
release.date = qd('span[data-dateadded]', 'YYYY-MM-DD', null, 'data-dateadded');
release.title = q('.trailer_title', true);
release.description = qtx('.text p');
release.date = qd('span[data-dateadded]', 'YYYY-MM-DD', null, 'data-dateadded');
release.actors = qa('.update_models a', true);
release.tags = qa('.video-info a[href*="/categories"]', true);
release.actors = qa('.update_models a', true);
release.tags = qa('.video-info a[href*="/categories"]', true);
const poster = q('#image_parent img', 'src0_1x');
release.poster = [
poster.replace('-1x', '-2x'),
poster,
];
const poster = q('#image_parent img', 'src0_1x');
release.poster = [
poster.replace('-1x', '-2x'),
poster,
];
return release;
return release;
}
function scrapeProfile({ el, q, qtx }) {
const profile = {};
const profile = {};
const description = qtx('.model-bio');
if (description) profile.description = description;
const description = qtx('.model-bio');
if (description) profile.description = description;
profile.avatar = [
q('.model-image img', 'src0_2x'),
q('.model-image img', 'src0_1x'),
];
profile.avatar = [
q('.model-image img', 'src0_2x'),
q('.model-image img', 'src0_1x'),
];
profile.releases = scrapeAll(ctxa(el, '.update'));
profile.releases = scrapeAll(ctxa(el, '.update'));
return profile;
return profile;
}
async function fetchLatest(site, page = 1) {
const url = `${site.url}/categories/movies_${page}_d.html`;
const res = await geta(url, '.latest-updates .update');
const url = `${site.url}/categories/movies_${page}_d.html`;
const res = await geta(url, '.latest-updates .update');
return res.ok ? scrapeAll(res.items, site) : res.status;
return res.ok ? scrapeAll(res.items, site) : res.status;
}
async function fetchScene(url, site) {
const res = await get(url, '.content-wrapper');
const res = await get(url, '.content-wrapper');
return res.ok ? scrapeScene(res.item, url, site) : res.status;
return res.ok ? scrapeScene(res.item, url, site) : res.status;
}
async function fetchProfile(actorName, scraperSlug) {
const actorSlug = slugify(actorName, '');
const url = scraperSlug === 'povperverts'
? `https://povperverts.net/models/${actorSlug}.html`
: `https://${scraperSlug}.com/models/${actorSlug}.html`;
const actorSlug = slugify(actorName, '');
const url = scraperSlug === 'povperverts'
? `https://povperverts.net/models/${actorSlug}.html`
: `https://${scraperSlug}.com/models/${actorSlug}.html`;
const res = await get(url);
const res = await get(url);
return res.ok ? scrapeProfile(res.item, actorName) : res.status;
return res.ok ? scrapeProfile(res.item, actorName) : res.status;
}
module.exports = {
fetchLatest,
fetchScene,
fetchProfile,
fetchLatest,
fetchScene,
fetchProfile,
};

Some files were not shown because too many files have changed in this diff Show More