Compare commits

..

No commits in common. "e2c2c9b4f09bfef699f22b33a0820467ddfe1151" and "36c5fa3b52d8c28e7790f2f7ac5ad25fbc291901" have entirely different histories.

104 changed files with 1923 additions and 2774 deletions

View File

@ -3,7 +3,7 @@
v-if="actor" v-if="actor"
class="content actor" class="content actor"
> >
<FilterBar :fetch-releases="fetchActor" /> <FilterBar :fetch-releases="fetchReleases" />
<div class="actor-inner"> <div class="actor-inner">
<div class="profile"> <div class="profile">
@ -136,8 +136,8 @@
> >
<dfn class="bio-label"><Icon icon="height" />Height</dfn> <dfn class="bio-label"><Icon icon="height" />Height</dfn>
<span> <span>
<span class="height-metric">{{ actor.height.metric }} cm</span> <span class="height-metric">{{ actor.height }} cm</span>
<span class="height-imperial">{{ actor.height.imperial }}</span> <span class="height-imperial">{{ imperialHeight.feet }}' {{ imperialHeight.inches }}"</span>
</span> </span>
</li> </li>
@ -148,8 +148,8 @@
<dfn class="bio-label"><Icon icon="scale" />Weight</dfn> <dfn class="bio-label"><Icon icon="scale" />Weight</dfn>
<span> <span>
<span class="weight-metric">{{ actor.weight.metric }} kg</span> <span class="weight-metric">{{ actor.weight }} kg</span>
<span class="weight-imperial">{{ actor.weight.imperial }} lbs</span> <span class="weight-imperial">{{ imperialWeight }} lbs</span>
</span> </span>
</li> </li>
@ -232,19 +232,29 @@
/> />
</div> </div>
<Releases :releases="actor.releases" /> <Releases :releases="releases" />
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import { cmToFeetInches, kgToLbs } from '../../../src/utils/convert';
import Photos from './photos.vue'; import Photos from './photos.vue';
import FilterBar from '../header/filter-bar.vue'; import FilterBar from '../header/filter-bar.vue';
import Releases from '../releases/releases.vue'; import Releases from '../releases/releases.vue';
async function fetchActor() { async function fetchReleases() {
this.actor = await this.$store.dispatch('fetchActors', { actorSlug: this.$route.params.actorSlug }); this.releases = await this.$store.dispatch('fetchActorReleases', this.$route.params.actorSlug);
}
function imperialHeight() {
return cmToFeetInches(this.actor.height);
}
function imperialWeight() {
return kgToLbs(this.actor.weight);
} }
function scrollPhotos(event) { function scrollPhotos(event) {
@ -252,7 +262,10 @@ function scrollPhotos(event) {
} }
async function mounted() { async function mounted() {
this.fetchActor(); [this.actor] = await Promise.all([
this.$store.dispatch('fetchActors', { actorId: this.$route.params.actorSlug }),
this.fetchReleases(),
]);
if (this.actor) { if (this.actor) {
this.pageTitle = this.actor.name; this.pageTitle = this.actor.name;
@ -273,9 +286,13 @@ export default {
expanded: false, expanded: false,
}; };
}, },
computed: {
imperialHeight,
imperialWeight,
},
mounted, mounted,
methods: { methods: {
fetchActor, fetchReleases,
scrollPhotos, scrollPhotos,
}, },
}; };

View File

@ -3,8 +3,7 @@
<Header /> <Header />
<div class="content"> <div class="content">
<!-- key forces rerender when new and old path use same component --> <router-view />
<router-view :key="$route.fullPath" />
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,6 +1,20 @@
<template> <template>
<div class="filter-bar noselect"> <div class="filter-bar noselect">
<span> <span>
<label class="range">
<input
:id="`${_uid}-all`"
:checked="range === 'all'"
type="radio"
class="range-input"
@click="setRange('all')"
>
<label
:for="`${_uid}-all`"
class="range-button"
>All</label>
</label>
<label class="range"> <label class="range">
<input <input
:id="`${_uid}-new`" :id="`${_uid}-new`"
@ -28,20 +42,6 @@
class="range-button" class="range-button"
>Upcoming</label> >Upcoming</label>
</label> </label>
<label class="range">
<input
:id="`${_uid}-all`"
:checked="range === 'all'"
type="radio"
class="range-input"
@click="setRange('all')"
>
<label
:for="`${_uid}-all`"
class="range-button"
>All</label>
</label>
</span> </span>
<span> <span>

View File

@ -15,7 +15,7 @@ import FilterBar from '../header/filter-bar.vue';
import Releases from '../releases/releases.vue'; import Releases from '../releases/releases.vue';
async function fetchReleases() { async function fetchReleases() {
this.releases = await this.$store.dispatch('fetchReleases', { limit: 100 }); this.releases = await this.$store.dispatch('fetchReleases');
} }
async function mounted() { async function mounted() {

View File

@ -1,35 +1,11 @@
<template> <template>
<div <div
v-if="network" v-if="network"
class="content" class="content network"
> >
<FilterBar :fetch-releases="fetchNetwork" /> <FilterBar :fetch-releases="fetchReleases" />
<div class="network">
<div class="sidebar">
<a
:href="network.url"
target="_blank"
rel="noopener noreferrer"
class="title"
>
<img
:src="`/img/logos/${network.slug}/network.png`"
class="logo"
>
</a>
<p
v-if="network.description"
class="description"
>{{ network.description }}</p>
<Sites
v-if="sites.length"
:sites="sites"
/>
</div>
<div class="content-inner">
<div class="header"> <div class="header">
<a <a
:href="network.url" :href="network.url"
@ -41,18 +17,34 @@
:src="`/img/logos/${network.slug}/network.png`" :src="`/img/logos/${network.slug}/network.png`"
class="logo" class="logo"
> >
<Icon
v-if="network.url"
icon="new-tab"
class="icon-href"
/>
</a> </a>
<p class="description">{{ network.description }}</p>
</div> </div>
<div class="content-inner"> <template v-if="sites.length">
<Sites <h3 class="heading">Sites</h3>
v-if="sites.length"
:sites="sites"
class="compact"
/>
<Releases :releases="releases" /> <ul class="nolist sites">
</div> <li
v-for="site in sites"
:key="`site-${site.id}`"
>
<SiteTile :site="site" />
</li>
</ul>
</template>
<Releases
:releases="releases"
:context="network.name"
/>
</div> </div>
</div> </div>
</template> </template>
@ -60,20 +52,22 @@
<script> <script>
import FilterBar from '../header/filter-bar.vue'; import FilterBar from '../header/filter-bar.vue';
import Releases from '../releases/releases.vue'; import Releases from '../releases/releases.vue';
import Sites from '../sites/sites.vue'; import SiteTile from '../tile/site.vue';
async function fetchNetwork() { async function fetchReleases() {
this.network = await this.$store.dispatch('fetchNetworks', this.$route.params.networkSlug); this.releases = await this.$store.dispatch('fetchNetworkReleases', this.$route.params.networkSlug);
}
async function mounted() {
[[this.network]] = await Promise.all([
this.$store.dispatch('fetchNetworks', this.$route.params.networkSlug),
this.fetchReleases(),
]);
this.sites = this.network.sites this.sites = this.network.sites
.filter(site => !site.independent) .filter(site => !site.independent)
.sort(({ name: nameA }, { name: nameB }) => nameA.localeCompare(nameB)); .sort(({ name: nameA }, { name: nameB }) => nameA.localeCompare(nameB));
this.releases = this.network.sites.map(site => site.releases).flat();
}
async function mounted() {
await this.fetchNetwork();
this.pageTitle = this.network.name; this.pageTitle = this.network.name;
} }
@ -81,108 +75,64 @@ export default {
components: { components: {
FilterBar, FilterBar,
Releases, Releases,
Sites, SiteTile,
}, },
data() { data() {
return { return {
network: null, network: null,
sites: null, sites: null,
releases: [], releases: null,
pageTitle: null, pageTitle: null,
}; };
}, },
mounted, mounted,
methods: { methods: {
fetchNetwork, fetchReleases,
}, },
}; };
</script> </script>
<style lang="scss">
@import 'theme';
@media(max-width: $breakpoint3) {
.releases .tiles {
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
}
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
@import 'theme'; @import 'theme';
.network { .header {
display: flex; display: flex;
flex-direction: row; flex-wrap: wrap;
flex-grow: 1; justify-content: space-between;
justify-content: stretch; align-items: top;
overflow-y: auto; margin: 0 0 2rem 0;
} }
.sidebar { .title {
height: 100%; display: inline-flex;
width: 18rem; align-items: top;
display: flex; margin: 0 1rem 0 0;
flex-direction: column;
flex-shrink: 0;
color: $text-contrast;
border-right: solid 1px $shadow-hint;
overflow: hidden;
}
.sidebar .title { &:hover .icon {
border-bottom: solid 1px $shadow-hint; fill: $primary;
}
} }
.logo { .logo {
width: 100%; width: 20rem;
max-height: 8rem; max-height: 8rem;
display: flex;
justify-content: center;
object-fit: contain; object-fit: contain;
box-sizing: border-box; margin: 0 .5rem 0 0;
padding: 1rem;
margin: 0;
filter: $logo-shadow;
} }
.sites.compact { .sites {
display: none; display: grid;
grid-gap: 1rem;
margin: 0 0 2rem 0;
} }
.header { .sites {
width: 100%; grid-template-columns: repeat(auto-fit, 15rem);
height: 4rem;
display: none;
justify-content: center;
border-bottom: solid 1px $shadow-hint;
} }
@media(max-width: $breakpoint) { @media(max-width: $breakpoint) {
.header { .sites {
display: flex; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}
.sites.compact {
display: flex;
padding: 0 1rem 1rem 1rem;
}
.network {
flex-direction: column;
}
.logo {
max-width: 20rem;
height: 100%;
padding: .5rem;
}
.sidebar {
display: none;
height: auto;
width: 100%;
overflow: hidden;
} }
} }
</style> </style>

View File

@ -108,22 +108,6 @@
</ul> </ul>
</div> </div>
<div v-if="release.scenes && release.scenes.length > 0">
<h3>Scenes</h3>
<Releases
v-if="release.scenes && release.scenes.length > 0"
:releases="release.scenes"
class="row"
/>
</div>
<div v-if="release.movie">
<h3>Movie</h3>
<Release :release="release.movie" />
</div>
<div <div
v-if="release.tags.length > 0" v-if="release.tags.length > 0"
class="row" class="row"
@ -212,10 +196,8 @@
</template> </template>
<script> <script>
import Banner from './banner.vue';
import Actor from '../tile/actor.vue'; import Actor from '../tile/actor.vue';
import Release from '../tile/release.vue'; import Banner from './banner.vue';
import Releases from './releases.vue';
function pageTitle() { function pageTitle() {
return this.release && this.release.title; return this.release && this.release.title;
@ -229,8 +211,6 @@ export default {
components: { components: {
Actor, Actor,
Banner, Banner,
Releases,
Release,
}, },
data() { data() {
return { return {
@ -313,7 +293,7 @@ export default {
.logo { .logo {
display: inline-block; display: inline-block;
filter: $logo-shadow; filter: $logo-outline;
} }
.logo-site { .logo-site {

View File

@ -56,7 +56,6 @@ export default {
} }
.tiles { .tiles {
width: 100%;
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, .33fr)); grid-template-columns: repeat(auto-fit, minmax(20rem, .33fr));
grid-gap: 1rem; grid-gap: 1rem;

View File

@ -0,0 +1,157 @@
<template>
<div
v-if="site"
class="content site"
>
<FilterBar :fetch-releases="fetchReleases" />
<div class="content-inner">
<div class="header">
<a
v-if="site.url"
:href="site.url"
target="_blank"
rel="noopener noreferrer"
class="title"
>
<object
:data="`/img/logos/${site.network.slug}/${site.slug}.png`"
:title="site.name"
type="image/png"
class="logo"
><h2>{{ site.name }}</h2></object>
<Icon
icon="new-tab"
class="icon-href"
/>
</a>
<span class="link">
<span class="networklogo-container">
Part of
<a
:href="`/network/${site.network.slug}`"
class="networklogo-link"
>
<object
:data="`/img/logos/${site.network.slug}/network.png`"
:title="site.network.name"
type="image/png"
class="networklogo"
>{{ site.network.name }}</object>
</a>
</span>
</span>
</div>
<p class="description">{{ site.description }}</p>
<Releases
:releases="releases"
:context="site.name"
/>
</div>
</div>
</template>
<script>
import FilterBar from '../header/filter-bar.vue';
import Releases from '../releases/releases.vue';
async function fetchReleases() {
this.releases = await this.$store.dispatch('fetchSiteReleases', this.$route.params.siteSlug);
}
async function mounted() {
[[this.site]] = await Promise.all([
this.$store.dispatch('fetchSites', this.$route.params.siteSlug),
this.fetchReleases(),
]);
this.pageTitle = this.site.name;
}
export default {
components: {
FilterBar,
Releases,
},
data() {
return {
site: null,
releases: null,
pageTitle: null,
};
},
mounted,
methods: {
fetchReleases,
},
};
</script>
<style lang="scss" scoped>
@import 'theme';
.header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.title {
display: inline-flex;
align-items: top;
margin: 0 1rem 0 0;
&:hover .icon {
fill: $primary;
}
}
.heading {
padding: 0;
margin: 0 0 1rem 0;
}
.link {
display: flex;
flex-shrink: 0;
flex-direction: column;
align-items: flex-end;
}
.logo {
width: 20rem;
max-height: 8rem;
object-fit: contain;
margin: 0 .5rem 1rem 0;
}
.networklogo-container {
display: flex;
align-items: center;
}
.networklogo {
color: $text;
width: 15rem;
max-height: 6rem;
font-weight: bold;
object-fit: contain;
object-position: 100% 0;
margin: 0 0 0 .5rem;
}
.sites,
.scenes {
display: grid;
grid-gap: 1rem;
margin: 0 0 1rem 0;
}
.sites {
grid-template-columns: repeat(auto-fit, 15rem);
}
</style>

View File

@ -16,7 +16,6 @@
> >
<img <img
:src="`/img/${tag.poster.thumbnail}`" :src="`/img/${tag.poster.thumbnail}`"
:alt="tag.poster.comment"
class="poster" class="poster"
> >
</a> </a>
@ -44,7 +43,6 @@
> >
<img <img
:src="`/img/${photo.thumbnail}`" :src="`/img/${photo.thumbnail}`"
:alt="photo.comment"
class="photo" class="photo"
> >
</a> </a>
@ -52,7 +50,7 @@
</div> </div>
<div class="content-inner"> <div class="content-inner">
<Releases :releases="tag.releases" /> <Releases :releases="releases" />
</div> </div>
</div> </div>
</div> </div>
@ -70,13 +68,17 @@ import Releases from '../releases/releases.vue';
const converter = new Converter(); const converter = new Converter();
async function fetchReleases() { async function fetchReleases() {
this.tag = await this.$store.dispatch('fetchTags', { tagSlug: this.$route.params.tagSlug }); this.releases = await this.$store.dispatch('fetchTagReleases', this.$route.params.tagSlug);
} }
async function mounted() { async function mounted() {
this.tag = await this.$store.dispatch('fetchTags', { tagSlug: this.$route.params.tagSlug }); [this.tag] = await Promise.all([
this.$store.dispatch('fetchTags', { tagId: this.$route.params.tagSlug }),
this.fetchReleases(),
]);
this.description = converter.makeHtml(escapeHtml(this.tag.description));
this.description = this.tag.description && converter.makeHtml(escapeHtml(this.tag.description));
this.pageTitle = this.tag.name; this.pageTitle = this.tag.name;
} }
@ -88,7 +90,6 @@ export default {
data() { data() {
return { return {
tag: null, tag: null,
description: null,
releases: null, releases: null,
pageTitle: null, pageTitle: null,
}; };

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="tags"> <div class="tags">
<h3>Oral</h3> <h3>Ethnicity</h3>
<div class="tiles"> <div class="tiles">
<Tag <Tag
v-for="tag in tags.oral" v-for="tag in tags.ethnicity"
:key="`tag-${tag.id}`" :key="`tag-${tag.id}`"
:tag="tag" :tag="tag"
/> />
@ -30,16 +30,6 @@
/> />
</div> </div>
<h3>Ethnicity</h3>
<div class="tiles">
<Tag
v-for="tag in tags.ethnicity"
:key="`tag-${tag.id}`"
:tag="tag"
/>
</div>
<h3>Finish</h3> <h3>Finish</h3>
<div class="tiles"> <div class="tiles">
@ -49,16 +39,6 @@
:tag="tag" :tag="tag"
/> />
</div> </div>
<h3>Misc</h3>
<div class="tiles">
<Tag
v-for="tag in tags.misc.concat(tags.body)"
:key="`tag-${tag.id}`"
:tag="tag"
/>
</div>
</div> </div>
</template> </template>
@ -67,55 +47,41 @@ import Tag from '../tile/tag.vue';
async function mounted() { async function mounted() {
const tags = await this.$store.dispatch('fetchTags', { const tags = await this.$store.dispatch('fetchTags', {
slugs: [ slug: [
'airtight', 'airtight',
'anal', 'anal',
'anal-creampie',
'asian',
'ass-eating',
'ass-to-mouth',
'blowbang',
'blowjob',
'bukkake',
'caucasian',
'creampie',
'da-tp',
'deepthroat',
'double-anal', 'double-anal',
'double-blowjob',
'double-penetration', 'double-penetration',
'double-vaginal', 'double-vaginal',
'da-tp',
'dv-tp', 'dv-tp',
'ebony', 'triple-anal',
'facefuck', 'blowbang',
'facial',
'gangbang', 'gangbang',
'gapes',
'interracial',
'latina',
'mff', 'mff',
'mfm', 'mfm',
'oral-creampie',
'orgy', 'orgy',
'pussy-eating', 'asian',
'caucasian',
'ebony',
'interracial',
'latina',
'anal-creampie',
'bukkake',
'creampie',
'facial',
'oral-creampie',
'swallowing', 'swallowing',
'tattoo',
'trainbang',
'triple-anal',
], ],
}); });
this.tags = tags.reduce((acc, tag) => { this.tags = tags.reduce((acc, tag) => {
if (!tag.group) {
return { ...acc, misc: [...acc.misc, tag] };
}
if (acc[tag.group.slug]) { if (acc[tag.group.slug]) {
return { ...acc, [tag.group.slug]: [...acc[tag.group.slug], tag] }; return { ...acc, [tag.group.slug]: [...acc[tag.group.slug], tag] };
} }
return { ...acc, [tag.group.slug]: [tag] }; return { ...acc, [tag.group.slug]: [tag] };
}, { misc: [] }); }, {});
} }
export default { export default {

View File

@ -27,18 +27,17 @@
v-if="actor.age || actor.origin" v-if="actor.age || actor.origin"
class="details" class="details"
> >
<span> <span v-if="actor.age">
<span <span
v-if="actor.age"
v-tooltip="`Born on ${formatDate(actor.birthdate, 'MMMM D, YYYY')}`" v-tooltip="`Born on ${formatDate(actor.birthdate, 'MMMM D, YYYY')}`"
class="age" class="age"
>{{ actor.age }}</span> >{{ actor.age }}</span>
<span <span
v-if="actor.ageThen && actor.ageThen < actor.age" v-if="actor.ageThen && actor.ageThen < actor.age"
v-tooltip="`${actor.ageThen} years old on release date`" v-tooltip="'Age at scene date'"
class="age-then" class="age-then"
>{{ actor.ageThen }}</span> >@ {{ actor.ageThen }}</span>
</span> </span>
<span <span
v-if="actor.origin" v-if="actor.origin"
@ -115,7 +114,6 @@ export default {
color: $text-contrast; color: $text-contrast;
width: 100%; width: 100%;
display: flex; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
box-sizing: border-box; box-sizing: border-box;
padding: .5rem; padding: .5rem;

View File

@ -52,7 +52,7 @@ export default {
object-fit: contain; object-fit: contain;
font-size: 1rem; font-size: 1rem;
font-weight: bold; font-weight: bold;
filter: $logo-shadow; filter: $logo-outline;
} }
.title { .title {

View File

@ -1,8 +1,5 @@
<template> <template>
<div <div class="tile">
class="tile"
:class="{ movie: release.type === 'movie' }"
>
<span class="banner"> <span class="banner">
<span class="details"> <span class="details">
<router-link <router-link
@ -42,7 +39,7 @@
</span> </span>
<router-link <router-link
:to="`/${release.type || 'scene'}/${release.id}`" :to="`/scene/${release.id}`"
class="link" class="link"
> >
<img <img
@ -69,19 +66,14 @@
<div class="info"> <div class="info">
<router-link <router-link
:to="`/${release.type || 'scene'}/${release.id}`" :to="`/scene/${release.id}`"
class="row link" class="row link"
> >
<h3 <h3
v-tooltip.top="release.title" v-tooltip.top="release.title"
:title="release.title" :title="release.title"
class="title" class="title"
> >{{ release.title }}</h3>
<Icon
v-if="release.type === 'movie'"
icon="film"
/>{{ release.title }}
</h3>
</router-link> </router-link>
<span class="row"> <span class="row">
@ -220,19 +212,13 @@ export default {
} }
.title { .title {
display: flex;
align-items: center;
margin: 0 .25rem .25rem 0;
color: $text; color: $text;
margin: 0 .25rem .25rem 0;
font-size: 1rem; font-size: 1rem;
max-height: 3rem; max-height: 3rem;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
.icon {
margin: 0 .25rem 0 0;
}
} }
.network { .network {
@ -249,8 +235,9 @@ export default {
} }
.tags { .tags {
max-height: .5rem; max-height: 2.5rem;
padding: .25rem .5rem 1rem .5rem; padding: .25rem .5rem 1rem .5rem;
line-height: 1.5rem;
word-wrap: break-word; word-wrap: break-word;
overflow-y: hidden; overflow-y: hidden;
} }

View File

@ -4,11 +4,11 @@
:title="site.name" :title="site.name"
class="tile" class="tile"
> >
<img <object
:src="`/img/logos/${site.network.slug}/${site.slug}.png`" :data="`/img/logos/${site.network.slug}/${site.slug}.png`"
:alt="site.name" type="image/png"
class="logo" class="logo"
> >{{ site.name }}</object>
</a> </a>
</template> </template>
@ -53,7 +53,7 @@ export default {
object-fit: contain; object-fit: contain;
font-size: 1rem; font-size: 1rem;
font-weight: bold; font-weight: bold;
filter: $logo-shadow; filter: $logo-outline;
} }
.title { .title {

View File

@ -4,14 +4,14 @@
:title="tag.name" :title="tag.name"
class="tile" class="tile"
> >
<span class="title">{{ tag.name }}</span>
<img <img
v-if="tag.poster" v-if="tag.poster"
:src="`/img/${tag.poster.thumbnail}`" :src="`/img/${tag.poster.thumbnail}`"
:alt="tag.name" :alt="tag.name"
class="poster" class="poster"
> >
<span class="title">{{ tag.name }}</span>
</a> </a>
</template> </template>

View File

@ -23,8 +23,7 @@ $highlight-strong: rgba(255, 255, 255, .7);
$highlight-weak: rgba(255, 255, 255, .2); $highlight-weak: rgba(255, 255, 255, .2);
$highlight-hint: rgba(255, 255, 255, .075); $highlight-hint: rgba(255, 255, 255, .075);
$logo-shadow: drop-shadow(1px 0 0 $shadow-weak) drop-shadow(-1px 0 0 $shadow-weak) drop-shadow(0 1px 0 $shadow-weak) drop-shadow(0 -1px 0 $shadow-weak); $logo-outline: drop-shadow(1px 0 0 $shadow-weak) drop-shadow(-1px 0 0 $shadow-weak) drop-shadow(0 1px 0 $shadow-weak) drop-shadow(0 -1px 0 $shadow-weak);
$logo-highlight: drop-shadow(1px 0 0 $highlight-weak) drop-shadow(-1px 0 0 $highlight-weak) drop-shadow(0 1px 0 $highlight-weak) drop-shadow(0 -1px 0 $highlight-weak);
$profile: #222; $profile: #222;

View File

@ -1,5 +0,0 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>film</title>
<path d="M0 0v16h16v-16h-16zM10 1h2v2h-2v-2zM7 1h2v2h-2v-2zM4 1h2v2h-2v-2zM1 1h2v2h-2v-2zM1 4h6v8h-6v-8zM3 15h-2v-2h2v2zM6 15h-2v-2h2v2zM9 15h-2v-2h2v2zM12 15h-2v-2h2v2zM15 15h-2v-2h2v2zM15 12h-7v-8h7v8zM15 3h-2v-2h2v2z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 387 B

View File

@ -1,5 +0,0 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>film2</title>
<path d="M0 1v14h16v-14h-16zM3 14h-2v-2h2v2zM3 11h-2v-2h2v2zM3 7h-2v-2h2v2zM3 4h-2v-2h2v2zM12 14h-8v-5h8v5zM12 7h-8v-5h8v5zM15 14h-2v-2h2v2zM15 11h-2v-2h2v2zM15 7h-2v-2h2v2zM15 4h-2v-2h2v2z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 358 B

View File

@ -1,5 +0,0 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>film3</title>
<path d="M0 2v12h16v-12h-16zM3 13h-2v-2h2v2zM3 9h-2v-2h2v2zM3 5h-2v-2h2v2zM12 13h-8v-10h8v10zM15 13h-2v-2h2v2zM15 9h-2v-2h2v2zM15 5h-2v-2h2v2z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 311 B

View File

@ -1,191 +1,12 @@
import { graphql, get } from '../api'; import { get } from '../api';
import {
releasePosterFragment,
releaseActorsFragment,
releaseTagsFragment,
} from '../fragments';
import { curateRelease } from '../curate';
function curateActor(actor) {
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,
},
};
if (actor.avatar) {
curatedActor.avatar = actor.avatar.media;
}
if (actor.releases) {
curatedActor.releases = actor.releases.map(release => curateRelease(release.release));
}
if (actor.photos) {
curatedActor.photos = actor.photos.map(photo => photo.media);
}
return curatedActor;
}
function initActorActions(store, _router) { function initActorActions(store, _router) {
async function fetchActorBySlug(actorSlug, limit = 100) { async function fetchActors({ _commit }, { actorId, limit = 100 }) {
const { actor } = await graphql(` if (actorId) {
query Actor( return get(`/actors/${actorId}`, { limit });
$actorSlug: String!
$limit:Int = 1000,
$after:Date = "1900-01-01",
$before:Date = "2100-01-01",
) {
actor: actorBySlug(slug: $actorSlug) {
id
name
slug
gender
birthdate
age
ethnicity
bust
waist
hip
heightMetric: height(units:METRIC)
heightImperial: height(units:IMPERIAL)
weightMetric: weight(units:METRIC)
weightImperial: weight(units:IMPERIAL)
hasTattoos
hasPiercings
tattoos
piercings
avatar: actorsAvatarByActorId {
media {
thumbnail
path
}
}
photos: actorsPhotos {
media {
id
thumbnail
path
index
}
}
birthCity
birthState
birthCountry: countryByBirthCountryAlpha2 {
alpha2
name
alias
}
residenceCity
residenceState
residenceCountry: countryByResidenceCountryAlpha2 {
alpha2
name
alias
}
social: actorsSocials {
id
url
platform
}
aliases: actorsByAliasFor {
id
name
slug
}
releases: releasesActors(
filter: {
release: {
date: {
lessThan: $before,
greaterThan: $after,
}
}
},
first: $limit,
orderBy: RELEASE_BY_RELEASE_ID__DATE_DESC,
) {
release {
id
url
title
date
${releaseActorsFragment}
${releaseTagsFragment}
${releasePosterFragment}
site {
id
name
slug
url
network {
id
name
slug
url
}
}
}
}
}
}
`, {
actorSlug,
limit,
after: store.getters.after,
before: store.getters.before,
});
return curateActor(actor);
}
async function fetchActors({ _commit }, { actorSlug, limit = 100 }) {
if (actorSlug) {
return fetchActorBySlug(actorSlug);
} }
const { actors } = await graphql(` return get('/actors', { limit });
query Actors($limit:Int) {
actors(first:$limit) {
id
name
slug
age
birthdate
avatar: actorsAvatarByActorId {
media {
thumbnail
}
}
birthCountry: countryByBirthCountryAlpha2 {
alpha2
name
alias
}
}
}
`, {
limit,
});
return actors.map(actor => curateActor(actor));
} }
async function fetchActorReleases({ _commit }, actorId) { async function fetchActorReleases({ _commit }, actorId) {

View File

@ -21,7 +21,7 @@ async function get(endpoint, query = {}) {
async function post(endpoint, data) { async function post(endpoint, data) {
const res = await fetch(`${config.api.url}${endpoint}`, { const res = await fetch(`${config.api.url}${endpoint}`, {
method: 'POST', method: 'GET',
mode: 'cors', mode: 'cors',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -39,33 +39,4 @@ async function post(endpoint, data) {
throw new Error(errorMsg); throw new Error(errorMsg);
} }
async function graphql(query, variables = null) { export { get, post };
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();
return data;
}
const errorMsg = await res.text();
throw new Error(errorMsg);
}
export {
get,
post,
graphql,
};

View File

@ -1,76 +0,0 @@
function curateActor(actor) {
const curatedActor = {
...actor,
origin: actor.originCountry && {
country: actor.originCountry,
},
};
if (actor.avatar) curatedActor.avatar = actor.avatar.media;
return curatedActor;
}
function curateRelease(release) {
const curatedRelease = {
...release,
actors: release.actors ? release.actors.map(({ actor }) => curateActor(actor)) : [],
poster: release.poster && release.poster.media,
tags: release.tags ? release.tags.map(({ tag }) => tag) : [],
network: release.site.network,
};
if (release.photos) curatedRelease.photos = release.photos.map(({ media }) => media);
if (release.trailer) curatedRelease.trailer = release.trailer.media;
return curatedRelease;
}
function curateSite(site, network) {
const curatedSite = {
id: site.id,
name: site.name,
slug: site.slug,
url: site.url,
};
if (site.releases) curatedSite.releases = site.releases.map(release => curateRelease(release));
if (site.network || network) curatedSite.network = site.network || network;
return curatedSite;
}
function curateNetwork(network) {
const curatedNetwork = {
id: network.id,
name: network.name,
slug: network.slug,
url: network.url,
};
if (network.sites) {
curatedNetwork.sites = network.sites.map(site => curateSite(site, curatedNetwork));
}
return curatedNetwork;
}
function curateTag(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;
return curatedTag;
}
export {
curateActor,
curateRelease,
curateSite,
curateNetwork,
curateTag,
};

View File

@ -1,154 +0,0 @@
const siteFragment = `
site {
id
name
slug
url
network {
id
name
slug
url
}
}
`;
const sitesFragment = `
sites {
id
name
slug
url
network {
id
name
slug
url
}
}
`;
const releaseActorsFragment = `
actors: releasesActors(orderBy: ACTOR_BY_ACTOR_ID__GENDER_ASC) {
actor {
id
name
slug
birthdate
age
originCountry: countryByBirthCountryAlpha2 {
alpha2
name
alias
}
avatar: actorsAvatarByActorId {
media {
thumbnail
}
}
}
}
`;
const releaseTagsFragment = `
tags: releasesTags(orderBy: TAG_BY_TAG_ID__PRIORITY_DESC) {
tag {
name
priority
slug
id
}
}
`;
const releasePosterFragment = `
poster: releasesPosterByReleaseId {
media {
index
path
thumbnail
comment
}
}
`;
const releasePhotosFragment = `
photos: releasesPhotos {
media {
index
path
thumbnail
comment
}
}
`;
const releaseTrailerFragment = `
trailer: releasesTrailerByReleaseId {
media {
index
path
thumbnail
}
}
`;
const releasesFragment = `
releases(
filter: {
date: {
lessThan: $before,
greaterThan: $after,
}
},
first: $limit,
orderBy: DATE_DESC,
) {
id
title
date
createdAt
url
${releaseActorsFragment}
${releaseTagsFragment}
${releasePosterFragment}
${siteFragment}
}
`;
const releaseFragment = `
release(id: $releaseId) {
id
title
description
date
duration
createdAt
shootId
url
${releaseActorsFragment}
${releaseTagsFragment}
${releasePosterFragment}
${releasePhotosFragment}
${releaseTrailerFragment}
${siteFragment}
studio {
id
name
slug
url
}
}
`;
export {
releaseActorsFragment,
releaseTagsFragment,
releasePosterFragment,
releasePhotosFragment,
releaseTrailerFragment,
releasesFragment,
releaseFragment,
siteFragment,
sitesFragment,
};

View File

@ -1,68 +1,27 @@
import { graphql } from '../api'; import { get } from '../api';
import { sitesFragment, releasesFragment } from '../fragments';
import { curateNetwork } from '../curate';
function initNetworksActions(store, _router) { function initNetworksActions(store, _router) {
async function fetchNetworkBySlug(networkSlug, limit = 100) { async function fetchNetworks({ _commit }, networkId) {
const { network } = await graphql(` const networks = await get(`/networks/${networkId || ''}`, {
query Network(
$networkSlug: String! });
$limit:Int = 1000,
$after:Date = "1900-01-01", return networks;
$before:Date = "2100-01-01", }
) {
network: networkBySlug(slug: $networkSlug) { async function fetchNetworkReleases({ _commit }, networkId) {
id const releases = await get(`/networks/${networkId}/releases`, {
name filter: store.state.ui.filter,
slug
url
sites {
id
name
slug
url
${releasesFragment}
network {
id
name
slug
url
}
}
}
}
`, {
networkSlug,
limit,
after: store.getters.after, after: store.getters.after,
before: store.getters.before, before: store.getters.before,
}); });
return curateNetwork(network); return releases;
}
async function fetchNetworks({ _commit }, networkSlug) {
if (networkSlug) {
return fetchNetworkBySlug(networkSlug);
}
const { networks } = await graphql(`
query Networks {
networks {
id
name
slug
url
${sitesFragment}
}
}
`);
return networks.map(network => curateNetwork(network));
} }
return { return {
fetchNetworks, fetchNetworks,
fetchNetworkReleases,
}; };
} }

View File

@ -1,40 +1,20 @@
import { graphql } from '../api'; import { get } from '../api';
import { releasesFragment, releaseFragment } from '../fragments';
import { curateRelease } from '../curate';
function initReleasesActions(store, _router) { function initReleasesActions(store, _router) {
async function fetchReleases({ _commit }, { limit = 100 }) { async function fetchReleases({ _commit }) {
console.log(store.state.ui.filter, store.getters.after, store.getters.before); const releases = await get('/releases', {
filter: store.state.ui.filter,
const { releases } = await graphql(`
query Releases(
$limit:Int = 1000,
$after:Date = "1900-01-01",
$before:Date = "2100-01-01",
) {
${releasesFragment}
}
`, {
limit,
after: store.getters.after, after: store.getters.after,
before: store.getters.before, before: store.getters.before,
}); });
return releases.map(release => curateRelease(release)); return releases;
} }
async function fetchReleaseById({ _commit }, releaseId) { async function fetchReleaseById({ _commit }, releaseId) {
// const release = await get(`/releases/${releaseId}`); const release = await get(`/releases/${releaseId}`);
const { release } = await graphql(` return release;
query Release($releaseId:Int!) {
${releaseFragment}
}
`, {
releaseId: Number(releaseId),
});
return curateRelease(release);
} }
return { return {

View File

@ -3,7 +3,7 @@ import VueRouter from 'vue-router';
import Home from '../components/home/home.vue'; import Home from '../components/home/home.vue';
import Release from '../components/releases/release.vue'; import Release from '../components/releases/release.vue';
import Site from '../components/sites/site.vue'; import Site from '../components/site/site.vue';
import Network from '../components/networks/network.vue'; import Network from '../components/networks/network.vue';
import Networks from '../components/networks/networks.vue'; import Networks from '../components/networks/networks.vue';
import Actor from '../components/actors/actor.vue'; import Actor from '../components/actors/actor.vue';

View File

@ -1,69 +1,12 @@
import { graphql } from '../api'; import { get } from '../api';
import { releasesFragment } from '../fragments';
import { curateSite } from '../curate';
function initSitesActions(store, _router) { function initSitesActions(store, _router) {
async function fetchSiteBySlug(siteSlug, limit = 100) { async function fetchSites({ _commit }, siteId) {
const { site } = await graphql(` const sites = await get(`/sites/${siteId || ''}`);
query Site(
$siteSlug: String!,
$limit:Int = 100,
$after:Date = "1900-01-01",
$before:Date = "2100-01-01",
) {
site: siteBySlug(slug: $siteSlug) {
name
slug
url
network {
id
name
slug
url
}
${releasesFragment}
}
}
`, {
siteSlug,
limit,
after: store.getters.after,
before: store.getters.before,
});
console.log(site);
return curateSite(site);
}
async function fetchSites({ _commit }, { siteSlug, limit = 100 }) {
if (siteSlug) {
return fetchSiteBySlug(siteSlug, limit);
}
const { sites } = await graphql(`
query Sites(
$actorSlug: String!
$limit:Int = 100,
$after:Date = "1900-01-01",
$before:Date = "2100-01-01",
) {
site {
name
slug
url
}
}
`, {
limit,
after: store.getters.after,
before: store.getters.before,
});
return sites; return sites;
} }
/*
async function fetchSiteReleases({ _commit }, siteId) { async function fetchSiteReleases({ _commit }, siteId) {
const releases = await get(`/sites/${siteId}/releases`, { const releases = await get(`/sites/${siteId}/releases`, {
filter: store.state.ui.filter, filter: store.state.ui.filter,
@ -73,11 +16,10 @@ function initSitesActions(store, _router) {
return releases; return releases;
} }
*/
return { return {
fetchSites, fetchSites,
// fetchSiteReleases, fetchSiteReleases,
}; };
} }

View File

@ -1,97 +1,23 @@
import { graphql, get } from '../api'; import { get } from '../api';
import {
releasePosterFragment,
releaseActorsFragment,
releaseTagsFragment,
siteFragment,
} from '../fragments';
import { curateTag } from '../curate';
function initTagsActions(store, _router) { function initTagsActions(store, _router) {
async function fetchTagBySlug(tagSlug) {
const { tagBySlug } = await graphql(`
query Tag($tagSlug:String!) {
tagBySlug(slug:$tagSlug) {
id
name
slug
description
group {
name
slug
}
poster: tagsPosterByTagId {
media {
id
thumbnail
path
comment
}
}
photos: tagsPhotos {
media {
id
thumbnail
path
comment
}
}
releases: releasesTags {
release {
id
title
date
createdAt
url
${releaseActorsFragment}
${releaseTagsFragment}
${releasePosterFragment}
${siteFragment}
}
}
}
}
`, {
tagSlug,
});
return curateTag(tagBySlug);
}
async function fetchTags({ _commit }, { async function fetchTags({ _commit }, {
tagSlug, tagId,
limit = 100, limit = 100,
slugs = [], slug,
_group, group,
_priority, priority,
}) { }) {
if (tagSlug) { if (tagId) {
return fetchTagBySlug(tagSlug); return get(`/tags/${tagId}`);
} }
const { tags } = await graphql(` return get('/tags', {
query Tags($slugs: [String!] = [], $limit: Int = 100) {
tags(filter: {slug: {in: $slugs}}, first: $limit) {
id
name
slug
poster: tagsPosterByTagId {
media {
thumbnail
}
}
group {
name
slug
}
}
}
`, {
slugs,
limit, limit,
slug,
priority,
group,
}); });
return tags.map(tag => curateTag(tag));
} }
async function fetchTagReleases({ _commit }, tagId) { async function fetchTagReleases({ _commit }, tagId) {
@ -107,7 +33,6 @@ function initTagsActions(store, _router) {
return { return {
fetchTags, fetchTags,
fetchTagReleases, fetchTagReleases,
fetchTagBySlug,
}; };
} }

View File

@ -2,16 +2,16 @@ import dayjs from 'dayjs';
const dateRanges = { const dateRanges = {
new: () => ({ new: () => ({
after: '1900-01-01', after: dayjs(new Date(0)).format('YYYY-MM-DD'),
before: dayjs(new Date()).add(1, 'day').format('YYYY-MM-DD'), before: dayjs(new Date()).format('YYYY-MM-DD'),
}), }),
upcoming: () => ({ upcoming: () => ({
after: dayjs(new Date()).format('YYYY-MM-DD'), after: dayjs(new Date()).format('YYYY-MM-DD'),
before: '2100-01-01', before: dayjs(new Date(2 ** 42)).format('YYYY-MM-DD'),
}), }),
all: () => ({ all: () => ({
after: '1900-01-01', after: dayjs(new Date(0)).format('YYYY-MM-DD'),
before: '2100-01-01', before: dayjs(new Date(2 ** 42)).format('YYYY-MM-DD'),
}), }),
}; };

View File

@ -17,184 +17,6 @@ exports.up = knex => Promise.resolve()
table.integer('priority', 2) table.integer('priority', 2)
.defaultTo(0); .defaultTo(0);
})) }))
.then(() => knex.schema.createTable('media', (table) => {
table.increments('id', 16);
table.string('path');
table.string('thumbnail');
table.integer('index');
table.string('mime');
table.string('type');
table.string('quality', 6);
table.string('hash');
table.text('comment');
table.string('source', 1000);
table.unique('hash');
table.unique('source');
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('tags_groups', (table) => {
table.increments('id', 12);
table.string('name', 32);
table.text('description');
table.string('slug', 32)
.unique();
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('tags', (table) => {
table.increments('id', 12);
table.string('name');
table.text('description');
table.integer('priority', 2)
.defaultTo(0);
table.integer('group_id', 12)
.references('id')
.inTable('tags_groups');
table.integer('alias_for', 12)
.references('id')
.inTable('tags');
table.string('slug', 32)
.unique();
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('tags_posters', (table) => {
table.integer('tag_id', 12)
.notNullable()
.references('id')
.inTable('tags');
table.integer('media_id', 16)
.notNullable()
.references('id')
.inTable('media');
table.unique('tag_id');
}))
.then(() => knex.schema.createTable('tags_photos', (table) => {
table.integer('tag_id', 12)
.notNullable()
.references('id')
.inTable('tags');
table.integer('media_id', 16)
.notNullable()
.references('id')
.inTable('media');
table.unique(['tag_id', 'media_id']);
}))
.then(() => knex.schema.createTable('networks', (table) => {
table.increments('id', 12);
table.string('name');
table.string('url');
table.text('description');
table.string('parameters');
table.string('slug', 32)
.unique();
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('networks_social', (table) => {
table.increments('id', 16);
table.string('url');
table.string('platform');
table.integer('network_id', 12)
.notNullable()
.references('id')
.inTable('networks');
table.unique(['url', 'network_id']);
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('sites', (table) => {
table.increments('id', 12);
table.integer('network_id', 12)
.notNullable()
.references('id')
.inTable('networks');
table.string('name');
table.string('url');
table.text('description');
table.string('parameters');
table.string('slug', 32)
.unique();
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('sites_tags', (table) => {
table.integer('tag_id', 12)
.notNullable()
.references('id')
.inTable('tags');
table.integer('site_id', 12)
.notNullable()
.references('id')
.inTable('sites');
table.unique(['tag_id', 'site_id']);
}))
.then(() => knex.schema.createTable('sites_social', (table) => {
table.increments('id', 16);
table.string('url');
table.string('platform');
table.integer('site_id', 12)
.notNullable()
.references('id')
.inTable('sites');
table.unique(['url', 'site_id']);
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('studios', (table) => {
table.increments('id', 12);
table.integer('network_id', 12)
.notNullable()
.references('id')
.inTable('networks');
table.string('name');
table.string('url');
table.text('description');
table.string('slug', 32)
.unique();
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('actors', (table) => { .then(() => knex.schema.createTable('actors', (table) => {
table.increments('id', 12); table.increments('id', 12);
@ -248,48 +70,6 @@ exports.up = knex => Promise.resolve()
table.datetime('scraped_at'); table.datetime('scraped_at');
table.boolean('scrape_success'); table.boolean('scrape_success');
})) }))
.then(() => knex.schema.createTable('actors_avatars', (table) => {
table.integer('actor_id', 12)
.notNullable()
.references('id')
.inTable('actors');
table.integer('media_id', 16)
.notNullable()
.references('id')
.inTable('media');
table.unique('actor_id');
}))
.then(() => knex.schema.createTable('actors_photos', (table) => {
table.integer('actor_id', 12)
.notNullable()
.references('id')
.inTable('actors');
table.integer('media_id', 16)
.notNullable()
.references('id')
.inTable('media');
table.unique(['actor_id', 'media_id']);
}))
.then(() => knex.schema.createTable('actors_social', (table) => {
table.increments('id', 16);
table.string('url');
table.string('platform');
table.integer('actor_id', 8)
.notNullable()
.references('id')
.inTable('actors');
table.unique(['url', 'actor_id']);
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('directors', (table) => { .then(() => knex.schema.createTable('directors', (table) => {
table.increments('id', 12); table.increments('id', 12);
@ -304,6 +84,92 @@ exports.up = knex => Promise.resolve()
table.datetime('created_at') table.datetime('created_at')
.defaultTo(knex.fn.now()); .defaultTo(knex.fn.now());
})) }))
.then(() => knex.schema.createTable('tags_groups', (table) => {
table.increments('id', 12);
table.string('name', 32);
table.text('description');
table.string('slug', 32)
.unique();
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('tags', (table) => {
table.increments('id', 12);
table.string('name');
table.text('description');
table.integer('priority', 2)
.defaultTo(0);
table.integer('group_id', 12)
.references('id')
.inTable('tags_groups');
table.integer('alias_for', 12)
.references('id')
.inTable('tags');
table.string('slug', 32)
.unique();
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('networks', (table) => {
table.increments('id', 12);
table.string('name');
table.string('url');
table.text('description');
table.string('parameters');
table.string('slug', 32)
.unique();
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('sites', (table) => {
table.increments('id', 12);
table.integer('network_id', 12)
.notNullable()
.references('id')
.inTable('networks');
table.string('name');
table.string('url');
table.text('description');
table.string('parameters');
table.string('slug', 32)
.unique();
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('studios', (table) => {
table.increments('id', 12);
table.integer('network_id', 12)
.notNullable()
.references('id')
.inTable('networks');
table.string('name');
table.string('url');
table.text('description');
table.string('slug', 32)
.unique();
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('releases', (table) => { .then(() => knex.schema.createTable('releases', (table) => {
table.increments('id', 16); table.increments('id', 16);
@ -327,10 +193,14 @@ exports.up = knex => Promise.resolve()
table.date('date'); table.date('date');
table.text('description'); table.text('description');
table.integer('director', 12)
.references('id')
.inTable('directors');
table.integer('duration') table.integer('duration')
.unsigned(); .unsigned();
table.integer('parent_id', 16) table.integer('parent', 16)
.references('id') .references('id')
.inTable('releases'); .inTable('releases');
@ -339,7 +209,46 @@ exports.up = knex => Promise.resolve()
table.datetime('created_at') table.datetime('created_at')
.defaultTo(knex.fn.now()); .defaultTo(knex.fn.now());
})) }))
.then(() => knex.schema.createTable('releases_actors', (table) => { .then(() => knex.schema.createTable('media', (table) => {
table.increments('id', 16);
table.string('path');
table.string('thumbnail');
table.integer('index');
table.string('mime');
table.string('domain');
table.integer('target_id', 16);
table.string('role');
table.string('quality', 6);
table.string('hash');
table.text('comment');
table.string('source', 1000);
table.unique(['domain', 'target_id', 'role', 'hash']);
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('social', (table) => {
table.increments('id', 16);
table.string('url');
table.string('platform');
table.string('domain');
table.integer('target_id', 16);
table.unique(['url', 'domain', 'target_id']);
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('actors_associated', (table) => {
table.increments('id', 16);
table.integer('release_id', 16) table.integer('release_id', 16)
.notNullable() .notNullable()
.references('id') .references('id')
@ -352,7 +261,9 @@ exports.up = knex => Promise.resolve()
table.unique(['release_id', 'actor_id']); table.unique(['release_id', 'actor_id']);
})) }))
.then(() => knex.schema.createTable('releases_directors', (table) => { .then(() => knex.schema.createTable('directors_associated', (table) => {
table.increments('id', 16);
table.integer('release_id', 16) table.integer('release_id', 16)
.notNullable() .notNullable()
.references('id') .references('id')
@ -365,131 +276,30 @@ exports.up = knex => Promise.resolve()
table.unique(['release_id', 'director_id']); table.unique(['release_id', 'director_id']);
})) }))
.then(() => knex.schema.createTable('releases_posters', (table) => { .then(() => knex.schema.createTable('tags_associated', (table) => {
table.integer('release_id', 16)
.notNullable()
.references('id')
.inTable('releases');
table.integer('media_id', 16)
.notNullable()
.references('id')
.inTable('media');
table.unique('release_id');
}))
.then(() => knex.schema.createTable('releases_covers', (table) => {
table.integer('release_id', 16)
.notNullable()
.references('id')
.inTable('releases');
table.integer('media_id', 16)
.notNullable()
.references('id')
.inTable('media');
table.unique(['release_id', 'media_id']);
}))
.then(() => knex.schema.createTable('releases_trailers', (table) => {
table.integer('release_id', 16)
.notNullable()
.references('id')
.inTable('releases');
table.integer('media_id', 16)
.notNullable()
.references('id')
.inTable('media');
table.unique('release_id');
}))
.then(() => knex.schema.createTable('releases_photos', (table) => {
table.integer('release_id', 16)
.notNullable()
.references('id')
.inTable('releases');
table.integer('media_id', 16)
.notNullable()
.references('id')
.inTable('media');
table.unique(['release_id', 'media_id']);
}))
.then(() => knex.schema.createTable('releases_tags', (table) => {
table.integer('tag_id', 12) table.integer('tag_id', 12)
.notNullable() .notNullable()
.references('id') .references('id')
.inTable('tags'); .inTable('tags');
table.integer('release_id', 16) table.string('domain');
.notNullable() table.integer('target_id', 16);
.references('id')
.inTable('releases');
table.unique(['tag_id', 'release_id']); table.unique(['domain', 'tag_id', 'target_id']);
})) }));
.then(() => knex.raw(`
COMMENT ON COLUMN actors.height IS E'@omit read,update,create,delete,all,many';
COMMENT ON COLUMN actors.weight IS E'@omit read,update,create,delete,all,many';
/* exports.down = knex => Promise.resolve()
CREATE VIEW releases_actors_sortable AS .then(() => knex.schema.dropTable('tags_associated'))
SELECT releases_actors.*, actors.gender, actors.name, actors.birthdate FROM releases_actors .then(() => knex.schema.dropTable('directors_associated'))
JOIN actors ON releases_actors.actor_id = actors.id; .then(() => knex.schema.dropTable('actors_associated'))
.then(() => knex.schema.dropTable('tags'))
CREATE VIEW releases_tags_sortable AS .then(() => knex.schema.dropTable('tags_groups'))
SELECT releases_tags.*, tags.name, tags.priority FROM releases_tags .then(() => knex.schema.dropTable('media'))
JOIN tags ON releases_tags.tag_id = tags.id; .then(() => knex.schema.dropTable('social'))
.then(() => knex.schema.dropTable('actors'))
CREATE VIEW actors_releases_sortable AS .then(() => knex.schema.dropTable('releases'))
SELECT releases_actors.*, releases.date FROM releases_actors .then(() => knex.schema.dropTable('sites'))
JOIN releases ON releases_actors.release_id = releases.id; .then(() => knex.schema.dropTable('studios'))
.then(() => knex.schema.dropTable('directors'))
COMMENT ON VIEW releases_actors_sortable IS E'@foreignKey (release_id) references releases (id)\n@foreignKey (actor_id) references actors (id)'; .then(() => knex.schema.dropTable('networks'))
COMMENT ON VIEW releases_tags_sortable IS E'@foreignKey (release_id) references releases (id)\n@foreignKey (tag_id) references tags (id)'; .then(() => knex.schema.dropTable('countries'));
COMMENT ON VIEW actors_releases_sortable IS E'@foreignKey (release_id) references releases (id)\n@foreignKey (actor_id) references actors (id)';
/* allow conversion resolver to be added for height and weight */
CREATE FUNCTION releases_by_tag_slugs(slugs text[]) RETURNS setof releases AS $$
SELECT DISTINCT ON (releases.id) releases.* FROM releases
JOIN releases_tags ON (releases_tags.release_id = releases.id)
JOIN tags ON (releases_tags.tag_id = tags.id)
WHERE tags.slug = ANY($1);
$$ LANGUAGE sql STABLE
*/
`));
exports.down = knex => knex.raw(`
DROP FUNCTION IF EXISTS releases_by_tag_slugs;
DROP VIEW IF EXISTS releases_actors_view;
DROP TABLE IF EXISTS releases_actors CASCADE;
DROP TABLE IF EXISTS releases_directors CASCADE;
DROP TABLE IF EXISTS releases_posters CASCADE;
DROP TABLE IF EXISTS releases_photos CASCADE;
DROP TABLE IF EXISTS releases_covers CASCADE;
DROP TABLE IF EXISTS releases_trailers CASCADE;
DROP TABLE IF EXISTS releases_tags CASCADE;
DROP TABLE IF EXISTS actors_avatars CASCADE;
DROP TABLE IF EXISTS actors_photos CASCADE;
DROP TABLE IF EXISTS actors_social CASCADE;
DROP TABLE IF EXISTS sites_tags CASCADE;
DROP TABLE IF EXISTS sites_social CASCADE;
DROP TABLE IF EXISTS networks_social CASCADE;
DROP TABLE IF EXISTS tags_posters CASCADE;
DROP TABLE IF EXISTS tags_photos CASCADE;
DROP TABLE IF EXISTS releases CASCADE;
DROP TABLE IF EXISTS actors CASCADE;
DROP TABLE IF EXISTS directors CASCADE;
DROP TABLE IF EXISTS tags CASCADE;
DROP TABLE IF EXISTS tags_groups CASCADE;
DROP TABLE IF EXISTS social CASCADE;
DROP TABLE IF EXISTS sites CASCADE;
DROP TABLE IF EXISTS studios CASCADE;
DROP TABLE IF EXISTS media CASCADE;
DROP TABLE IF EXISTS countries CASCADE;
DROP TABLE IF EXISTS networks CASCADE;
`);

1248
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "traxxx", "name": "traxxx",
"version": "1.44.0", "version": "1.43.1",
"description": "All the latest porn releases in one place", "description": "All the latest porn releases in one place",
"main": "src/app.js", "main": "src/app.js",
"scripts": { "scripts": {
@ -39,11 +39,12 @@
"@babel/core": "^7.7.5", "@babel/core": "^7.7.5",
"@babel/plugin-proposal-optional-chaining": "^7.7.5", "@babel/plugin-proposal-optional-chaining": "^7.7.5",
"@babel/preset-env": "^7.7.6", "@babel/preset-env": "^7.7.6",
"@babel/register": "^7.7.4",
"autoprefixer": "^9.7.3", "autoprefixer": "^9.7.3",
"babel-cli": "^6.26.0",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"babel-preset-airbnb": "^3.3.2", "babel-preset-airbnb": "^3.3.2",
"babel-register": "^6.26.0",
"css-loader": "^2.1.1", "css-loader": "^2.1.1",
"eslint": "^5.16.0", "eslint": "^5.16.0",
"eslint-config-airbnb": "^17.1.1", "eslint-config-airbnb": "^17.1.1",
@ -66,8 +67,6 @@
"webpack-cli": "^3.3.10" "webpack-cli": "^3.3.10"
}, },
"dependencies": { "dependencies": {
"@graphile-contrib/pg-order-by-related": "^1.0.0-beta.6",
"@graphile-contrib/pg-simplify-inflector": "^5.0.0-beta.1",
"@tensorflow/tfjs-node": "^1.4.0", "@tensorflow/tfjs-node": "^1.4.0",
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"bhttp": "^1.2.4", "bhttp": "^1.2.4",
@ -84,7 +83,6 @@
"express-react-views": "^0.11.0", "express-react-views": "^0.11.0",
"face-api.js": "^0.21.0", "face-api.js": "^0.21.0",
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
"graphile-utils": "^4.5.6",
"jsdom": "^15.2.1", "jsdom": "^15.2.1",
"knex": "^0.16.5", "knex": "^0.16.5",
"knex-migrate": "^1.7.4", "knex-migrate": "^1.7.4",
@ -92,8 +90,6 @@
"moment": "^2.24.0", "moment": "^2.24.0",
"opn": "^5.5.0", "opn": "^5.5.0",
"pg": "^7.14.0", "pg": "^7.14.0",
"postgraphile": "^4.5.5",
"postgraphile-plugin-connection-filter": "^1.1.3",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",

View File

@ -188,21 +188,14 @@
text-decoration: none; text-decoration: none;
} }
.title[data-v-3abcf101] { .title[data-v-3abcf101] {
display: -webkit-box;
display: flex;
-webkit-box-align: center;
align-items: center;
margin: 0 .25rem .25rem 0;
color: #222; color: #222;
margin: 0 .25rem .25rem 0;
font-size: 1rem; font-size: 1rem;
max-height: 3rem; max-height: 3rem;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
} }
.title .icon[data-v-3abcf101] {
margin: 0 .25rem 0 0;
}
.network[data-v-3abcf101] { .network[data-v-3abcf101] {
color: #555; color: #555;
margin: 0 .25rem 0 0; margin: 0 .25rem 0 0;
@ -215,8 +208,9 @@
line-height: 1.5rem; line-height: 1.5rem;
} }
.tags[data-v-3abcf101] { .tags[data-v-3abcf101] {
max-height: .5rem; max-height: 2.5rem;
padding: .25rem .5rem 1rem .5rem; padding: .25rem .5rem 1rem .5rem;
line-height: 1.5rem;
word-wrap: break-word; word-wrap: break-word;
overflow-y: hidden; overflow-y: hidden;
} }
@ -261,7 +255,6 @@
text-transform: capitalize; text-transform: capitalize;
} }
.tiles[data-v-22ffe3e4] { .tiles[data-v-22ffe3e4] {
width: 100%;
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 0.33fr)); grid-template-columns: repeat(auto-fit, minmax(20rem, 0.33fr));
grid-gap: 1rem; grid-gap: 1rem;
@ -277,31 +270,6 @@
} }
} }
/* $primary: #ff886c; */
.banner[data-v-42bb19c4] {
background: #222;
flex-shrink: 0;
white-space: nowrap;
overflow-x: auto;
scrollbar-width: none;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
font-size: 0;
}
.banner[data-v-42bb19c4]::-webkit-scrollbar {
display: none;
}
.trailer[data-v-42bb19c4] {
display: inline-block;
max-width: 100vw;
}
.trailer-video[data-v-42bb19c4] {
max-width: 100%;
}
.item[data-v-42bb19c4] {
height: 18rem;
vertical-align: middle;
}
/* $primary: #ff886c; */ /* $primary: #ff886c; */
.actor[data-v-6989dc6f] { .actor[data-v-6989dc6f] {
width: 10rem; width: 10rem;
@ -349,8 +317,6 @@
width: 100%; width: 100%;
display: -webkit-box; display: -webkit-box;
display: flex; display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: justify; -webkit-box-pack: justify;
justify-content: space-between; justify-content: space-between;
box-sizing: border-box; box-sizing: border-box;
@ -364,6 +330,31 @@
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
} }
/* $primary: #ff886c; */
.banner[data-v-42bb19c4] {
background: #222;
flex-shrink: 0;
white-space: nowrap;
overflow-x: auto;
scrollbar-width: none;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
font-size: 0;
}
.banner[data-v-42bb19c4]::-webkit-scrollbar {
display: none;
}
.trailer[data-v-42bb19c4] {
display: inline-block;
max-width: 100vw;
}
.trailer-video[data-v-42bb19c4] {
max-width: 100%;
}
.item[data-v-42bb19c4] {
height: 18rem;
vertical-align: middle;
}
/* $primary: #ff886c; */ /* $primary: #ff886c; */
.column[data-v-d4b03dc2] { .column[data-v-d4b03dc2] {
width: 1200px; width: 1200px;
@ -519,28 +510,28 @@
} }
/* $primary: #ff886c; */ /* $primary: #ff886c; */
.header[data-v-194630f6] { .header[data-v-3e57cf44] {
display: -webkit-box; display: -webkit-box;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
-webkit-box-pack: justify; -webkit-box-pack: justify;
justify-content: space-between; justify-content: space-between;
} }
.title[data-v-194630f6] { .title[data-v-3e57cf44] {
display: -webkit-inline-box; display: -webkit-inline-box;
display: inline-flex; display: inline-flex;
-webkit-box-align: top; -webkit-box-align: top;
align-items: top; align-items: top;
margin: 0 1rem 0 0; margin: 0 1rem 0 0;
} }
.title:hover .icon[data-v-194630f6] { .title:hover .icon[data-v-3e57cf44] {
fill: #ff6c88; fill: #ff6c88;
} }
.heading[data-v-194630f6] { .heading[data-v-3e57cf44] {
padding: 0; padding: 0;
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
} }
.link[data-v-194630f6] { .link[data-v-3e57cf44] {
display: -webkit-box; display: -webkit-box;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
@ -550,20 +541,20 @@
-webkit-box-align: end; -webkit-box-align: end;
align-items: flex-end; align-items: flex-end;
} }
.logo[data-v-194630f6] { .logo[data-v-3e57cf44] {
width: 20rem; width: 20rem;
max-height: 8rem; max-height: 8rem;
-o-object-fit: contain; -o-object-fit: contain;
object-fit: contain; object-fit: contain;
margin: 0 .5rem 1rem 0; margin: 0 .5rem 1rem 0;
} }
.networklogo-container[data-v-194630f6] { .networklogo-container[data-v-3e57cf44] {
display: -webkit-box; display: -webkit-box;
display: flex; display: flex;
-webkit-box-align: center; -webkit-box-align: center;
align-items: center; align-items: center;
} }
.networklogo[data-v-194630f6] { .networklogo[data-v-3e57cf44] {
color: #222; color: #222;
width: 15rem; width: 15rem;
max-height: 6rem; max-height: 6rem;
@ -574,13 +565,13 @@
object-position: 100% 0; object-position: 100% 0;
margin: 0 0 0 .5rem; margin: 0 0 0 .5rem;
} }
.sites[data-v-194630f6], .sites[data-v-3e57cf44],
.scenes[data-v-194630f6] { .scenes[data-v-3e57cf44] {
display: grid; display: grid;
grid-gap: 1rem; grid-gap: 1rem;
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
} }
.sites[data-v-194630f6] { .sites[data-v-3e57cf44] {
grid-template-columns: repeat(auto-fit, 15rem); grid-template-columns: repeat(auto-fit, 15rem);
} }
@ -632,111 +623,44 @@
} }
/* $primary: #ff886c; */ /* $primary: #ff886c; */
.sites[data-v-7bebaa3e] { .header[data-v-e2e12602] {
display: grid;
grid-gap: 1rem;
padding: 1rem;
margin: 0;
grid-template-columns: 1fr;
overflow-y: auto;
}
.sites.compact[data-v-7bebaa3e] {
display: -webkit-box;
display: flex;
overflow-x: auto;
}
.sites.compact .tile[data-v-7bebaa3e] {
width: 15rem;
margin: 0 1rem 0 0;
}
/* $primary: #ff886c; */
@media (max-width: 1200px) {
.releases .tiles {
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
}
}
/* $primary: #ff886c; */
.network[data-v-e2e12602] {
display: -webkit-box; display: -webkit-box;
display: flex; display: flex;
-webkit-box-orient: horizontal; flex-wrap: wrap;
-webkit-box-direction: normal; -webkit-box-pack: justify;
flex-direction: row; justify-content: space-between;
-webkit-box-flex: 1; -webkit-box-align: top;
flex-grow: 1; align-items: top;
-webkit-box-pack: stretch; margin: 0 0 2rem 0;
justify-content: stretch;
overflow-y: auto;
} }
.sidebar[data-v-e2e12602] { .title[data-v-e2e12602] {
height: 100%; display: -webkit-inline-box;
width: 18rem; display: inline-flex;
display: -webkit-box; -webkit-box-align: top;
display: flex; align-items: top;
-webkit-box-orient: vertical; margin: 0 1rem 0 0;
-webkit-box-direction: normal;
flex-direction: column;
flex-shrink: 0;
color: #fff;
border-right: solid 1px rgba(0, 0, 0, 0.1);
overflow: hidden;
} }
.sidebar .title[data-v-e2e12602] { .title:hover .icon[data-v-e2e12602] {
border-bottom: solid 1px rgba(0, 0, 0, 0.1); fill: #ff6c88;
} }
.logo[data-v-e2e12602] { .logo[data-v-e2e12602] {
width: 100%; width: 20rem;
max-height: 8rem; max-height: 8rem;
display: -webkit-box;
display: flex;
-webkit-box-pack: center;
justify-content: center;
-o-object-fit: contain; -o-object-fit: contain;
object-fit: contain; object-fit: contain;
box-sizing: border-box; margin: 0 .5rem 0 0;
padding: 1rem;
margin: 0;
-webkit-filter: drop-shadow(1px 0 0 rgba(0, 0, 0, 0.2)) drop-shadow(-1px 0 0 rgba(0, 0, 0, 0.2)) drop-shadow(0 1px 0 rgba(0, 0, 0, 0.2)) drop-shadow(0 -1px 0 rgba(0, 0, 0, 0.2));
filter: drop-shadow(1px 0 0 rgba(0, 0, 0, 0.2)) drop-shadow(-1px 0 0 rgba(0, 0, 0, 0.2)) drop-shadow(0 1px 0 rgba(0, 0, 0, 0.2)) drop-shadow(0 -1px 0 rgba(0, 0, 0, 0.2));
} }
.sites.compact[data-v-e2e12602] { .sites[data-v-e2e12602] {
display: none; display: grid;
grid-gap: 1rem;
margin: 0 0 2rem 0;
} }
.header[data-v-e2e12602] { .sites[data-v-e2e12602] {
width: 100%; grid-template-columns: repeat(auto-fit, 15rem);
height: 4rem;
display: none;
-webkit-box-pack: center;
justify-content: center;
border-bottom: solid 1px rgba(0, 0, 0, 0.1);
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.header[data-v-e2e12602] { .sites[data-v-e2e12602] {
display: -webkit-box; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
display: flex;
}
.sites.compact[data-v-e2e12602] {
display: -webkit-box;
display: flex;
padding: 0 1rem 1rem 1rem;
}
.network[data-v-e2e12602] {
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
flex-direction: column;
}
.logo[data-v-e2e12602] {
max-width: 20rem;
height: 100%;
padding: .5rem;
}
.sidebar[data-v-e2e12602] {
display: none;
height: auto;
width: 100%;
overflow: hidden;
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 845 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

BIN
public/img/tags/anal/poster.jpeg Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 KiB

After

Width:  |  Height:  |  Size: 107 KiB

BIN
public/img/tags/anal/poster_thumb.jpeg Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 800 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

BIN
public/img/tags/double-penetration/poster.jpeg Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 782 KiB

After

Width:  |  Height:  |  Size: 142 KiB

BIN
public/img/tags/double-penetration/poster_thumb.jpeg Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

BIN
public/img/tags/gangbang/poster.jpeg Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 115 KiB

BIN
public/img/tags/gangbang/poster_thumb.jpeg Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

View File

@ -125,4 +125,10 @@ const networks = [
]; ];
exports.seed = knex => Promise.resolve() exports.seed = knex => Promise.resolve()
.then(async () => upsert('networks', networks, 'slug', knex)); .then(async () => {
// find network IDs
const duplicates = await knex('networks').select('*');
const duplicatesBySlug = duplicates.reduce((acc, network) => ({ ...acc, [network.slug]: network }), {});
return upsert('networks', networks, duplicatesBySlug, 'slug', knex);
});

View File

@ -2428,10 +2428,15 @@ function getSites(networksMap) {
/* eslint-disable max-len */ /* eslint-disable max-len */
exports.seed = knex => Promise.resolve() exports.seed = knex => Promise.resolve()
.then(async () => { .then(async () => {
const networks = await knex('networks').select('*'); const [duplicates, networks] = await Promise.all([
knex('sites').select('*'),
knex('networks').select('*'),
]);
const duplicatesBySlug = duplicates.reduce((acc, site) => ({ ...acc, [site.slug]: site }), {});
const networksMap = networks.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {}); const networksMap = networks.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
const sites = getSites(networksMap); const sites = getSites(networksMap);
return upsert('sites', sites, 'slug', knex); return upsert('sites', sites, duplicatesBySlug, 'slug', knex);
}); });

View File

@ -1,3 +1,5 @@
'use strict';
const upsert = require('../src/utils/upsert'); const upsert = require('../src/utils/upsert');
function getStudios(networksMap) { function getStudios(networksMap) {
@ -7,133 +9,133 @@ function getStudios(networksMap) {
slug: 'gonzocom', slug: 'gonzocom',
name: 'Gonzo.com', name: 'Gonzo.com',
url: 'https://www.legalporno.com/studios/gonzo_com', url: 'https://www.legalporno.com/studios/gonzo_com',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'giorgiograndi', slug: 'giorgiograndi',
name: 'Giorgio Grandi', name: 'Giorgio Grandi',
url: 'https://www.legalporno.com/studios/giorgio-grandi', url: 'https://www.legalporno.com/studios/giorgio-grandi',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'hardpornworld', slug: 'hardpornworld',
name: 'Hard Porn World', name: 'Hard Porn World',
url: 'https://www.legalporno.com/studios/hard-porn-world', url: 'https://www.legalporno.com/studios/hard-porn-world',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'interracialvision', slug: 'interracialvision',
name: 'Interracial Vision', name: 'Interracial Vision',
url: 'https://www.legalporno.com/studios/interracial-vision', url: 'https://www.legalporno.com/studios/interracial-vision',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'giorgioslab', slug: 'giorgioslab',
name: 'Giorgio\'s Lab', name: 'Giorgio\'s Lab',
url: 'https://www.legalporno.com/studios/giorgio--s-lab', url: 'https://www.legalporno.com/studios/giorgio--s-lab',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'americananal', slug: 'americananal',
name: 'American Anal', name: 'American Anal',
url: 'https://www.legalporno.com/studios/american-anal', url: 'https://www.legalporno.com/studios/american-anal',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'assablanca', slug: 'assablanca',
name: 'Assablanca', name: 'Assablanca',
url: 'https://www.legalporno.com/studios/assablanca', url: 'https://www.legalporno.com/studios/assablanca',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'focus', slug: 'focus',
name: 'Focus', name: 'Focus',
url: 'https://www.legalporno.com/studios/focus', url: 'https://www.legalporno.com/studios/focus',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'analforever', slug: 'analforever',
name: 'Anal Forever', name: 'Anal Forever',
url: 'https://www.legalporno.com/studios/anal-forever', url: 'https://www.legalporno.com/studios/anal-forever',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'gonzoinbrazil', slug: 'gonzoinbrazil',
name: 'Gonzo in Brazil', name: 'Gonzo in Brazil',
url: 'https://www.legalporno.com/studios/gonzo-in-brazil', url: 'https://www.legalporno.com/studios/gonzo-in-brazil',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'mranal', slug: 'mranal',
name: 'Mr Anal', name: 'Mr Anal',
url: 'https://www.legalporno.com/studios/mr-anal', url: 'https://www.legalporno.com/studios/mr-anal',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'tarrawhite', slug: 'tarrawhite',
name: 'Tarra White', name: 'Tarra White',
url: 'https://www.legalporno.com/studios/tarra-white', url: 'https://www.legalporno.com/studios/tarra-white',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'sineplexsos', slug: 'sineplexsos',
name: 'Sineplex SOS', name: 'Sineplex SOS',
url: 'https://www.legalporno.com/studios/sineplex-sos', url: 'https://www.legalporno.com/studios/sineplex-sos',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'fmodels', slug: 'fmodels',
name: 'F Models', name: 'F Models',
url: 'https://www.legalporno.com/studios/f-models', url: 'https://www.legalporno.com/studios/f-models',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'sineplexcz', slug: 'sineplexcz',
name: 'Sineplex CZ', name: 'Sineplex CZ',
url: 'https://www.legalporno.com/studios/sineplex-cz', url: 'https://www.legalporno.com/studios/sineplex-cz',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'gg', slug: 'gg',
name: 'GG', name: 'GG',
url: 'https://www.legalporno.com/studios/gg', url: 'https://www.legalporno.com/studios/gg',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'firstgape', slug: 'firstgape',
name: 'First Gape', name: 'First Gape',
url: 'https://www.legalporno.com/studios/first-gape', url: 'https://www.legalporno.com/studios/first-gape',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'omargalantiproductions', slug: 'omargalantiproductions',
name: 'Omar Galanti Productions', name: 'Omar Galanti Productions',
url: 'https://www.legalporno.com/studios/omar-galanti-productions', url: 'https://www.legalporno.com/studios/omar-galanti-productions',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'norestfortheass', slug: 'norestfortheass',
name: 'No Rest For The Ass', name: 'No Rest For The Ass',
url: 'https://www.legalporno.com/studios/no-rest-for-the-ass', url: 'https://www.legalporno.com/studios/no-rest-for-the-ass',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'hairygonzo', slug: 'hairygonzo',
name: 'Hairy Gonzo', name: 'Hairy Gonzo',
url: 'https://www.legalporno.com/studios/hairy-gonzo', url: 'https://www.legalporno.com/studios/hairy-gonzo',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'sineplexclassic', slug: 'sineplexclassic',
name: 'Sineplex Classic', name: 'Sineplex Classic',
url: 'https://www.legalporno.com/studios/sineplex-classic', url: 'https://www.legalporno.com/studios/sineplex-classic',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
{ {
slug: 'sinemale', slug: 'sinemale',
name: 'Sinemale', name: 'Sinemale',
url: 'https://www.legalporno.com/studios/sinemale', url: 'https://www.legalporno.com/studios/sinemale',
network_id: networksMap.legalporno, network_id: networksMap['legalporno'],
}, },
]; ];
} }
@ -141,10 +143,15 @@ function getStudios(networksMap) {
/* eslint-disable max-len */ /* eslint-disable max-len */
exports.seed = knex => Promise.resolve() exports.seed = knex => Promise.resolve()
.then(async () => { .then(async () => {
const networks = await knex('networks').select('*'); const [duplicates, networks] = await Promise.all([
knex('studios').select('*'),
knex('networks').select('*'),
]);
const duplicatesBySlug = duplicates.reduce((acc, studio) => ({ ...acc, [studio.slug]: studio }), {});
const networksMap = networks.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {}); const networksMap = networks.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
const studios = getStudios(networksMap); const studios = getStudios(networksMap);
return upsert('studios', studios, 'slug', knex); return upsert('studios', studios, duplicatesBySlug, 'slug', knex);
}); });

View File

@ -33,10 +33,6 @@ const groups = [
slug: 'location', slug: 'location',
name: 'Location', name: 'Location',
}, },
{
slug: 'oral',
name: 'Oral',
},
{ {
slug: 'orientation', slug: 'orientation',
name: 'Orientation', name: 'Orientation',
@ -73,7 +69,7 @@ function getTags(groupsMap) {
name: 'airtight', name: 'airtight',
slug: 'airtight', slug: 'airtight',
alias_for: null, alias_for: null,
description: 'Stuffing one cock in her ass, one in her pussy, and one in her mouth, filling all of her penetrable holes and sealing her airtight like a figurative balloon. In other words, simultaneously getting [double penetrated](/tag/double-penetration), and giving a [blowjob](/tag/blowjob) or getting [facefucked](/tag/facefuck). Being airtight implies being [gangbanged](/tag/gangbang).', /* eslint-disable-line max-len */ description: 'Stuffing one cock in her ass, one in her pussy, and one in her mouth, filling all of her penetrable holes and sealing her airtight like a figurative balloon. In other words, simultaneously getting [double penetrated](/tag/double-penetration), and giving a [blowjob](/tag/blowjob) or getting [facefucked](/tag/facefuck). Being airtight implies being [gangbanged](/tag/gangbang).',
priority: 9, priority: 9,
group_id: groupsMap.penetration, group_id: groupsMap.penetration,
}, },
@ -140,19 +136,16 @@ function getTags(groupsMap) {
priority: 6, priority: 6,
description: 'Sucking off a cock right after anal, giving your own or someone else`s asshole a second hand taste.', description: 'Sucking off a cock right after anal, giving your own or someone else`s asshole a second hand taste.',
alias_for: null, alias_for: null,
group_id: groupsMap.oral,
}, },
{ {
name: 'ass eating', name: 'ass eating',
slug: 'ass-eating', slug: 'ass-eating',
alias_for: null, alias_for: null,
group_id: groupsMap.oral,
}, },
{ {
name: 'ball licking', name: 'ball licking',
slug: 'ball-licking', slug: 'ball-licking',
alias_for: null, alias_for: null,
group_id: groupsMap.oral,
}, },
{ {
name: 'ballerina', name: 'ballerina',
@ -218,7 +211,6 @@ function getTags(groupsMap) {
slug: 'blowjob', slug: 'blowjob',
priority: 7, priority: 7,
alias_for: null, alias_for: null,
group_id: groupsMap.oral,
}, },
{ {
name: 'blowbang', name: 'blowbang',
@ -327,7 +319,6 @@ function getTags(groupsMap) {
slug: 'deepthroat', slug: 'deepthroat',
priority: 7, priority: 7,
alias_for: null, alias_for: null,
group_id: groupsMap.oral,
}, },
{ {
name: 'double penetration', name: 'double penetration',
@ -354,13 +345,11 @@ function getTags(groupsMap) {
name: 'double blowjob', name: 'double blowjob',
slug: 'double-blowjob', slug: 'double-blowjob',
alias_for: null, alias_for: null,
group_id: groupsMap.oral,
}, },
{ {
name: 'doggy style', name: 'doggy style',
slug: 'doggy-style', slug: 'doggy-style',
alias_for: null, alias_for: null,
group_id: groupsMap.position,
}, },
{ {
name: 'dress', name: 'dress',
@ -390,7 +379,7 @@ function getTags(groupsMap) {
slug: 'facefuck', slug: 'facefuck',
priority: 9, priority: 9,
alias_for: null, alias_for: null,
group_id: groupsMap.oral, group_id: groupsMap.position,
}, },
{ {
name: 'facesitting', name: 'facesitting',
@ -440,7 +429,7 @@ function getTags(groupsMap) {
{ {
name: 'gangbang', name: 'gangbang',
slug: 'gangbang', slug: 'gangbang',
description: 'A group of three or more guys fucking a woman, at least two at the same time, often but not necessarily involving a [blowbang](/tag/blowbang), [double penetration](/tag/airtight) and [airtight](/tag/airtight). If she only gets fucked by one guy at a time, it might be considered a [trainbang](/tag/trainbang) instead. In a reverse gangbang, multiple women fuck one man.', /* eslint-disable-line max-len */ description: 'A group of three or more guys fucking a woman, at least two at the same time, often but not necessarily involving a [blowbang](/tag/blowbang), [double penetration](/tag/airtight) and [airtight](/tag/airtight). If she only gets fucked by one guy at a time, it might be considered a [trainbang](/tag/trainbang) instead. In a reverse gangbang, multiple women fuck one man.',
alias_for: null, alias_for: null,
priority: 9, priority: 9,
group_id: groupsMap.group, group_id: groupsMap.group,
@ -656,7 +645,6 @@ function getTags(groupsMap) {
name: 'pussy eating', name: 'pussy eating',
slug: 'pussy-eating', slug: 'pussy-eating',
alias_for: null, alias_for: null,
group_id: groupsMap.oral,
}, },
{ {
name: 'redhead', name: 'redhead',
@ -1554,20 +1542,35 @@ function getTagAliases(tagsMap) {
} }
exports.seed = knex => Promise.resolve() exports.seed = knex => Promise.resolve()
.then(async () => upsert('tags_groups', groups, 'slug', knex))
.then(async () => { .then(async () => {
const groupEntries = await knex('tags_groups').select('*'); const duplicates = await knex('tags_groups').select('*');
const duplicatesBySlug = duplicates.reduce((acc, group) => ({ ...acc, [group.slug]: group }), {});
return upsert('tags_groups', groups, duplicatesBySlug, 'slug', knex);
})
.then(async () => {
const [duplicates, groupEntries] = await Promise.all([
knex('tags').select('*'),
knex('tags_groups').select('*'),
]);
const duplicatesBySlug = duplicates.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag }), {});
const groupsMap = groupEntries.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {}); const groupsMap = groupEntries.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
const tags = getTags(groupsMap); const tags = getTags(groupsMap);
return upsert('tags', tags, 'slug', knex); return upsert('tags', tags, duplicatesBySlug, 'slug', knex);
}) })
.then(async () => { .then(async () => {
const tags = await knex('tags').select('*').where({ alias_for: null }); const [duplicates, tags] = await Promise.all([
knex('tags').select('*').whereNotNull('alias_for'),
knex('tags').select('*').where({ alias_for: null }),
]);
const duplicatesByName = duplicates.reduce((acc, tag) => ({ ...acc, [tag.name]: tag }), {});
const tagsMap = tags.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {}); const tagsMap = tags.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
const tagAliases = getTagAliases(tagsMap); const tagAliases = getTagAliases(tagsMap);
return upsert('tags', tagAliases, 'name', knex); return upsert('tags', tagAliases, duplicatesByName, 'name', knex);
}); });

View File

@ -1,296 +1,256 @@
const upsert = require('../src/utils/upsert'); const upsert = require('../src/utils/upsert');
const tagPosters = [ function getMedia(tagsMap) {
{ return [
path: 'tags/airtight/poster.jpeg', {
tagSlug: 'airtight', path: 'tags/airtight/poster.jpeg',
comment: 'Jynx Maze in "Pump My Ass Full of Cum 3" for Jules Jordan', target_id: tagsMap.airtight,
}, role: 'poster',
{ comment: 'Jynx Maze in "Pump My Ass Full of Cum 3" for Jules Jordan',
path: 'tags/anal/poster.jpeg', },
tagSlug: 'anal', {
comment: 'Jynx Maze in "Anal Buffet 6" for Evil Angel', path: 'tags/airtight/2.jpeg',
}, target_id: tagsMap.airtight,
{ comment: 'Dakota Skye in "Dakota Goes Nuts" for ArchAngel',
path: 'tags/ass-to-mouth/poster.jpeg', },
tagSlug: 'ass-to-mouth', {
comment: 'Alysa Gap and Logan in "Anal Buffet 4" for Evil Angel', path: 'tags/airtight/1.jpeg',
}, target_id: tagsMap.airtight,
{ comment: 'Chloe Amour in "DP Masters 4" for Jules Jordan',
path: 'tags/gapes/poster.jpeg', },
tagSlug: 'gapes', {
comment: 'Paulina in "Anal Buffet 4" for Evil Angel', path: 'tags/airtight/0/poster.jpeg',
}, domain: 'tags',
{ target_id: tagsMap.airtight,
path: 'tags/da-tp/0.jpeg', comment: 'Sheena Shaw in "Ass Worship 14" for Jules Jordan',
tagSlug: 'da-tp', },
comment: 'Natasha Teen in LegalPorno SZ2164', {
}, path: 'tags/anal/poster.jpeg',
{ target_id: tagsMap.anal,
path: 'tags/double-penetration/poster.jpeg', role: 'poster',
tagSlug: 'double-penetration', comment: '',
comment: 'Mia Malkova in "DP!" for HardX', },
}, {
{ path: 'tags/double-penetration/poster.jpeg',
path: 'tags/double-anal/poster.jpeg', target_id: tagsMap['double-penetration'],
tagSlug: 'double-anal', role: 'poster',
comment: 'Haley Reed in "Young Hot Ass" for Evil Angel', comment: '',
}, },
{ {
path: 'tags/double-vaginal/poster.jpeg', path: 'tags/double-anal/poster.jpeg',
tagSlug: 'double-vaginal', target_id: tagsMap['double-anal'],
comment: '', role: 'poster',
}, comment: '',
{ },
path: 'tags/dv-tp/poster.jpeg', {
tagSlug: 'dv-tp', path: 'tags/double-vaginal/poster.jpeg',
comment: 'Juelz Ventura in "Gangbanged 5" for Elegant Angel', target_id: tagsMap['double-vaginal'],
}, role: 'poster',
{ comment: '',
path: 'tags/tattoo/poster.jpeg', },
tagSlug: 'tattoo', {
comment: 'Kali Roses in "Goes All In For Anal" for Hussie Pass', path: 'tags/da-tp/0.jpeg',
}, target_id: tagsMap['da-tp'],
{ role: 'poster',
path: 'tags/triple-anal/poster.jpeg', comment: 'Natasha Teen in LegalPorno SZ2164',
tagSlug: 'triple-anal', },
comment: 'Kristy Black in SZ1986 for LegalPorno', {
}, path: 'tags/da-tp/3.jpeg',
{ target_id: tagsMap['da-tp'],
path: 'tags/blowbang/poster.jpeg', role: 'photo',
tagSlug: 'blowbang', comment: 'Evelina Darling in GIO294',
comment: '', },
}, {
{ path: 'tags/da-tp/1.jpeg',
path: 'tags/gangbang/poster.jpeg', target_id: tagsMap['da-tp'],
tagSlug: 'gangbang', role: 'photo',
comment: 'Kristen Scott in "Interracial Gangbang!" for Jules Jordan', comment: 'Francys Belle in SZ1702 for LegalPorno',
}, },
{ {
path: 'tags/mff/poster.jpeg', path: 'tags/da-tp/2.jpeg',
tagSlug: 'mff', target_id: tagsMap['da-tp'],
comment: '', role: 'photo',
}, comment: 'Angel Smalls in GIO408 for LegalPorno',
{ },
path: 'tags/mfm/poster.jpeg', {
tagSlug: 'mfm', path: 'tags/da-tp/4.jpeg',
comment: '', target_id: tagsMap['da-tp'],
}, role: 'photo',
{ comment: 'Ninel Mojado aka Mira Cuckold in GIO063 for LegalPorno',
path: 'tags/orgy/poster.jpeg', },
tagSlug: 'orgy', {
comment: '', path: 'tags/dv-tp/poster.jpeg',
}, target_id: tagsMap['dv-tp'],
{ role: 'poster',
path: 'tags/asian/poster.jpeg', comment: 'Juelz Ventura in "Gangbanged 5" for Elegant Angel',
tagSlug: 'asian', },
comment: 'Vina Sky in "Young and Glamorous 10" for Jules Jordan', {
}, path: 'tags/dv-tp/0.jpeg',
{ target_id: tagsMap['dv-tp'],
path: 'tags/caucasian/poster.jpeg', role: 'photo',
tagSlug: 'caucasian', comment: 'Luna Rival in LegalPorno SZ1490',
comment: '', },
}, {
{ path: 'tags/tattoo/poster.jpeg',
path: 'tags/ebony/poster.jpeg', target_id: tagsMap.tattoo,
tagSlug: 'ebony', role: 'poster',
comment: '', comment: 'Kali Roses in "Goes All In For Anal" for Hussie Pass',
}, },
{ {
path: 'tags/latina/poster.jpeg', path: 'tags/triple-anal/poster.jpeg',
tagSlug: 'latina', target_id: tagsMap['triple-anal'],
comment: '', role: 'poster',
}, comment: 'Kristy Black in SZ1986 for LegalPorno',
{ },
path: 'tags/interracial/poster.jpeg', {
tagSlug: 'interracial', path: 'tags/triple-anal/1.jpeg',
comment: '', target_id: tagsMap['triple-anal'],
}, role: 'photo',
{ comment: 'Natasha Teen in SZ2098 for LegalPorno',
path: 'tags/facial/poster.jpeg', },
tagSlug: 'facial', {
comment: '', path: 'tags/triple-anal/2.jpeg',
}, target_id: tagsMap['triple-anal'],
{ role: 'photo',
path: 'tags/trainbang/poster.jpeg', comment: 'Kira Thorn in GIO1018 for LegalPorno',
tagSlug: 'trainbang', },
comment: 'Kali Roses in "Passing Me Around" for Blacked', {
}, path: 'tags/blowbang/poster.jpeg',
{ target_id: tagsMap.blowbang,
path: 'tags/bukkake/poster.jpeg', role: 'poster',
tagSlug: 'bukkake', comment: '',
comment: '', },
}, {
{ path: 'tags/gangbang/poster.jpeg',
path: 'tags/swallowing/poster.jpeg', target_id: tagsMap.gangbang,
tagSlug: 'swallowing', role: 'poster',
comment: '', comment: '',
}, },
{ {
path: 'tags/creampie/poster.jpeg', path: 'tags/gangbang/1.jpeg',
tagSlug: 'creampie', target_id: tagsMap.gangbang,
comment: '', role: 'photo',
}, comment: 'Ginger Lynn in "Gangbang Mystique", a photoset shot by Suze Randall for Puritan No. 10, 1984. This photo pushed the boundaries of pornography at the time, as depicting a woman \'fully occupied\' was unheard of.',
{ },
path: 'tags/anal-creampie/poster.jpeg', {
tagSlug: 'anal-creampie', path: 'tags/gangbang/2.jpeg',
comment: '', target_id: tagsMap.gangbang,
}, role: 'photo',
{ comment: 'Riley Reid\'s double anal in "The Gangbang of Riley Reid" for Jules Jordan',
path: 'tags/oral-creampie/poster.jpeg', },
tagSlug: 'oral-creampie', {
comment: '', path: 'tags/gangbang/3.jpeg',
}, target_id: tagsMap.gangbang,
] role: 'photo',
.map((file, index) => ({ comment: 'Kelsi Monroe in "Brazzers House 2, Day 2" for Brazzers',
...file, },
thumbnail: file.thumbnail || file.path.replace('.jpeg', '_thumb.jpeg'), {
mime: 'image/jpeg', path: 'tags/mff/poster.jpeg',
index, target_id: tagsMap.mff,
})); role: 'poster',
comment: '',
const tagPhotos = [ },
{ {
path: 'tags/airtight/3.jpeg', path: 'tags/mfm/poster.jpeg',
tagSlug: 'airtight', target_id: tagsMap.mfm,
comment: 'Anita Bellini in "Triple Dick Gangbang" for Hands On Hardcore (DDF Network)', role: 'poster',
}, comment: '',
{ },
path: 'tags/airtight/2.jpeg', {
tagSlug: 'airtight', path: 'tags/orgy/poster.jpeg',
comment: 'Dakota Skye in "Dakota Goes Nuts" for ArchAngel', target_id: tagsMap.orgy,
}, role: 'poster',
{ comment: '',
path: 'tags/airtight/1.jpeg', },
tagSlug: 'airtight', {
comment: 'Chloe Amour in "DP Masters 4" for Jules Jordan', path: 'tags/asian/poster.jpeg',
}, target_id: tagsMap.asian,
{ role: 'poster',
path: 'tags/airtight/0.jpeg', comment: '',
domain: 'tags', },
tagSlug: 'airtight', {
comment: 'Sheena Shaw in "Ass Worship 14" for Jules Jordan', path: 'tags/caucasian/poster.jpeg',
}, target_id: tagsMap.caucasian,
{ role: 'poster',
path: 'tags/anal/0.jpeg', comment: '',
tagSlug: 'anal', },
comment: '', {
}, path: 'tags/ebony/poster.jpeg',
{ target_id: tagsMap.ebony,
path: 'tags/double-anal/1.jpeg', role: 'poster',
tagSlug: 'double-anal', comment: '',
comment: 'Ria Sunn in SZ1801 for LegalPorno', },
}, {
{ path: 'tags/latina/poster.jpeg',
path: 'tags/double-anal/0.jpeg', target_id: tagsMap.latina,
tagSlug: 'double-anal', role: 'poster',
comment: 'Nicole Black doing double anal during a gangbang in GIO971 for LegalPorno', comment: '',
}, },
{ {
path: 'tags/da-tp/3.jpeg', path: 'tags/interracial/poster.jpeg',
tagSlug: 'da-tp', target_id: tagsMap.interracial,
comment: 'Evelina Darling in GIO294', role: 'poster',
}, comment: '',
{ },
path: 'tags/da-tp/1.jpeg', {
tagSlug: 'da-tp', path: 'tags/facial/poster.jpeg',
comment: 'Francys Belle in SZ1702 for LegalPorno', target_id: tagsMap.facial,
}, role: 'poster',
{ comment: '',
path: 'tags/da-tp/2.jpeg', },
tagSlug: 'da-tp', {
comment: 'Angel Smalls in GIO408 for LegalPorno', path: 'tags/bukkake/poster.jpeg',
}, target_id: tagsMap.bukkake,
{ role: 'poster',
path: 'tags/da-tp/4.jpeg', comment: '',
tagSlug: 'da-tp', },
comment: 'Ninel Mojado aka Mira Cuckold in GIO063 for LegalPorno', {
}, path: 'tags/swallowing/poster.jpeg',
{ target_id: tagsMap.swallowing,
path: 'tags/dv-tp/0.jpeg', role: 'poster',
tagSlug: 'dv-tp', comment: '',
comment: 'Luna Rival in LegalPorno SZ1490', },
}, {
{ path: 'tags/creampie/poster.jpeg',
path: 'tags/double-penetration/0.jpeg', target_id: tagsMap.creampie,
tagSlug: 'double-penetration', role: 'poster',
comment: '', comment: '',
}, },
{ {
path: 'tags/gapes/0.jpeg', path: 'tags/anal-creampie/poster.jpeg',
tagSlug: 'gapes', target_id: tagsMap['anal-creampie'],
comment: 'McKenzee Miles in "Anal Buffet 4" for Evil Angel', role: 'poster',
}, comment: '',
{ },
path: 'tags/trainbang/0.jpeg', {
tagSlug: 'trainbang', path: 'tags/oral-creampie/poster.jpeg',
comment: 'Nicole Black in GIO971 for LegalPorno', target_id: tagsMap['oral-creampie'],
}, role: 'poster',
{ comment: '',
path: 'tags/triple-anal/1.jpeg', },
tagSlug: 'triple-anal', ]
comment: 'Natasha Teen in SZ2098 for LegalPorno', .map((file, index) => ({
}, ...file,
{ thumbnail: file.thumbnail || file.path.replace('.jpeg', '_thumb.jpeg'),
path: 'tags/triple-anal/2.jpeg', mime: 'image/jpeg',
tagSlug: 'triple-anal', index,
comment: 'Kira Thorn in GIO1018 for LegalPorno', domain: file.domain || 'tags',
}, role: file.role || 'photo',
{ }));
path: 'tags/gangbang/1.jpeg', }
tagSlug: 'gangbang',
comment: 'Ginger Lynn in "Gangbang Mystique", a photoset shot by Suze Randall for Puritan No. 10, 1984. This photo pushed the boundaries of pornography at the time, as depicting a woman \'fully occupied\' was unheard of.',
},
{
path: 'tags/gangbang/0.jpeg',
tagSlug: 'gangbang',
comment: '"4 On 1 Gangbangs" for Doghouse Digital',
},
{
path: 'tags/gangbang/2.jpeg',
tagSlug: 'gangbang',
comment: 'Riley Reid\'s double anal in "The Gangbang of Riley Reid" for Jules Jordan',
},
{
path: 'tags/gangbang/3.jpeg',
tagSlug: 'gangbang',
comment: 'Kelsi Monroe in "Brazzers House 2, Day 2" for Brazzers',
},
]
.map((file, index) => ({
...file,
thumbnail: file.thumbnail || file.path.replace('.jpeg', '_thumb.jpeg'),
mime: 'image/jpeg',
index,
}));
/* eslint-disable max-len */ /* eslint-disable max-len */
exports.seed = knex => Promise.resolve() exports.seed = knex => Promise.resolve()
.then(async () => { .then(async () => {
const tagMedia = tagPosters.concat(tagPhotos); const [duplicates, tags] = await Promise.all([
knex('media').where('domain', 'tags'),
const tags = await knex('tags').whereIn('slug', tagMedia.map(item => item.tagSlug)); knex('tags').where('alias_for', null),
const { inserted, updated } = await upsert('media', tagMedia.map(({
path, thumbnail, mime, index, comment,
}) => ({
path, thumbnail, mime, index, comment,
})), 'path', knex);
const tagIdsBySlug = tags.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag.id }), {});
const mediaIdsByPath = inserted.concat(updated).reduce((acc, item) => ({ ...acc, [item.path]: item.id }), {});
const tagPosterEntries = tagPosters.map(poster => ({
tag_id: tagIdsBySlug[poster.tagSlug],
media_id: mediaIdsByPath[poster.path],
}));
const tagPhotoEntries = tagPhotos.map(photo => ({
tag_id: tagIdsBySlug[photo.tagSlug],
media_id: mediaIdsByPath[photo.path],
}));
return Promise.all([
upsert('tags_posters', tagPosterEntries, 'tag_id', knex),
upsert('tags_photos', tagPhotoEntries, 'tag_id', knex),
]); ]);
const duplicatesByPath = duplicates.reduce((acc, file) => ({ ...acc, [file.path]: file }), {});
const tagsMap = tags.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
const media = getMedia(tagsMap);
return upsert('media', media, duplicatesByPath, 'path', knex);
}); });

View File

@ -1755,4 +1755,9 @@ const countries = [
]; ];
exports.seed = knex => knex('countries') exports.seed = knex => knex('countries')
.then(async () => upsert('countries', countries, 'alpha2', knex)); .then(async () => {
const duplicates = await knex('countries').select('*');
const duplicatesByAlpha2 = duplicates.reduce((acc, country) => ({ ...acc, [country.alpha2]: country }), {});
return upsert('countries', countries, duplicatesByAlpha2, 'alpha2', knex);
});

View File

@ -17,8 +17,8 @@ async function curateActor(actor) {
knex('media') knex('media')
.where({ domain: 'actors', target_id: actor.id }) .where({ domain: 'actors', target_id: actor.id })
.orderBy('index'), .orderBy('index'),
knex('actors_social') knex('social')
.where('actor_id', actor.id) .where({ domain: 'actors', target_id: actor.id })
.orderBy('platform', 'desc'), .orderBy('platform', 'desc'),
]); ]);
@ -197,7 +197,8 @@ function curateSocialEntry(url, actorId) {
return { return {
url: match.url, url: match.url,
platform: match.platform, platform: match.platform,
actor_id: actorId, domain: 'actors',
target_id: actorId,
}; };
} }
@ -206,7 +207,10 @@ async function curateSocialEntries(urls, actorId) {
return []; return [];
} }
const existingSocialLinks = await knex('actors_social').where('actor_id', actorId); const existingSocialLinks = await knex('social').where({
domain: 'actors',
target_id: actorId,
});
return urls.reduce((acc, url) => { return urls.reduce((acc, url) => {
const socialEntry = curateSocialEntry(url, actorId); const socialEntry = curateSocialEntry(url, actorId);
@ -239,7 +243,7 @@ async function fetchActors(queryObject, limit = 100) {
async function storeSocialLinks(urls, actorId) { 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('social').insert(curatedSocialEntries);
} }
async function storeActor(actor, scraped = false, scrapeSuccess = false) { async function storeActor(actor, scraped = false, scrapeSuccess = false) {
@ -354,7 +358,7 @@ async function scrapeActors(actorNames) {
updateActor(profile, true, true), updateActor(profile, true, true),
// storeAvatars(profile, actorEntry), // storeAvatars(profile, actorEntry),
storePhotos(profile.avatars, { storePhotos(profile.avatars, {
domain: 'actor', domain: 'actors',
role: 'photo', role: 'photo',
primaryRole: 'avatar', primaryRole: 'avatar',
targetId: actorEntry.id, targetId: actorEntry.id,
@ -370,7 +374,7 @@ async function scrapeActors(actorNames) {
await createMediaDirectory('actors', `${newActorEntry.slug}/`); await createMediaDirectory('actors', `${newActorEntry.slug}/`);
await storePhotos(profile.avatars, { await storePhotos(profile.avatars, {
domain: 'actor', domain: 'actors',
role: 'photo', role: 'photo',
primaryRole: 'avatar', primaryRole: 'avatar',
targetId: newActorEntry.id, targetId: newActorEntry.id,
@ -395,7 +399,7 @@ async function scrapeBasicActors() {
async function associateActors(mappedActors, releases) { async function associateActors(mappedActors, releases) {
const [existingActorEntries, existingAssociationEntries] = await Promise.all([ const [existingActorEntries, existingAssociationEntries] = await Promise.all([
knex('actors').whereIn('name', Object.keys(mappedActors)), knex('actors').whereIn('name', Object.keys(mappedActors)),
knex('releases_actors').whereIn('release_id', releases.map(release => release.id)), knex('actors_associated').whereIn('release_id', releases.map(release => release.id)),
]); ]);
const associations = await Promise.map(Object.entries(mappedActors), async ([actorName, releaseIds]) => { const associations = await Promise.map(Object.entries(mappedActors), async ([actorName, releaseIds]) => {
@ -414,7 +418,7 @@ async function associateActors(mappedActors, releases) {
}); });
await Promise.all([ await Promise.all([
knex('releases_actors').insert(associations.flat()), knex('actors_associated').insert(associations.flat()),
scrapeBasicActors(), scrapeBasicActors(),
]); ]);
} }

View File

@ -1,20 +1,26 @@
'use strict'; 'use strict';
const Promise = require('bluebird');
const argv = require('./argv'); const argv = require('./argv');
const knex = require('./knex'); const knex = require('./knex');
const initServer = require('./web/server'); const initServer = require('./web/server');
const scrapeSites = require('./scrape-sites'); const scrapeSites = require('./scrape-sites');
const { scrapeReleases } = require('./scrape-releases'); const scrapeRelease = require('./scrape-release');
const { scrapeActors, scrapeBasicActors } = require('./actors'); const { scrapeActors, scrapeBasicActors } = require('./actors');
async function init() { async function init() {
if (argv.scene) { if (argv.scene) {
await scrapeReleases(argv.scene, null, 'scene'); await Promise.map(argv.scene, async url => scrapeRelease(url, null, false, false), {
concurrency: 5,
});
} }
if (argv.movie) { if (argv.movie) {
await scrapeReleases(argv.movie, null, 'movie'); await Promise.map(argv.movie, async url => scrapeRelease(url, null, false, true), {
concurrency: 5,
});
} }
if (argv.scrape || argv.networks || argv.sites) { if (argv.scrape || argv.networks || argv.sites) {

View File

@ -10,7 +10,6 @@ const sharp = require('sharp');
const blake2 = require('blake2'); const blake2 = require('blake2');
const knex = require('./knex'); const knex = require('./knex');
const upsert = require('./utils/upsert');
function getHash(buffer) { function getHash(buffer) {
const hash = blake2.createHash('blake2b', { digestLength: 24 }); const hash = blake2.createHash('blake2b', { digestLength: 24 });
@ -41,9 +40,6 @@ async function createThumbnail(buffer) {
height: config.media.thumbnailSize, height: config.media.thumbnailSize,
withoutEnlargement: true, withoutEnlargement: true,
}) })
.jpeg({
quality: 75,
})
.toBuffer(); .toBuffer();
} }
@ -54,7 +50,7 @@ async function createMediaDirectory(domain, subpath) {
return filepath; return filepath;
} }
function curatePhotoEntries(files) { function curatePhotoEntries(files, domain = 'releases', role = 'photo', targetId) {
return files.map((file, index) => ({ return files.map((file, index) => ({
path: file.filepath, path: file.filepath,
thumbnail: file.thumbpath, thumbnail: file.thumbpath,
@ -62,33 +58,51 @@ function curatePhotoEntries(files) {
hash: file.hash, hash: file.hash,
source: file.source, source: file.source,
index, index,
domain,
target_id: targetId,
role: file.role || role,
})); }));
} }
async function findDuplicates(photos, identifier, prop = null, label) { // before fetching
const duplicates = await knex('media') async function filterSourceDuplicates(photos, domains = ['releases'], roles = ['photo'], identifier) {
.whereIn(identifier, photos.flat().map(photo => (prop ? photo[prop] : photo))); const photoSourceEntries = await knex('media')
.whereIn('source', photos.flat())
.whereIn('domain', domains)
.whereIn('role', roles); // accept string argument
const duplicateLookup = new Set(duplicates.map(photo => photo[prop || identifier])); const photoSources = new Set(photoSourceEntries.map(photo => photo.source));
const originals = photos.filter(source => (Array.isArray(source) // fallbacks provided? const newPhotos = photos.filter(source => (Array.isArray(source) // fallbacks provided?
? !source.some(sourceX => duplicateLookup.has(prop ? sourceX[prop] : sourceX)) // ensure none of the sources match ? !source.some(sourceX => photoSources.has(sourceX)) // ensure none of the sources match
: !duplicateLookup.has(prop ? source[prop] : source))); : !photoSources.has(source)));
if (duplicates.length > 0) { if (photoSourceEntries.length > 0) {
console.log(`${duplicates.length} media items already present by ${identifier} for ${label}`); console.log(`Ignoring ${photoSourceEntries.length} ${roles} items already present by source for ${identifier}`);
} }
if (originals.length > 0) { return newPhotos;
console.log(`Fetching ${originals.length} new media items for ${label}`);
}
return [duplicates, originals];
} }
async function fetchPhoto(photoUrl, index, label, attempt = 1) { // after fetching
async function filterHashDuplicates(files, domains = ['releases'], roles = ['photo'], identifier) {
const photoHashEntries = await knex('media')
.whereIn('hash', files.map(file => file.hash))
.whereIn('domain', [].concat(domains))
.whereIn('role', [].concat(roles)); // accept string argument
const photoHashes = new Set(photoHashEntries.map(entry => entry.hash));
if (photoHashEntries.length > 0) {
console.log(`Ignoring ${photoHashEntries.length} ${roles} items already present by hash for ${identifier}`);
}
return files.filter(file => file && !photoHashes.has(file.hash));
}
async function fetchPhoto(photoUrl, index, identifier, attempt = 1) {
if (Array.isArray(photoUrl)) { if (Array.isArray(photoUrl)) {
return photoUrl.reduce(async (outcome, url) => outcome.catch(async () => { return photoUrl.reduce(async (outcome, url) => outcome.catch(async () => {
const photo = await fetchPhoto(url, index, label); const photo = await fetchPhoto(url, index, identifier);
if (photo) { if (photo) {
return photo; return photo;
@ -119,11 +133,11 @@ async function fetchPhoto(photoUrl, index, label, attempt = 1) {
throw new Error(`Response ${res.statusCode} not OK`); throw new Error(`Response ${res.statusCode} not OK`);
} catch (error) { } catch (error) {
console.warn(`Failed attempt ${attempt}/3 to fetch photo ${index + 1} for ${label} (${photoUrl}): ${error}`); console.warn(`Failed attempt ${attempt}/3 to fetch photo ${index + 1} for ${identifier} (${photoUrl}): ${error}`);
if (attempt < 3) { if (attempt < 3) {
await Promise.delay(1000); await Promise.delay(1000);
return fetchPhoto(photoUrl, index, label, attempt + 1); return fetchPhoto(photoUrl, index, identifier, attempt + 1);
} }
return null; return null;
@ -131,7 +145,7 @@ async function fetchPhoto(photoUrl, index, label, attempt = 1) {
} }
async function savePhotos(files, { async function savePhotos(files, {
domain = 'release', domain = 'releases',
subpath, subpath,
role = 'photo', role = 'photo',
naming = 'index', naming = 'index',
@ -141,11 +155,11 @@ async function savePhotos(files, {
const thumbnail = await createThumbnail(file.photo); const thumbnail = await createThumbnail(file.photo);
const filename = naming === 'index' const filename = naming === 'index'
? `${file.role || role}${index + 1}` ? `${file.role || role}-${index + 1}`
: `${timestamp + index}`; : `${timestamp + index}`;
const filepath = path.join(`${domain}s`, subpath, `${filename}.${file.extension}`); const filepath = path.join(domain, subpath, `${filename}.${file.extension}`);
const thumbpath = path.join(`${domain}s`, subpath, `${filename}_thumb.${file.extension}`); const thumbpath = path.join(domain, subpath, `${filename}_thumb.${file.extension}`);
await Promise.all([ await Promise.all([
fs.writeFile(path.join(config.media.path, filepath), file.photo), fs.writeFile(path.join(config.media.path, filepath), file.photo),
@ -162,28 +176,49 @@ async function savePhotos(files, {
} }
async function storePhotos(photos, { async function storePhotos(photos, {
domain = 'release', domain = 'releases',
role = 'photo', role = 'photo',
naming = 'index', naming = 'index',
targetId, targetId,
subpath, subpath,
primaryRole, // role to assign to first photo if not already in database, used mainly for avatars primaryRole, // role to assign to first photo if not already in database, used mainly for avatars
}, label) { }, identifier) {
if (!photos || photos.length === 0) { if (!photos || photos.length === 0) {
console.warn(`No ${role}s available for ${label}`); console.warn(`No ${role}s available for ${identifier}`);
return; return;
} }
const pluckedPhotos = pluckPhotos(photos); const pluckedPhotos = pluckPhotos(photos);
const [sourceDuplicates, sourceOriginals] = await findDuplicates(pluckedPhotos, 'source', null, label); const roles = primaryRole ? [role, primaryRole] : [role];
const metaFiles = await Promise.map(sourceOriginals, async (photoUrl, index) => fetchPhoto(photoUrl, index, label), { const newPhotos = await filterSourceDuplicates(pluckedPhotos, [domain], roles, identifier);
if (newPhotos.length === 0) return;
console.log(`Fetching ${newPhotos.length} ${role}s for ${identifier}`);
const metaFiles = await Promise.map(newPhotos, async (photoUrl, index) => fetchPhoto(photoUrl, index, identifier), {
concurrency: 10, concurrency: 10,
}).filter(photo => photo); }).filter(photo => photo);
const [hashDuplicates, hashOriginals] = await findDuplicates(metaFiles, 'hash', 'hash', label); const [uniquePhotos, primaryPhoto] = await Promise.all([
filterHashDuplicates(metaFiles, [domain], roles, identifier),
primaryRole
? await knex('media')
.where('domain', domain)
.where('target_id', targetId)
.where('role', primaryRole)
.first()
: null,
]);
const savedPhotos = await savePhotos(hashOriginals, { if (primaryRole && !primaryPhoto) {
console.log(`Setting first photo as ${primaryRole} for ${identifier}`);
uniquePhotos[0].role = primaryRole;
}
const savedPhotos = await savePhotos(uniquePhotos, {
domain, domain,
role, role,
targetId, targetId,
@ -193,102 +228,59 @@ async function storePhotos(photos, {
const curatedPhotoEntries = curatePhotoEntries(savedPhotos, domain, role, targetId); const curatedPhotoEntries = curatePhotoEntries(savedPhotos, domain, role, targetId);
const newPhotos = await knex('media').insert(curatedPhotoEntries).returning('*'); await knex('media').insert(curatedPhotoEntries);
const photoEntries = Array.isArray(newPhotos)
? [...sourceDuplicates, ...hashDuplicates, ...newPhotos]
: [...sourceDuplicates, ...hashDuplicates];
const photoAssociations = photoEntries console.log(`Stored ${newPhotos.length} ${role}s for ${identifier}`);
.map(photoEntry => ({
[`${domain}_id`]: targetId,
media_id: photoEntry.id,
}));
if (primaryRole) {
// store one photo as a 'primary' photo, such as an avatar or cover
const primaryPhoto = await knex(`${domain}s_${primaryRole}s`)
.where(`${domain}_id`, targetId)
.first();
if (primaryPhoto) {
const remainingAssociations = photoAssociations.filter(association => association.media_id === primaryPhoto.media_id);
await upsert(`${domain}s_${role}s`, remainingAssociations, [`${domain}_id`, 'media_id']);
return;
}
await Promise.all([
upsert(`${domain}s_${primaryRole}s`, photoAssociations.slice(0, 1), [`${domain}_id`, 'media_id']),
upsert(`${domain}s_${role}s`, photoAssociations.slice(1), [`${domain}_id`, 'media_id']),
]);
return;
}
await upsert(`${domain}s_${role}s`, photoAssociations, [`${domain}_id`, 'media_id']);
} }
async function storeTrailer(trailers, { async function storeTrailer(trailers, {
domain = 'releases', domain = 'releases',
role = 'trailer',
targetId, targetId,
subpath, subpath,
}, label) { }, identifier) {
// support scrapers supplying multiple qualities // support scrapers supplying multiple qualities
const trailer = Array.isArray(trailers) const trailer = Array.isArray(trailers)
? trailers.find(trailerX => [1080, 720].includes(trailerX.quality)) || trailers[0] ? trailers.find(trailerX => [1080, 720].includes(trailerX.quality)) || trailers[0]
: trailers; : trailers;
if (!trailer || !trailer.src) { if (!trailer || !trailer.src) {
console.warn(`No trailer available for ${label}`); console.warn(`No trailer available for ${identifier}`);
return; return;
} }
const [sourceDuplicates, sourceOriginals] = await findDuplicates([trailer], 'source', 'src', label); console.log(`Storing trailer for ${identifier}`);
const metaFiles = await Promise.map(sourceOriginals, async (trailerX) => { const { pathname } = new URL(trailer.src);
const { pathname } = new URL(trailerX.src); const mimetype = trailer.type || mime.getType(pathname);
const mimetype = trailerX.type || mime.getType(pathname);
const res = await bhttp.get(trailerX.src); const res = await bhttp.get(trailer.src);
const hash = getHash(res.body); const filepath = path.join('releases', subpath, `trailer${trailer.quality ? `_${trailer.quality}` : ''}.${mime.getExtension(mimetype)}`);
const filepath = path.join(domain, subpath, `trailer${trailerX.quality ? `_${trailerX.quality}` : ''}.${mime.getExtension(mimetype)}`);
return { await Promise.all([
trailer: res.body, fs.writeFile(path.join(config.media.path, filepath), res.body),
knex('media').insert({
path: filepath, path: filepath,
mime: mimetype, mime: mimetype,
source: trailerX.src, source: trailer.src,
quality: trailerX.quality || null, domain,
hash, target_id: targetId,
}; role,
}); quality: trailer.quality || null,
}),
]);
}
const [hashDuplicates, hashOriginals] = await findDuplicates(metaFiles, 'hash', 'hash', label); async function findAvatar(actorId, domain = 'actors') {
return knex('media')
const newTrailers = await knex('media') .where('domain', domain)
.insert(hashOriginals.map(trailerX => ({ .where('target_id', actorId)
path: trailerX.path, .where('role', 'avatar');
mime: trailerX.mime,
source: trailerX.source,
quality: trailerX.quality,
hash: trailerX.hash,
})))
.returning('*');
await Promise.all(hashOriginals.map(trailerX => fs.writeFile(path.join(config.media.path, trailerX.path), trailerX.trailer)));
const trailerEntries = Array.isArray(newTrailers)
? [...sourceDuplicates, ...hashDuplicates, ...newTrailers]
: [...sourceDuplicates, ...hashDuplicates];
await upsert('releases_trailers', trailerEntries.map(trailerEntry => ({
release_id: targetId,
media_id: trailerEntry.id,
})), ['release_id', 'media_id']);
} }
module.exports = { module.exports = {
createMediaDirectory, createMediaDirectory,
findAvatar,
storePhotos, storePhotos,
storeTrailer, storeTrailer,
}; };

View File

@ -15,40 +15,6 @@ const {
} = require('./media'); } = require('./media');
const { fetchSites, findSiteByUrl } = require('./sites'); const { fetchSites, findSiteByUrl } = require('./sites');
function commonQuery(queryBuilder, {
filter = [],
after = new Date(0), // January 1970
before = new Date(2 ** 44), // May 2109
limit = 100,
}) {
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);
}
async function curateRelease(release) { async function curateRelease(release) {
const [actors, tags, media] = await Promise.all([ const [actors, tags, media] = await Promise.all([
knex('actors_associated') knex('actors_associated')
@ -83,9 +49,8 @@ async function curateRelease(release) {
.orderBy(['role', 'index']), .orderBy(['role', 'index']),
]); ]);
const curatedRelease = { return {
id: release.id, id: release.id,
type: release.type,
title: release.title, title: release.title,
date: release.date, date: release.date,
dateAdded: release.created_at, dateAdded: release.created_at,
@ -143,51 +108,33 @@ async function curateRelease(release) {
url: release.network_url, url: release.network_url,
}, },
}; };
return curatedRelease;
} }
function curateReleases(releases) { 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) { async function getChannelSite(release) {
if (!release.site.isFallback) { try {
return release; const site = await findSiteByUrl(release.channel);
return site || null;
} catch (error) {
const [site] = await fetchSites({
name: release.channel,
slug: release.channel,
});
return site || null;
} }
if (!release.channel) {
throw new Error(`Unable to derive channel site from generic URL: ${release.url}.`);
}
const [site] = await fetchSites({
name: release.channel,
slug: release.channel,
});
if (site) {
return {
...release,
site,
};
}
const urlSite = await findSiteByUrl(release.channel);
return {
...release,
site: urlSite,
};
} }
async function curateReleaseEntry(release) { async function curateScrapedRelease(release) {
const curatedRelease = { const curatedRelease = {
site_id: release.site.id, site_id: release.site.id,
studio_id: release.studio ? release.studio.id : null, studio_id: release.studio ? release.studio.id : null,
shoot_id: release.shootId || null, shoot_id: release.shootId || null,
entry_id: release.entryId || null, entry_id: release.entryId || null,
parent_id: release.parentId,
type: release.type,
url: release.url, url: release.url,
title: release.title, title: release.title,
date: release.date, date: release.date,
@ -200,9 +147,52 @@ async function curateReleaseEntry(release) {
deep: typeof release.deep === 'boolean' ? release.deep : false, deep: typeof release.deep === 'boolean' ? release.deep : false,
}; };
if (release.site.isFallback && release.channel) {
const site = await getChannelSite(release);
if (site) {
curatedRelease.site_id = site.id;
return curatedRelease;
}
}
return curatedRelease; return curatedRelease;
} }
function commonQuery(queryBuilder, {
filter = [],
after = new Date(0), // January 1970
before = new Date(2 ** 44), // May 2109
limit = 100,
}) {
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('date', '>', after)
.andWhere('date', '<=', before)
.orderBy([{ column: 'date', order: 'desc' }, { column: 'created_at', order: 'desc' }])
.limit(limit);
}
async function fetchReleases(queryObject = {}, options = {}) { async function fetchReleases(queryObject = {}, options = {}) {
const releases = await knex('releases') const releases = await knex('releases')
.modify(commonQuery, options) .modify(commonQuery, options)
@ -254,40 +244,6 @@ async function fetchTagReleases(queryObject, options = {}) {
return curateReleases(releases); return curateReleases(releases);
} }
function accumulateActors(releases) {
return releases.reduce((acc, release) => {
if (!release.actors) return acc;
release.actors.forEach((actor) => {
const trimmedActor = actor.trim();
if (acc[trimmedActor]) {
acc[trimmedActor] = acc[trimmedActor].concat(release.id);
return;
}
acc[trimmedActor] = [release.id];
});
return acc;
}, {});
}
function accumulateMovies(releases) {
return releases.reduce((acc, release) => {
if (release.movie) {
if (acc[release.movie]) {
acc[release.movie] = acc[release.movie].concat(release.id);
return acc;
}
acc[release.movie] = [release.id];
}
return acc;
}, {});
}
async function storeReleaseAssets(release, releaseId) { async function storeReleaseAssets(release, releaseId) {
const subpath = `${release.site.network.slug}/${release.site.slug}/${release.id}/`; const subpath = `${release.site.network.slug}/${release.site.slug}/${release.id}/`;
const identifier = `"${release.title}" (${releaseId})`; const identifier = `"${release.title}" (${releaseId})`;
@ -323,7 +279,7 @@ async function storeReleaseAssets(release, releaseId) {
async function storeRelease(release) { async function storeRelease(release) {
const existingRelease = await knex('releases').where('entry_id', release.entryId).first(); const existingRelease = await knex('releases').where('entry_id', release.entryId).first();
const curatedRelease = await curateReleaseEntry(release); const curatedRelease = await curateScrapedRelease(release);
if (existingRelease && !argv.redownload) { if (existingRelease && !argv.redownload) {
return existingRelease.id; return existingRelease.id;
@ -361,13 +317,18 @@ async function storeRelease(release) {
async function storeReleases(releases) { async function storeReleases(releases) {
const storedReleases = await Promise.map(releases, async (release) => { const storedReleases = await Promise.map(releases, async (release) => {
if (release.site.isFallback && !release.channel) {
console.error(`Unable to derive channel site from generic URL: ${release.url}.`);
return null;
}
try { try {
const releaseWithChannelSite = await attachChannelSite(release); const releaseId = await storeRelease(release);
const releaseId = await storeRelease(releaseWithChannelSite);
return { return {
id: releaseId, id: releaseId,
...releaseWithChannelSite, ...release,
}; };
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -378,8 +339,22 @@ async function storeReleases(releases) {
concurrency: 10, concurrency: 10,
}).filter(release => release); }).filter(release => release);
const actors = accumulateActors(storedReleases); const actors = storedReleases.reduce((acc, release) => {
const movies = accumulateMovies(storedReleases); if (!release.actors) return acc;
release.actors.forEach((actor) => {
const trimmedActor = actor.trim();
if (acc[trimmedActor]) {
acc[trimmedActor] = acc[trimmedActor].concat(release.id);
return;
}
acc[trimmedActor] = [release.id];
});
return acc;
}, {});
await Promise.all([ await Promise.all([
associateActors(actors, storedReleases), associateActors(actors, storedReleases),
@ -388,11 +363,7 @@ async function storeReleases(releases) {
}), }),
]); ]);
return { return storedReleases;
releases: storedReleases,
actors,
movies,
};
} }
module.exports = { module.exports = {

68
src/scrape-release.js Normal file
View File

@ -0,0 +1,68 @@
'use strict';
const config = require('config');
const argv = require('./argv');
const scrapers = require('./scrapers/scrapers');
const { storeReleases } = require('./releases');
const { findSiteByUrl } = require('./sites');
const { findNetworkByUrl } = require('./networks');
async function findSite(url, release) {
const site = (release && release.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(url, release, deep = false, isMovie = false) {
const site = await findSite(url, release);
if (!site) {
throw new Error('Could not find site in database');
}
const scraper = scrapers.releases[site.slug] || scrapers.releases[site.network.slug];
if (!scraper) {
throw new Error('Could not find scraper for URL');
}
if (!isMovie && !scraper.fetchScene) {
throw new Error(`The '${site.name}'-scraper cannot fetch individual scenes`);
}
if (isMovie && !scraper.fetchMovie) {
throw new Error(`The '${site.name}'-scraper cannot fetch individual movies`);
}
const scrapedRelease = isMovie
? await scraper.fetchMovie(url, site, release)
: await scraper.fetchScene(url, site, release);
if (!deep && argv.save) {
// don't store release when called by site scraper
const [storedRelease] = await storeReleases([scrapedRelease]);
if (storedRelease) {
console.log(`http://${config.web.host}:${config.web.port}/scene/${storedRelease.id}`);
}
}
return scrapedRelease;
}
module.exports = scrapeRelease;

View File

@ -1,90 +0,0 @@
'use strict';
const config = require('config');
const Promise = require('bluebird');
const argv = require('./argv');
const scrapers = require('./scrapers/scrapers');
const { storeReleases } = require('./releases');
const { findSiteByUrl } = require('./sites');
const { findNetworkByUrl } = require('./networks');
async function findSite(url, release) {
const site = (release && release.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(url, release, type = 'scene') {
const site = await findSite(url, release);
if (!site) {
throw new Error('Could not find site in database');
}
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) {
throw new Error(`The '${site.name}'-scraper cannot fetch individual scenes`);
}
if (type === 'movie' && !scraper.fetchMovie) {
throw new Error(`The '${site.name}'-scraper cannot fetch individual movies`);
}
const scrapedRelease = type === 'scene'
? await scraper.fetchScene(url, site, release)
: await scraper.fetchMovie(url, site, release);
return scrapedRelease;
}
async function scrapeReleases(urls, release, type = 'scene') {
const scrapedReleases = await Promise.map(urls, async url => scrapeRelease(url, release, type), {
concurrency: 5,
});
const curatedReleases = scrapedReleases.map(scrapedRelease => ({ ...scrapedRelease, type }));
if (argv.save) {
/*
const movie = scrapedRelease.movie
? await scrapeRelease(scrapedRelease.movie, null, false, 'movie')
: null;
if (movie) {
const { releases: [storedMovie] } = await storeReleases([movie]);
curatedRelease.parentId = storedMovie.id;
}
*/
const { releases: storedReleases } = await storeReleases(curatedReleases);
if (storedReleases) {
console.log(storedReleases.map(storedRelease => `http://${config.web.host}:${config.web.port}/scene/${storedRelease.id}`).join('\n'));
}
}
}
module.exports = {
scrapeRelease,
scrapeReleases,
};

View File

@ -7,7 +7,7 @@ const argv = require('./argv');
const knex = require('./knex'); const knex = require('./knex');
const { fetchIncludedSites } = require('./sites'); const { fetchIncludedSites } = require('./sites');
const scrapers = require('./scrapers/scrapers'); const scrapers = require('./scrapers/scrapers');
const { scrapeRelease } = require('./scrape-releases'); const scrapeRelease = require('./scrape-release');
const { storeReleases } = require('./releases'); const { storeReleases } = require('./releases');
function getAfterDate() { function getAfterDate() {
@ -70,7 +70,7 @@ async function deepFetchReleases(baseReleases) {
return Promise.map(baseReleases, async (release) => { return Promise.map(baseReleases, async (release) => {
if (release.url) { if (release.url) {
try { try {
const fullRelease = await scrapeRelease(release.url, release, 'scene'); const fullRelease = await scrapeRelease(release.url, release, true);
return { return {
...release, ...release,
@ -111,10 +111,10 @@ async function scrapeSiteReleases(scraper, site) {
return baseReleases; return baseReleases;
} }
async function scrapeSites() { async function scrapeReleases() {
const networks = await fetchIncludedSites(); const networks = await fetchIncludedSites();
const scrapedNetworks = await Promise.map(networks, async network => Promise.map(network.sites, async (site) => { const scrapedReleases = await Promise.map(networks, async network => Promise.map(network.sites, async (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) { if (!scraper) {
@ -143,8 +143,8 @@ async function scrapeSites() {
}); });
if (argv.save) { if (argv.save) {
await storeReleases(scrapedNetworks.flat(2)); await storeReleases(scrapedReleases.flat(2));
} }
} }
module.exports = scrapeSites; module.exports = scrapeReleases;

View File

@ -27,7 +27,7 @@ async function getPhotos(albumUrl) {
const lastPhotoPage = Array.from(document.querySelectorAll('.preview-image-container a')).slice(-1)[0].href; const lastPhotoPage = Array.from(document.querySelectorAll('.preview-image-container a')).slice(-1)[0].href;
const lastPhotoIndex = parseInt(lastPhotoPage.match(/\d+.jpg/)[0], 10); const lastPhotoIndex = parseInt(lastPhotoPage.match(/\d+.jpg/)[0], 10);
const photoUrls = await Promise.map(Array.from({ length: lastPhotoIndex }), async (value, index) => { const photoUrls = await Promise.map(Array.from({ length: lastPhotoIndex }), async (index) => {
const pageUrl = `https://blacksonblondes.com${lastPhotoPage.replace(/\d+.jpg/, `${index.toString().padStart(3, '0')}.jpg`)}`; const pageUrl = `https://blacksonblondes.com${lastPhotoPage.replace(/\d+.jpg/, `${index.toString().padStart(3, '0')}.jpg`)}`;
return getPhoto(pageUrl); return getPhoto(pageUrl);

View File

@ -50,9 +50,7 @@ function scrapeProfile(html, actorName) {
if (bio.weight) profile.weight = Number(bio.weight.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);
profile.avatar = document.querySelector('.profile-image-large img').src;
const avatar = document.querySelector('.profile-image-large img').src;
if (!avatar.match('placeholder')) profile.avatar = document.querySelector('.profile-image-large img').src;
return profile; return profile;
} }

View File

@ -16,7 +16,7 @@ async function scrapeProfile(html, _url, actorName) {
const { document } = new JSDOM(html).window; const { document } = new JSDOM(html).window;
const entries = Array.from(document.querySelectorAll('.infoPiece'), el => el.textContent.replace(/\n|\t/g, '').split(':')); const entries = Array.from(document.querySelectorAll('.infoPiece'), el => el.textContent.replace(/\n|\t/g, '').split(':'));
const bio = entries.reduce((acc, [key, value]) => (key ? { ...acc, [key.trim()]: value.trim() } : acc), {}); const bio = entries.reduce((acc, [key, value]) => ({ ...acc, [key.trim()]: value.trim() }), {});
const profile = { const profile = {
name: actorName, name: actorName,

View File

@ -54,13 +54,13 @@ module.exports = {
actors: { actors: {
// ordered by data priority // ordered by data priority
xempire, xempire,
julesjordan,
brazzers, brazzers,
legalporno, legalporno,
pornhub, pornhub,
freeones, freeones,
freeonesLegacy, freeonesLegacy,
kellymadison, kellymadison,
julesjordan,
ddfnetwork, ddfnetwork,
}, },
}; };

View File

@ -66,16 +66,20 @@ async function associateTags(release, releaseId) {
? await matchTags(release.tags) // scraper returned raw tags ? await matchTags(release.tags) // scraper returned raw tags
: release.tags; // tags already matched by (outdated) scraper : release.tags; // tags already matched by (outdated) scraper
const associationEntries = await knex('releases_tags') const associationEntries = await knex('tags_associated')
.where('release_id', releaseId) .where({
domain: 'releases',
target_id: releaseId,
})
.whereIn('tag_id', tags); .whereIn('tag_id', tags);
const existingAssociations = new Set(associationEntries.map(association => association.tag_id)); const existingAssociations = new Set(associationEntries.map(association => association.tag_id));
const newAssociations = tags.filter(tagId => !existingAssociations.has(tagId)); const newAssociations = tags.filter(tagId => !existingAssociations.has(tagId));
await knex('releases_tags').insert(newAssociations.map(tagId => ({ await knex('tags_associated').insert(newAssociations.map(tagId => ({
tag_id: tagId, tag_id: tagId,
release_id: releaseId, domain: 'releases',
target_id: releaseId,
}))); })));
} }

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