Compare commits

...

18 Commits

Author SHA1 Message Date
ThePendulum e2c2c9b4f0 1.44.0 2020-01-05 01:07:56 +01:00
ThePendulum 2271577874 Improved network overview responsivity. 2020-01-05 01:07:32 +01:00
ThePendulum 30cf597ec9 Improving network overview. Added DDF logos and tag posters. 2020-01-04 04:58:56 +01:00
ThePendulum 72b175e9e2 Removed views in favor of PostGraphile filter and sort plugins. Updated site modules to GraphQL. Added tag posters. 2020-01-04 02:51:58 +01:00
ThePendulum 70e27a6cd9 Moved networks to GraphQL. 2020-01-03 00:59:02 +01:00
ThePendulum e77dbca954 Fixed actor data and avatar fetching and display. 2020-01-02 17:13:57 +01:00
ThePendulum 5a6bf2b42f Further refactoring. Fixed actor pages and more. 2019-12-31 03:12:52 +01:00
ThePendulum 1c43884102 Refactoring client to reflect database changes. 2019-12-19 04:42:50 +01:00
ThePendulum 31aee71edb Normalized database. Updated seed files. Simplified seed upsert. 2019-12-19 02:35:07 +01:00
ThePendulum 9b17add4e2 Re-added date ranges. 2019-12-18 02:42:55 +01:00
ThePendulum 3845c3f52d Added comment to poster and photo fragment. 2019-12-16 05:32:09 +01:00
ThePendulum 6950a76cb5 Abstracted fragments and curation. Using GraphQL for tags. 2019-12-16 05:30:25 +01:00
ThePendulum f4c2e6c08c Replaced default height and weight fields with fields taking units argument. 2019-12-16 02:39:13 +01:00
ThePendulum 577c03f9b7 Fixed queries. 2019-12-15 23:46:42 +01:00
ThePendulum ce92d13327 Fixed site fallback handling. 2019-12-15 23:14:43 +01:00
ThePendulum 13b45e1709 Fixed Babel dependencies. 2019-12-15 23:01:48 +01:00
ThePendulum 07a6c77ce2 Improvements, GrapQL experiments. Fixed Babel dependencies. 2019-12-15 22:16:55 +01:00
ThePendulum 7ba716cd6f Experimenting using GraphQL in favor of REST. 2019-12-15 05:42:51 +01:00
104 changed files with 2797 additions and 1946 deletions

View File

@ -3,7 +3,7 @@
v-if="actor" v-if="actor"
class="content actor" class="content actor"
> >
<FilterBar :fetch-releases="fetchReleases" /> <FilterBar :fetch-releases="fetchActor" />
<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 }} cm</span> <span class="height-metric">{{ actor.height.metric }} cm</span>
<span class="height-imperial">{{ imperialHeight.feet }}' {{ imperialHeight.inches }}"</span> <span class="height-imperial">{{ actor.height.imperial }}</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 }} kg</span> <span class="weight-metric">{{ actor.weight.metric }} kg</span>
<span class="weight-imperial">{{ imperialWeight }} lbs</span> <span class="weight-imperial">{{ actor.weight.imperial }} lbs</span>
</span> </span>
</li> </li>
@ -232,29 +232,19 @@
/> />
</div> </div>
<Releases :releases="releases" /> <Releases :releases="actor.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 fetchReleases() { async function fetchActor() {
this.releases = await this.$store.dispatch('fetchActorReleases', this.$route.params.actorSlug); this.actor = await this.$store.dispatch('fetchActors', { actorSlug: this.$route.params.actorSlug });
}
function imperialHeight() {
return cmToFeetInches(this.actor.height);
}
function imperialWeight() {
return kgToLbs(this.actor.weight);
} }
function scrollPhotos(event) { function scrollPhotos(event) {
@ -262,10 +252,7 @@ function scrollPhotos(event) {
} }
async function mounted() { async function mounted() {
[this.actor] = await Promise.all([ this.fetchActor();
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;
@ -286,13 +273,9 @@ export default {
expanded: false, expanded: false,
}; };
}, },
computed: {
imperialHeight,
imperialWeight,
},
mounted, mounted,
methods: { methods: {
fetchReleases, fetchActor,
scrollPhotos, scrollPhotos,
}, },
}; };

View File

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

View File

@ -1,20 +1,6 @@
<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`"
@ -42,6 +28,20 @@
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'); this.releases = await this.$store.dispatch('fetchReleases', { limit: 100 });
} }
async function mounted() { async function mounted() {

View File

@ -1,11 +1,35 @@
<template> <template>
<div <div
v-if="network" v-if="network"
class="content network" class="content"
> >
<FilterBar :fetch-releases="fetchReleases" /> <FilterBar :fetch-releases="fetchNetwork" />
<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"
@ -17,34 +41,18 @@
: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>
<template v-if="sites.length"> <div class="content-inner">
<h3 class="heading">Sites</h3> <Sites
v-if="sites.length"
:sites="sites"
class="compact"
/>
<ul class="nolist sites"> <Releases :releases="releases" />
<li </div>
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>
@ -52,22 +60,20 @@
<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 SiteTile from '../tile/site.vue'; import Sites from '../sites/sites.vue';
async function fetchReleases() { async function fetchNetwork() {
this.releases = await this.$store.dispatch('fetchNetworkReleases', this.$route.params.networkSlug); this.network = await this.$store.dispatch('fetchNetworks', 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;
} }
@ -75,64 +81,108 @@ export default {
components: { components: {
FilterBar, FilterBar,
Releases, Releases,
SiteTile, Sites,
}, },
data() { data() {
return { return {
network: null, network: null,
sites: null, sites: null,
releases: null, releases: [],
pageTitle: null, pageTitle: null,
}; };
}, },
mounted, mounted,
methods: { methods: {
fetchReleases, fetchNetwork,
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss">
@import 'theme'; @import 'theme';
.header { @media(max-width: $breakpoint3) {
display: flex; .releases .tiles {
flex-wrap: wrap; grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
justify-content: space-between; }
align-items: top; }
margin: 0 0 2rem 0; </style>
}
<style lang="scss" scoped>
.title { @import 'theme';
display: inline-flex;
align-items: top; .network {
margin: 0 1rem 0 0; display: flex;
flex-direction: row;
&:hover .icon { flex-grow: 1;
fill: $primary; justify-content: stretch;
} overflow-y: auto;
} }
.logo { .sidebar {
width: 20rem; height: 100%;
max-height: 8rem; width: 18rem;
object-fit: contain; display: flex;
margin: 0 .5rem 0 0; flex-direction: column;
} flex-shrink: 0;
color: $text-contrast;
.sites { border-right: solid 1px $shadow-hint;
display: grid; overflow: hidden;
grid-gap: 1rem; }
margin: 0 0 2rem 0;
} .sidebar .title {
border-bottom: solid 1px $shadow-hint;
.sites { }
grid-template-columns: repeat(auto-fit, 15rem);
} .logo {
width: 100%;
@media(max-width: $breakpoint) { max-height: 8rem;
.sites { display: flex;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); justify-content: center;
object-fit: contain;
box-sizing: border-box;
padding: 1rem;
margin: 0;
filter: $logo-shadow;
}
.sites.compact {
display: none;
}
.header {
width: 100%;
height: 4rem;
display: none;
justify-content: center;
border-bottom: solid 1px $shadow-hint;
}
@media(max-width: $breakpoint) {
.header {
display: flex;
}
.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,6 +108,22 @@
</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"
@ -196,8 +212,10 @@
</template> </template>
<script> <script>
import Actor from '../tile/actor.vue';
import Banner from './banner.vue'; import Banner from './banner.vue';
import Actor from '../tile/actor.vue';
import Release from '../tile/release.vue';
import Releases from './releases.vue';
function pageTitle() { function pageTitle() {
return this.release && this.release.title; return this.release && this.release.title;
@ -211,6 +229,8 @@ export default {
components: { components: {
Actor, Actor,
Banner, Banner,
Releases,
Release,
}, },
data() { data() {
return { return {
@ -293,7 +313,7 @@ export default {
.logo { .logo {
display: inline-block; display: inline-block;
filter: $logo-outline; filter: $logo-shadow;
} }
.logo-site { .logo-site {

View File

@ -56,6 +56,7 @@ 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

@ -1,157 +0,0 @@
<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,6 +16,7 @@
> >
<img <img
:src="`/img/${tag.poster.thumbnail}`" :src="`/img/${tag.poster.thumbnail}`"
:alt="tag.poster.comment"
class="poster" class="poster"
> >
</a> </a>
@ -43,6 +44,7 @@
> >
<img <img
:src="`/img/${photo.thumbnail}`" :src="`/img/${photo.thumbnail}`"
:alt="photo.comment"
class="photo" class="photo"
> >
</a> </a>
@ -50,7 +52,7 @@
</div> </div>
<div class="content-inner"> <div class="content-inner">
<Releases :releases="releases" /> <Releases :releases="tag.releases" />
</div> </div>
</div> </div>
</div> </div>
@ -68,17 +70,13 @@ import Releases from '../releases/releases.vue';
const converter = new Converter(); const converter = new Converter();
async function fetchReleases() { async function fetchReleases() {
this.releases = await this.$store.dispatch('fetchTagReleases', this.$route.params.tagSlug); this.tag = await this.$store.dispatch('fetchTags', { tagSlug: this.$route.params.tagSlug });
} }
async function mounted() { async function mounted() {
[this.tag] = await Promise.all([ this.tag = await this.$store.dispatch('fetchTags', { tagSlug: this.$route.params.tagSlug });
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;
} }
@ -90,6 +88,7 @@ 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>Ethnicity</h3> <h3>Oral</h3>
<div class="tiles"> <div class="tiles">
<Tag <Tag
v-for="tag in tags.ethnicity" v-for="tag in tags.oral"
:key="`tag-${tag.id}`" :key="`tag-${tag.id}`"
:tag="tag" :tag="tag"
/> />
@ -30,6 +30,16 @@
/> />
</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">
@ -39,6 +49,16 @@
: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>
@ -47,41 +67,55 @@ 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', {
slug: [ slugs: [
'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',
'triple-anal',
'blowbang',
'gangbang',
'mff',
'mfm',
'orgy',
'asian',
'caucasian',
'ebony', 'ebony',
'facefuck',
'facial',
'gangbang',
'gapes',
'interracial', 'interracial',
'latina', 'latina',
'anal-creampie', 'mff',
'bukkake', 'mfm',
'creampie',
'facial',
'oral-creampie', 'oral-creampie',
'orgy',
'pussy-eating',
'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,17 +27,18 @@
v-if="actor.age || actor.origin" v-if="actor.age || actor.origin"
class="details" class="details"
> >
<span v-if="actor.age"> <span>
<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="'Age at scene date'" v-tooltip="`${actor.ageThen} years old on release 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"
@ -114,6 +115,7 @@ 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-outline; filter: $logo-shadow;
} }
.title { .title {

View File

@ -1,5 +1,8 @@
<template> <template>
<div class="tile"> <div
class="tile"
:class="{ movie: release.type === 'movie' }"
>
<span class="banner"> <span class="banner">
<span class="details"> <span class="details">
<router-link <router-link
@ -39,7 +42,7 @@
</span> </span>
<router-link <router-link
:to="`/scene/${release.id}`" :to="`/${release.type || 'scene'}/${release.id}`"
class="link" class="link"
> >
<img <img
@ -66,14 +69,19 @@
<div class="info"> <div class="info">
<router-link <router-link
:to="`/scene/${release.id}`" :to="`/${release.type || '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">
@ -212,13 +220,19 @@ export default {
} }
.title { .title {
color: $text; display: flex;
align-items: center;
margin: 0 .25rem .25rem 0; margin: 0 .25rem .25rem 0;
color: $text;
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 {
@ -235,9 +249,8 @@ export default {
} }
.tags { .tags {
max-height: 2.5rem; max-height: .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"
> >
<object <img
:data="`/img/logos/${site.network.slug}/${site.slug}.png`" :src="`/img/logos/${site.network.slug}/${site.slug}.png`"
type="image/png" :alt="site.name"
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-outline; filter: $logo-shadow;
} }
.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,7 +23,8 @@ $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-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-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-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;

5
assets/img/film.svg Normal file
View File

@ -0,0 +1,5 @@
<!-- 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>

After

Width:  |  Height:  |  Size: 387 B

5
assets/img/film2.svg Normal file
View File

@ -0,0 +1,5 @@
<!-- 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>

After

Width:  |  Height:  |  Size: 358 B

5
assets/img/film3.svg Normal file
View File

@ -0,0 +1,5 @@
<!-- 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>

After

Width:  |  Height:  |  Size: 311 B

View File

@ -1,12 +1,191 @@
import { get } from '../api'; import { graphql, 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 fetchActors({ _commit }, { actorId, limit = 100 }) { async function fetchActorBySlug(actorSlug, limit = 100) {
if (actorId) { const { actor } = await graphql(`
return get(`/actors/${actorId}`, { limit }); query Actor(
$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);
} }
return get('/actors', { limit }); const { actors } = await graphql(`
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: 'GET', method: 'POST',
mode: 'cors', mode: 'cors',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -39,4 +39,33 @@ async function post(endpoint, data) {
throw new Error(errorMsg); throw new Error(errorMsg);
} }
export { get, post }; async function graphql(query, variables = null) {
const res = await fetch('/graphql', {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify({
query,
variables,
}),
});
if (res.ok) {
const { data } = await res.json();
return data;
}
const errorMsg = await res.text();
throw new Error(errorMsg);
}
export {
get,
post,
graphql,
};

76
assets/js/curate.js Normal file
View File

@ -0,0 +1,76 @@
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,
};

154
assets/js/fragments.js Normal file
View File

@ -0,0 +1,154 @@
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,27 +1,68 @@
import { get } from '../api'; import { graphql } from '../api';
import { sitesFragment, releasesFragment } from '../fragments';
import { curateNetwork } from '../curate';
function initNetworksActions(store, _router) { function initNetworksActions(store, _router) {
async function fetchNetworks({ _commit }, networkId) { async function fetchNetworkBySlug(networkSlug, limit = 100) {
const networks = await get(`/networks/${networkId || ''}`, { const { network } = await graphql(`
query Network(
}); $networkSlug: String!
$limit:Int = 1000,
return networks; $after:Date = "1900-01-01",
} $before:Date = "2100-01-01",
) {
async function fetchNetworkReleases({ _commit }, networkId) { network: networkBySlug(slug: $networkSlug) {
const releases = await get(`/networks/${networkId}/releases`, { id
filter: store.state.ui.filter, name
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 releases; return curateNetwork(network);
}
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,20 +1,40 @@
import { get } from '../api'; import { graphql } from '../api';
import { releasesFragment, releaseFragment } from '../fragments';
import { curateRelease } from '../curate';
function initReleasesActions(store, _router) { function initReleasesActions(store, _router) {
async function fetchReleases({ _commit }) { async function fetchReleases({ _commit }, { limit = 100 }) {
const releases = await get('/releases', { console.log(store.state.ui.filter, store.getters.after, store.getters.before);
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; return releases.map(release => curateRelease(release));
} }
async function fetchReleaseById({ _commit }, releaseId) { async function fetchReleaseById({ _commit }, releaseId) {
const release = await get(`/releases/${releaseId}`); // const release = await get(`/releases/${releaseId}`);
return release; const { release } = await graphql(`
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/site/site.vue'; import Site from '../components/sites/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,12 +1,69 @@
import { get } from '../api'; import { graphql } from '../api';
import { releasesFragment } from '../fragments';
import { curateSite } from '../curate';
function initSitesActions(store, _router) { function initSitesActions(store, _router) {
async function fetchSites({ _commit }, siteId) { async function fetchSiteBySlug(siteSlug, limit = 100) {
const sites = await get(`/sites/${siteId || ''}`); const { site } = await graphql(`
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,
@ -16,10 +73,11 @@ function initSitesActions(store, _router) {
return releases; return releases;
} }
*/
return { return {
fetchSites, fetchSites,
fetchSiteReleases, // fetchSiteReleases,
}; };
} }

View File

@ -1,23 +1,97 @@
import { get } from '../api'; import { graphql, 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 }, {
tagId, tagSlug,
limit = 100, limit = 100,
slug, slugs = [],
group, _group,
priority, _priority,
}) { }) {
if (tagId) { if (tagSlug) {
return get(`/tags/${tagId}`); return fetchTagBySlug(tagSlug);
} }
return get('/tags', { const { tags } = await graphql(`
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) {
@ -33,6 +107,7 @@ 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: dayjs(new Date(0)).format('YYYY-MM-DD'), after: '1900-01-01',
before: dayjs(new Date()).format('YYYY-MM-DD'), before: dayjs(new Date()).add(1, 'day').format('YYYY-MM-DD'),
}), }),
upcoming: () => ({ upcoming: () => ({
after: dayjs(new Date()).format('YYYY-MM-DD'), after: dayjs(new Date()).format('YYYY-MM-DD'),
before: dayjs(new Date(2 ** 42)).format('YYYY-MM-DD'), before: '2100-01-01',
}), }),
all: () => ({ all: () => ({
after: dayjs(new Date(0)).format('YYYY-MM-DD'), after: '1900-01-01',
before: dayjs(new Date(2 ** 42)).format('YYYY-MM-DD'), before: '2100-01-01',
}), }),
}; };

View File

@ -17,6 +17,184 @@ 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);
@ -70,6 +248,48 @@ 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);
@ -84,92 +304,6 @@ 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);
@ -193,14 +327,10 @@ 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', 16) table.integer('parent_id', 16)
.references('id') .references('id')
.inTable('releases'); .inTable('releases');
@ -209,46 +339,7 @@ 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('media', (table) => { .then(() => knex.schema.createTable('releases_actors', (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')
@ -261,9 +352,7 @@ exports.up = knex => Promise.resolve()
table.unique(['release_id', 'actor_id']); table.unique(['release_id', 'actor_id']);
})) }))
.then(() => knex.schema.createTable('directors_associated', (table) => { .then(() => knex.schema.createTable('releases_directors', (table) => {
table.increments('id', 16);
table.integer('release_id', 16) table.integer('release_id', 16)
.notNullable() .notNullable()
.references('id') .references('id')
@ -276,30 +365,131 @@ exports.up = knex => Promise.resolve()
table.unique(['release_id', 'director_id']); table.unique(['release_id', 'director_id']);
})) }))
.then(() => knex.schema.createTable('tags_associated', (table) => { .then(() => knex.schema.createTable('releases_posters', (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.string('domain'); table.integer('release_id', 16)
table.integer('target_id', 16); .notNullable()
.references('id')
.inTable('releases');
table.unique(['domain', 'tag_id', 'target_id']); table.unique(['tag_id', 'release_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() /*
.then(() => knex.schema.dropTable('tags_associated')) CREATE VIEW releases_actors_sortable AS
.then(() => knex.schema.dropTable('directors_associated')) SELECT releases_actors.*, actors.gender, actors.name, actors.birthdate FROM releases_actors
.then(() => knex.schema.dropTable('actors_associated')) JOIN actors ON releases_actors.actor_id = actors.id;
.then(() => knex.schema.dropTable('tags'))
.then(() => knex.schema.dropTable('tags_groups')) CREATE VIEW releases_tags_sortable AS
.then(() => knex.schema.dropTable('media')) SELECT releases_tags.*, tags.name, tags.priority FROM releases_tags
.then(() => knex.schema.dropTable('social')) JOIN tags ON releases_tags.tag_id = tags.id;
.then(() => knex.schema.dropTable('actors'))
.then(() => knex.schema.dropTable('releases')) CREATE VIEW actors_releases_sortable AS
.then(() => knex.schema.dropTable('sites')) SELECT releases_actors.*, releases.date FROM releases_actors
.then(() => knex.schema.dropTable('studios')) JOIN releases ON releases_actors.release_id = releases.id;
.then(() => knex.schema.dropTable('directors'))
.then(() => knex.schema.dropTable('networks')) 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('countries')); COMMENT ON VIEW releases_tags_sortable IS E'@foreignKey (release_id) references releases (id)\n@foreignKey (tag_id) references tags (id)';
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.43.1", "version": "1.44.0",
"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,12 +39,11 @@
"@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",
@ -67,6 +66,8 @@
"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",
@ -83,6 +84,7 @@
"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",
@ -90,6 +92,8 @@
"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,14 +188,21 @@
text-decoration: none; text-decoration: none;
} }
.title[data-v-3abcf101] { .title[data-v-3abcf101] {
color: #222; display: -webkit-box;
display: flex;
-webkit-box-align: center;
align-items: center;
margin: 0 .25rem .25rem 0; margin: 0 .25rem .25rem 0;
color: #222;
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;
@ -208,9 +215,8 @@
line-height: 1.5rem; line-height: 1.5rem;
} }
.tags[data-v-3abcf101] { .tags[data-v-3abcf101] {
max-height: 2.5rem; max-height: .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;
} }
@ -255,6 +261,7 @@
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;
@ -270,6 +277,31 @@
} }
} }
/* $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;
@ -317,6 +349,8 @@
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;
@ -330,31 +364,6 @@
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;
@ -510,28 +519,28 @@
} }
/* $primary: #ff886c; */ /* $primary: #ff886c; */
.header[data-v-3e57cf44] { .header[data-v-194630f6] {
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-3e57cf44] { .title[data-v-194630f6] {
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-3e57cf44] { .title:hover .icon[data-v-194630f6] {
fill: #ff6c88; fill: #ff6c88;
} }
.heading[data-v-3e57cf44] { .heading[data-v-194630f6] {
padding: 0; padding: 0;
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
} }
.link[data-v-3e57cf44] { .link[data-v-194630f6] {
display: -webkit-box; display: -webkit-box;
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
@ -541,20 +550,20 @@
-webkit-box-align: end; -webkit-box-align: end;
align-items: flex-end; align-items: flex-end;
} }
.logo[data-v-3e57cf44] { .logo[data-v-194630f6] {
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-3e57cf44] { .networklogo-container[data-v-194630f6] {
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-3e57cf44] { .networklogo[data-v-194630f6] {
color: #222; color: #222;
width: 15rem; width: 15rem;
max-height: 6rem; max-height: 6rem;
@ -565,13 +574,13 @@
object-position: 100% 0; object-position: 100% 0;
margin: 0 0 0 .5rem; margin: 0 0 0 .5rem;
} }
.sites[data-v-3e57cf44], .sites[data-v-194630f6],
.scenes[data-v-3e57cf44] { .scenes[data-v-194630f6] {
display: grid; display: grid;
grid-gap: 1rem; grid-gap: 1rem;
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
} }
.sites[data-v-3e57cf44] { .sites[data-v-194630f6] {
grid-template-columns: repeat(auto-fit, 15rem); grid-template-columns: repeat(auto-fit, 15rem);
} }
@ -623,44 +632,111 @@
} }
/* $primary: #ff886c; */ /* $primary: #ff886c; */
.header[data-v-e2e12602] { .sites[data-v-7bebaa3e] {
display: -webkit-box;
display: flex;
flex-wrap: wrap;
-webkit-box-pack: justify;
justify-content: space-between;
-webkit-box-align: top;
align-items: top;
margin: 0 0 2rem 0;
}
.title[data-v-e2e12602] {
display: -webkit-inline-box;
display: inline-flex;
-webkit-box-align: top;
align-items: top;
margin: 0 1rem 0 0;
}
.title:hover .icon[data-v-e2e12602] {
fill: #ff6c88;
}
.logo[data-v-e2e12602] {
width: 20rem;
max-height: 8rem;
-o-object-fit: contain;
object-fit: contain;
margin: 0 .5rem 0 0;
}
.sites[data-v-e2e12602] {
display: grid; display: grid;
grid-gap: 1rem; grid-gap: 1rem;
margin: 0 0 2rem 0; padding: 1rem;
margin: 0;
grid-template-columns: 1fr;
overflow-y: auto;
} }
.sites[data-v-e2e12602] { .sites.compact[data-v-7bebaa3e] {
grid-template-columns: repeat(auto-fit, 15rem); 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: flex;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
flex-direction: row;
-webkit-box-flex: 1;
flex-grow: 1;
-webkit-box-pack: stretch;
justify-content: stretch;
overflow-y: auto;
}
.sidebar[data-v-e2e12602] {
height: 100%;
width: 18rem;
display: -webkit-box;
display: flex;
-webkit-box-orient: vertical;
-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] {
border-bottom: solid 1px rgba(0, 0, 0, 0.1);
}
.logo[data-v-e2e12602] {
width: 100%;
max-height: 8rem;
display: -webkit-box;
display: flex;
-webkit-box-pack: center;
justify-content: center;
-o-object-fit: contain;
object-fit: contain;
box-sizing: border-box;
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] {
display: none;
}
.header[data-v-e2e12602] {
width: 100%;
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) {
.sites[data-v-e2e12602] { .header[data-v-e2e12602] {
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); display: -webkit-box;
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.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 845 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 110 KiB

BIN
public/img/tags/anal/0.jpeg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
public/img/tags/anal/0_thumb.jpeg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 684 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 800 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 782 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 137 KiB

BIN
public/img/tags/gangbang/0.jpeg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -125,10 +125,4 @@ const networks = [
]; ];
exports.seed = knex => Promise.resolve() exports.seed = knex => Promise.resolve()
.then(async () => { .then(async () => upsert('networks', networks, 'slug', knex));
// 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,15 +2428,10 @@ 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 [duplicates, networks] = await Promise.all([ const networks = await knex('networks').select('*');
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, duplicatesBySlug, 'slug', knex); return upsert('sites', sites, 'slug', knex);
}); });

View File

@ -1,5 +1,3 @@
'use strict';
const upsert = require('../src/utils/upsert'); const upsert = require('../src/utils/upsert');
function getStudios(networksMap) { function getStudios(networksMap) {
@ -9,133 +7,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,
}, },
]; ];
} }
@ -143,15 +141,10 @@ 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 [duplicates, networks] = await Promise.all([ const networks = await knex('networks').select('*');
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, duplicatesBySlug, 'slug', knex); return upsert('studios', studios, 'slug', knex);
}); });

View File

@ -33,6 +33,10 @@ const groups = [
slug: 'location', slug: 'location',
name: 'Location', name: 'Location',
}, },
{
slug: 'oral',
name: 'Oral',
},
{ {
slug: 'orientation', slug: 'orientation',
name: 'Orientation', name: 'Orientation',
@ -69,7 +73,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).', 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 */
priority: 9, priority: 9,
group_id: groupsMap.penetration, group_id: groupsMap.penetration,
}, },
@ -136,16 +140,19 @@ 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',
@ -211,6 +218,7 @@ 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',
@ -319,6 +327,7 @@ 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',
@ -345,11 +354,13 @@ 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',
@ -379,7 +390,7 @@ function getTags(groupsMap) {
slug: 'facefuck', slug: 'facefuck',
priority: 9, priority: 9,
alias_for: null, alias_for: null,
group_id: groupsMap.position, group_id: groupsMap.oral,
}, },
{ {
name: 'facesitting', name: 'facesitting',
@ -429,7 +440,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.', 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 */
alias_for: null, alias_for: null,
priority: 9, priority: 9,
group_id: groupsMap.group, group_id: groupsMap.group,
@ -645,6 +656,7 @@ 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',
@ -1542,35 +1554,20 @@ 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 duplicates = await knex('tags_groups').select('*'); const groupEntries = 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, duplicatesBySlug, 'slug', knex); return upsert('tags', tags, 'slug', knex);
}) })
.then(async () => { .then(async () => {
const [duplicates, tags] = await Promise.all([ const tags = await knex('tags').select('*').where({ alias_for: null });
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, duplicatesByName, 'name', knex); return upsert('tags', tagAliases, 'name', knex);
}); });

View File

@ -1,256 +1,296 @@
const upsert = require('../src/utils/upsert'); const upsert = require('../src/utils/upsert');
function getMedia(tagsMap) { const tagPosters = [
return [ {
{ path: 'tags/airtight/poster.jpeg',
path: 'tags/airtight/poster.jpeg', tagSlug: 'airtight',
target_id: tagsMap.airtight, comment: 'Jynx Maze in "Pump My Ass Full of Cum 3" for Jules Jordan',
role: 'poster', },
comment: 'Jynx Maze in "Pump My Ass Full of Cum 3" for Jules Jordan', {
}, path: 'tags/anal/poster.jpeg',
{ tagSlug: 'anal',
path: 'tags/airtight/2.jpeg', comment: 'Jynx Maze in "Anal Buffet 6" for Evil Angel',
target_id: tagsMap.airtight, },
comment: 'Dakota Skye in "Dakota Goes Nuts" for ArchAngel', {
}, path: 'tags/ass-to-mouth/poster.jpeg',
{ tagSlug: 'ass-to-mouth',
path: 'tags/airtight/1.jpeg', comment: 'Alysa Gap and Logan in "Anal Buffet 4" for Evil Angel',
target_id: tagsMap.airtight, },
comment: 'Chloe Amour in "DP Masters 4" for Jules Jordan', {
}, path: 'tags/gapes/poster.jpeg',
{ tagSlug: 'gapes',
path: 'tags/airtight/0/poster.jpeg', comment: 'Paulina in "Anal Buffet 4" for Evil Angel',
domain: 'tags', },
target_id: tagsMap.airtight, {
comment: 'Sheena Shaw in "Ass Worship 14" for Jules Jordan', path: 'tags/da-tp/0.jpeg',
}, tagSlug: 'da-tp',
{ comment: 'Natasha Teen in LegalPorno SZ2164',
path: 'tags/anal/poster.jpeg', },
target_id: tagsMap.anal, {
role: 'poster', path: 'tags/double-penetration/poster.jpeg',
comment: '', tagSlug: 'double-penetration',
}, comment: 'Mia Malkova in "DP!" for HardX',
{ },
path: 'tags/double-penetration/poster.jpeg', {
target_id: tagsMap['double-penetration'], path: 'tags/double-anal/poster.jpeg',
role: 'poster', tagSlug: 'double-anal',
comment: '', comment: 'Haley Reed in "Young Hot Ass" for Evil Angel',
}, },
{ {
path: 'tags/double-anal/poster.jpeg', path: 'tags/double-vaginal/poster.jpeg',
target_id: tagsMap['double-anal'], tagSlug: 'double-vaginal',
role: 'poster', comment: '',
comment: '', },
}, {
{ path: 'tags/dv-tp/poster.jpeg',
path: 'tags/double-vaginal/poster.jpeg', tagSlug: 'dv-tp',
target_id: tagsMap['double-vaginal'], comment: 'Juelz Ventura in "Gangbanged 5" for Elegant Angel',
role: 'poster', },
comment: '', {
}, path: 'tags/tattoo/poster.jpeg',
{ tagSlug: 'tattoo',
path: 'tags/da-tp/0.jpeg', comment: 'Kali Roses in "Goes All In For Anal" for Hussie Pass',
target_id: tagsMap['da-tp'], },
role: 'poster', {
comment: 'Natasha Teen in LegalPorno SZ2164', path: 'tags/triple-anal/poster.jpeg',
}, tagSlug: 'triple-anal',
{ comment: 'Kristy Black in SZ1986 for LegalPorno',
path: 'tags/da-tp/3.jpeg', },
target_id: tagsMap['da-tp'], {
role: 'photo', path: 'tags/blowbang/poster.jpeg',
comment: 'Evelina Darling in GIO294', tagSlug: 'blowbang',
}, comment: '',
{ },
path: 'tags/da-tp/1.jpeg', {
target_id: tagsMap['da-tp'], path: 'tags/gangbang/poster.jpeg',
role: 'photo', tagSlug: 'gangbang',
comment: 'Francys Belle in SZ1702 for LegalPorno', comment: 'Kristen Scott in "Interracial Gangbang!" for Jules Jordan',
}, },
{ {
path: 'tags/da-tp/2.jpeg', path: 'tags/mff/poster.jpeg',
target_id: tagsMap['da-tp'], tagSlug: 'mff',
role: 'photo', comment: '',
comment: 'Angel Smalls in GIO408 for LegalPorno', },
}, {
{ path: 'tags/mfm/poster.jpeg',
path: 'tags/da-tp/4.jpeg', tagSlug: 'mfm',
target_id: tagsMap['da-tp'], comment: '',
role: 'photo', },
comment: 'Ninel Mojado aka Mira Cuckold in GIO063 for LegalPorno', {
}, path: 'tags/orgy/poster.jpeg',
{ tagSlug: 'orgy',
path: 'tags/dv-tp/poster.jpeg', comment: '',
target_id: tagsMap['dv-tp'], },
role: 'poster', {
comment: 'Juelz Ventura in "Gangbanged 5" for Elegant Angel', path: 'tags/asian/poster.jpeg',
}, tagSlug: 'asian',
{ comment: 'Vina Sky in "Young and Glamorous 10" for Jules Jordan',
path: 'tags/dv-tp/0.jpeg', },
target_id: tagsMap['dv-tp'], {
role: 'photo', path: 'tags/caucasian/poster.jpeg',
comment: 'Luna Rival in LegalPorno SZ1490', tagSlug: 'caucasian',
}, comment: '',
{ },
path: 'tags/tattoo/poster.jpeg', {
target_id: tagsMap.tattoo, path: 'tags/ebony/poster.jpeg',
role: 'poster', tagSlug: 'ebony',
comment: 'Kali Roses in "Goes All In For Anal" for Hussie Pass', comment: '',
}, },
{ {
path: 'tags/triple-anal/poster.jpeg', path: 'tags/latina/poster.jpeg',
target_id: tagsMap['triple-anal'], tagSlug: 'latina',
role: 'poster', comment: '',
comment: 'Kristy Black in SZ1986 for LegalPorno', },
}, {
{ path: 'tags/interracial/poster.jpeg',
path: 'tags/triple-anal/1.jpeg', tagSlug: 'interracial',
target_id: tagsMap['triple-anal'], comment: '',
role: 'photo', },
comment: 'Natasha Teen in SZ2098 for LegalPorno', {
}, path: 'tags/facial/poster.jpeg',
{ tagSlug: 'facial',
path: 'tags/triple-anal/2.jpeg', comment: '',
target_id: tagsMap['triple-anal'], },
role: 'photo', {
comment: 'Kira Thorn in GIO1018 for LegalPorno', path: 'tags/trainbang/poster.jpeg',
}, tagSlug: 'trainbang',
{ comment: 'Kali Roses in "Passing Me Around" for Blacked',
path: 'tags/blowbang/poster.jpeg', },
target_id: tagsMap.blowbang, {
role: 'poster', path: 'tags/bukkake/poster.jpeg',
comment: '', tagSlug: 'bukkake',
}, comment: '',
{ },
path: 'tags/gangbang/poster.jpeg', {
target_id: tagsMap.gangbang, path: 'tags/swallowing/poster.jpeg',
role: 'poster', tagSlug: 'swallowing',
comment: '', comment: '',
}, },
{ {
path: 'tags/gangbang/1.jpeg', path: 'tags/creampie/poster.jpeg',
target_id: tagsMap.gangbang, tagSlug: 'creampie',
role: 'photo', comment: '',
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',
path: 'tags/gangbang/2.jpeg', tagSlug: 'anal-creampie',
target_id: tagsMap.gangbang, comment: '',
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',
path: 'tags/gangbang/3.jpeg', comment: '',
target_id: tagsMap.gangbang, },
role: 'photo', ]
comment: 'Kelsi Monroe in "Brazzers House 2, Day 2" for Brazzers', .map((file, index) => ({
}, ...file,
{ thumbnail: file.thumbnail || file.path.replace('.jpeg', '_thumb.jpeg'),
path: 'tags/mff/poster.jpeg', mime: 'image/jpeg',
target_id: tagsMap.mff, index,
role: 'poster', }));
comment: '',
}, const tagPhotos = [
{ {
path: 'tags/mfm/poster.jpeg', path: 'tags/airtight/3.jpeg',
target_id: tagsMap.mfm, tagSlug: 'airtight',
role: 'poster', comment: 'Anita Bellini in "Triple Dick Gangbang" for Hands On Hardcore (DDF Network)',
comment: '', },
}, {
{ path: 'tags/airtight/2.jpeg',
path: 'tags/orgy/poster.jpeg', tagSlug: 'airtight',
target_id: tagsMap.orgy, comment: 'Dakota Skye in "Dakota Goes Nuts" for ArchAngel',
role: 'poster', },
comment: '', {
}, path: 'tags/airtight/1.jpeg',
{ tagSlug: 'airtight',
path: 'tags/asian/poster.jpeg', comment: 'Chloe Amour in "DP Masters 4" for Jules Jordan',
target_id: tagsMap.asian, },
role: 'poster', {
comment: '', path: 'tags/airtight/0.jpeg',
}, domain: 'tags',
{ tagSlug: 'airtight',
path: 'tags/caucasian/poster.jpeg', comment: 'Sheena Shaw in "Ass Worship 14" for Jules Jordan',
target_id: tagsMap.caucasian, },
role: 'poster', {
comment: '', path: 'tags/anal/0.jpeg',
}, tagSlug: 'anal',
{ comment: '',
path: 'tags/ebony/poster.jpeg', },
target_id: tagsMap.ebony, {
role: 'poster', path: 'tags/double-anal/1.jpeg',
comment: '', tagSlug: 'double-anal',
}, comment: 'Ria Sunn in SZ1801 for LegalPorno',
{ },
path: 'tags/latina/poster.jpeg', {
target_id: tagsMap.latina, path: 'tags/double-anal/0.jpeg',
role: 'poster', tagSlug: 'double-anal',
comment: '', comment: 'Nicole Black doing double anal during a gangbang in GIO971 for LegalPorno',
}, },
{ {
path: 'tags/interracial/poster.jpeg', path: 'tags/da-tp/3.jpeg',
target_id: tagsMap.interracial, tagSlug: 'da-tp',
role: 'poster', comment: 'Evelina Darling in GIO294',
comment: '', },
}, {
{ path: 'tags/da-tp/1.jpeg',
path: 'tags/facial/poster.jpeg', tagSlug: 'da-tp',
target_id: tagsMap.facial, comment: 'Francys Belle in SZ1702 for LegalPorno',
role: 'poster', },
comment: '', {
}, path: 'tags/da-tp/2.jpeg',
{ tagSlug: 'da-tp',
path: 'tags/bukkake/poster.jpeg', comment: 'Angel Smalls in GIO408 for LegalPorno',
target_id: tagsMap.bukkake, },
role: 'poster', {
comment: '', path: 'tags/da-tp/4.jpeg',
}, tagSlug: 'da-tp',
{ comment: 'Ninel Mojado aka Mira Cuckold in GIO063 for LegalPorno',
path: 'tags/swallowing/poster.jpeg', },
target_id: tagsMap.swallowing, {
role: 'poster', path: 'tags/dv-tp/0.jpeg',
comment: '', tagSlug: 'dv-tp',
}, comment: 'Luna Rival in LegalPorno SZ1490',
{ },
path: 'tags/creampie/poster.jpeg', {
target_id: tagsMap.creampie, path: 'tags/double-penetration/0.jpeg',
role: 'poster', tagSlug: 'double-penetration',
comment: '', comment: '',
}, },
{ {
path: 'tags/anal-creampie/poster.jpeg', path: 'tags/gapes/0.jpeg',
target_id: tagsMap['anal-creampie'], tagSlug: 'gapes',
role: 'poster', comment: 'McKenzee Miles in "Anal Buffet 4" for Evil Angel',
comment: '', },
}, {
{ path: 'tags/trainbang/0.jpeg',
path: 'tags/oral-creampie/poster.jpeg', tagSlug: 'trainbang',
target_id: tagsMap['oral-creampie'], comment: 'Nicole Black in GIO971 for LegalPorno',
role: 'poster', },
comment: '', {
}, path: 'tags/triple-anal/1.jpeg',
] tagSlug: 'triple-anal',
.map((file, index) => ({ comment: 'Natasha Teen in SZ2098 for LegalPorno',
...file, },
thumbnail: file.thumbnail || file.path.replace('.jpeg', '_thumb.jpeg'), {
mime: 'image/jpeg', path: 'tags/triple-anal/2.jpeg',
index, tagSlug: 'triple-anal',
domain: file.domain || 'tags', comment: 'Kira Thorn in GIO1018 for LegalPorno',
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 [duplicates, tags] = await Promise.all([ const tagMedia = tagPosters.concat(tagPhotos);
knex('media').where('domain', 'tags'),
knex('tags').where('alias_for', null), const tags = await knex('tags').whereIn('slug', tagMedia.map(item => item.tagSlug));
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,9 +1755,4 @@ const countries = [
]; ];
exports.seed = knex => knex('countries') exports.seed = knex => knex('countries')
.then(async () => { .then(async () => upsert('countries', countries, 'alpha2', knex));
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('social') knex('actors_social')
.where({ domain: 'actors', target_id: actor.id }) .where('actor_id', actor.id)
.orderBy('platform', 'desc'), .orderBy('platform', 'desc'),
]); ]);
@ -197,8 +197,7 @@ function curateSocialEntry(url, actorId) {
return { return {
url: match.url, url: match.url,
platform: match.platform, platform: match.platform,
domain: 'actors', actor_id: actorId,
target_id: actorId,
}; };
} }
@ -207,10 +206,7 @@ async function curateSocialEntries(urls, actorId) {
return []; return [];
} }
const existingSocialLinks = await knex('social').where({ const existingSocialLinks = await knex('actors_social').where('actor_id', actorId);
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);
@ -243,7 +239,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('social').insert(curatedSocialEntries); await knex('actors_social').insert(curatedSocialEntries);
} }
async function storeActor(actor, scraped = false, scrapeSuccess = false) { async function storeActor(actor, scraped = false, scrapeSuccess = false) {
@ -358,7 +354,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: 'actors', domain: 'actor',
role: 'photo', role: 'photo',
primaryRole: 'avatar', primaryRole: 'avatar',
targetId: actorEntry.id, targetId: actorEntry.id,
@ -374,7 +370,7 @@ async function scrapeActors(actorNames) {
await createMediaDirectory('actors', `${newActorEntry.slug}/`); await createMediaDirectory('actors', `${newActorEntry.slug}/`);
await storePhotos(profile.avatars, { await storePhotos(profile.avatars, {
domain: 'actors', domain: 'actor',
role: 'photo', role: 'photo',
primaryRole: 'avatar', primaryRole: 'avatar',
targetId: newActorEntry.id, targetId: newActorEntry.id,
@ -399,7 +395,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('actors_associated').whereIn('release_id', releases.map(release => release.id)), knex('releases_actors').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]) => {
@ -418,7 +414,7 @@ async function associateActors(mappedActors, releases) {
}); });
await Promise.all([ await Promise.all([
knex('actors_associated').insert(associations.flat()), knex('releases_actors').insert(associations.flat()),
scrapeBasicActors(), scrapeBasicActors(),
]); ]);
} }

View File

@ -1,26 +1,20 @@
'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 scrapeRelease = require('./scrape-release'); const { scrapeReleases } = require('./scrape-releases');
const { scrapeActors, scrapeBasicActors } = require('./actors'); const { scrapeActors, scrapeBasicActors } = require('./actors');
async function init() { async function init() {
if (argv.scene) { if (argv.scene) {
await Promise.map(argv.scene, async url => scrapeRelease(url, null, false, false), { await scrapeReleases(argv.scene, null, 'scene');
concurrency: 5,
});
} }
if (argv.movie) { if (argv.movie) {
await Promise.map(argv.movie, async url => scrapeRelease(url, null, false, true), { await scrapeReleases(argv.movie, null, 'movie');
concurrency: 5,
});
} }
if (argv.scrape || argv.networks || argv.sites) { if (argv.scrape || argv.networks || argv.sites) {

View File

@ -10,6 +10,7 @@ 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 });
@ -40,6 +41,9 @@ async function createThumbnail(buffer) {
height: config.media.thumbnailSize, height: config.media.thumbnailSize,
withoutEnlargement: true, withoutEnlargement: true,
}) })
.jpeg({
quality: 75,
})
.toBuffer(); .toBuffer();
} }
@ -50,7 +54,7 @@ async function createMediaDirectory(domain, subpath) {
return filepath; return filepath;
} }
function curatePhotoEntries(files, domain = 'releases', role = 'photo', targetId) { function curatePhotoEntries(files) {
return files.map((file, index) => ({ return files.map((file, index) => ({
path: file.filepath, path: file.filepath,
thumbnail: file.thumbpath, thumbnail: file.thumbpath,
@ -58,51 +62,33 @@ function curatePhotoEntries(files, domain = 'releases', role = 'photo', targetId
hash: file.hash, hash: file.hash,
source: file.source, source: file.source,
index, index,
domain,
target_id: targetId,
role: file.role || role,
})); }));
} }
// before fetching async function findDuplicates(photos, identifier, prop = null, label) {
async function filterSourceDuplicates(photos, domains = ['releases'], roles = ['photo'], identifier) { const duplicates = await knex('media')
const photoSourceEntries = await knex('media') .whereIn(identifier, photos.flat().map(photo => (prop ? photo[prop] : photo)));
.whereIn('source', photos.flat())
.whereIn('domain', domains)
.whereIn('role', roles); // accept string argument
const photoSources = new Set(photoSourceEntries.map(photo => photo.source)); const duplicateLookup = new Set(duplicates.map(photo => photo[prop || identifier]));
const newPhotos = photos.filter(source => (Array.isArray(source) // fallbacks provided? const originals = photos.filter(source => (Array.isArray(source) // fallbacks provided?
? !source.some(sourceX => photoSources.has(sourceX)) // ensure none of the sources match ? !source.some(sourceX => duplicateLookup.has(prop ? sourceX[prop] : sourceX)) // ensure none of the sources match
: !photoSources.has(source))); : !duplicateLookup.has(prop ? source[prop] : source)));
if (photoSourceEntries.length > 0) { if (duplicates.length > 0) {
console.log(`Ignoring ${photoSourceEntries.length} ${roles} items already present by source for ${identifier}`); console.log(`${duplicates.length} media items already present by ${identifier} for ${label}`);
} }
return newPhotos; if (originals.length > 0) {
} console.log(`Fetching ${originals.length} new media items for ${label}`);
// 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)); return [duplicates, originals];
} }
async function fetchPhoto(photoUrl, index, identifier, attempt = 1) { async function fetchPhoto(photoUrl, index, label, 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, identifier); const photo = await fetchPhoto(url, index, label);
if (photo) { if (photo) {
return photo; return photo;
@ -133,11 +119,11 @@ async function fetchPhoto(photoUrl, index, identifier, 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 ${identifier} (${photoUrl}): ${error}`); console.warn(`Failed attempt ${attempt}/3 to fetch photo ${index + 1} for ${label} (${photoUrl}): ${error}`);
if (attempt < 3) { if (attempt < 3) {
await Promise.delay(1000); await Promise.delay(1000);
return fetchPhoto(photoUrl, index, identifier, attempt + 1); return fetchPhoto(photoUrl, index, label, attempt + 1);
} }
return null; return null;
@ -145,7 +131,7 @@ async function fetchPhoto(photoUrl, index, identifier, attempt = 1) {
} }
async function savePhotos(files, { async function savePhotos(files, {
domain = 'releases', domain = 'release',
subpath, subpath,
role = 'photo', role = 'photo',
naming = 'index', naming = 'index',
@ -155,11 +141,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, subpath, `${filename}.${file.extension}`); const filepath = path.join(`${domain}s`, subpath, `${filename}.${file.extension}`);
const thumbpath = path.join(domain, subpath, `${filename}_thumb.${file.extension}`); const thumbpath = path.join(`${domain}s`, 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),
@ -176,49 +162,28 @@ async function savePhotos(files, {
} }
async function storePhotos(photos, { async function storePhotos(photos, {
domain = 'releases', domain = 'release',
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
}, identifier) { }, label) {
if (!photos || photos.length === 0) { if (!photos || photos.length === 0) {
console.warn(`No ${role}s available for ${identifier}`); console.warn(`No ${role}s available for ${label}`);
return; return;
} }
const pluckedPhotos = pluckPhotos(photos); const pluckedPhotos = pluckPhotos(photos);
const roles = primaryRole ? [role, primaryRole] : [role]; const [sourceDuplicates, sourceOriginals] = await findDuplicates(pluckedPhotos, 'source', null, label);
const newPhotos = await filterSourceDuplicates(pluckedPhotos, [domain], roles, identifier); const metaFiles = await Promise.map(sourceOriginals, async (photoUrl, index) => fetchPhoto(photoUrl, index, label), {
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 [uniquePhotos, primaryPhoto] = await Promise.all([ const [hashDuplicates, hashOriginals] = await findDuplicates(metaFiles, 'hash', 'hash', label);
filterHashDuplicates(metaFiles, [domain], roles, identifier),
primaryRole
? await knex('media')
.where('domain', domain)
.where('target_id', targetId)
.where('role', primaryRole)
.first()
: null,
]);
if (primaryRole && !primaryPhoto) { const savedPhotos = await savePhotos(hashOriginals, {
console.log(`Setting first photo as ${primaryRole} for ${identifier}`);
uniquePhotos[0].role = primaryRole;
}
const savedPhotos = await savePhotos(uniquePhotos, {
domain, domain,
role, role,
targetId, targetId,
@ -228,59 +193,102 @@ async function storePhotos(photos, {
const curatedPhotoEntries = curatePhotoEntries(savedPhotos, domain, role, targetId); const curatedPhotoEntries = curatePhotoEntries(savedPhotos, domain, role, targetId);
await knex('media').insert(curatedPhotoEntries); const newPhotos = await knex('media').insert(curatedPhotoEntries).returning('*');
const photoEntries = Array.isArray(newPhotos)
? [...sourceDuplicates, ...hashDuplicates, ...newPhotos]
: [...sourceDuplicates, ...hashDuplicates];
console.log(`Stored ${newPhotos.length} ${role}s for ${identifier}`); const photoAssociations = photoEntries
.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,
}, identifier) { }, label) {
// 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 ${identifier}`); console.warn(`No trailer available for ${label}`);
return; return;
} }
console.log(`Storing trailer for ${identifier}`); const [sourceDuplicates, sourceOriginals] = await findDuplicates([trailer], 'source', 'src', label);
const { pathname } = new URL(trailer.src); const metaFiles = await Promise.map(sourceOriginals, async (trailerX) => {
const mimetype = trailer.type || mime.getType(pathname); const { pathname } = new URL(trailerX.src);
const mimetype = trailerX.type || mime.getType(pathname);
const res = await bhttp.get(trailer.src); const res = await bhttp.get(trailerX.src);
const filepath = path.join('releases', subpath, `trailer${trailer.quality ? `_${trailer.quality}` : ''}.${mime.getExtension(mimetype)}`); const hash = getHash(res.body);
const filepath = path.join(domain, subpath, `trailer${trailerX.quality ? `_${trailerX.quality}` : ''}.${mime.getExtension(mimetype)}`);
await Promise.all([ return {
fs.writeFile(path.join(config.media.path, filepath), res.body), trailer: res.body,
knex('media').insert({
path: filepath, path: filepath,
mime: mimetype, mime: mimetype,
source: trailer.src, source: trailerX.src,
domain, quality: trailerX.quality || null,
target_id: targetId, hash,
role, };
quality: trailer.quality || null, });
}),
]);
}
async function findAvatar(actorId, domain = 'actors') { const [hashDuplicates, hashOriginals] = await findDuplicates(metaFiles, 'hash', 'hash', label);
return knex('media')
.where('domain', domain) const newTrailers = await knex('media')
.where('target_id', actorId) .insert(hashOriginals.map(trailerX => ({
.where('role', 'avatar'); path: trailerX.path,
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,6 +15,40 @@ 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')
@ -49,8 +83,9 @@ async function curateRelease(release) {
.orderBy(['role', 'index']), .orderBy(['role', 'index']),
]); ]);
return { const curatedRelease = {
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,
@ -108,33 +143,51 @@ 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 getChannelSite(release) { async function attachChannelSite(release) {
try { if (!release.site.isFallback) {
const site = await findSiteByUrl(release.channel); return release;
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 curateScrapedRelease(release) { async function curateReleaseEntry(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,
@ -147,52 +200,9 @@ async function curateScrapedRelease(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)
@ -244,6 +254,40 @@ 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})`;
@ -279,7 +323,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 curateScrapedRelease(release); const curatedRelease = await curateReleaseEntry(release);
if (existingRelease && !argv.redownload) { if (existingRelease && !argv.redownload) {
return existingRelease.id; return existingRelease.id;
@ -317,18 +361,13 @@ 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 releaseId = await storeRelease(release); const releaseWithChannelSite = await attachChannelSite(release);
const releaseId = await storeRelease(releaseWithChannelSite);
return { return {
id: releaseId, id: releaseId,
...release, ...releaseWithChannelSite,
}; };
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -339,22 +378,8 @@ async function storeReleases(releases) {
concurrency: 10, concurrency: 10,
}).filter(release => release); }).filter(release => release);
const actors = storedReleases.reduce((acc, release) => { const actors = accumulateActors(storedReleases);
if (!release.actors) return acc; const movies = accumulateMovies(storedReleases);
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),
@ -363,7 +388,11 @@ async function storeReleases(releases) {
}), }),
]); ]);
return storedReleases; return {
releases: storedReleases,
actors,
movies,
};
} }
module.exports = { module.exports = {

View File

@ -1,68 +0,0 @@
'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;

90
src/scrape-releases.js Normal file
View File

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

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 (index) => { const photoUrls = await Promise.map(Array.from({ length: lastPhotoIndex }), async (value, 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,7 +50,9 @@ 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]) => ({ ...acc, [key.trim()]: value.trim() }), {}); const bio = entries.reduce((acc, [key, value]) => (key ? { ...acc, [key.trim()]: value.trim() } : acc), {});
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,20 +66,16 @@ 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('tags_associated') const associationEntries = await knex('releases_tags')
.where({ .where('release_id', releaseId)
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('tags_associated').insert(newAssociations.map(tagId => ({ await knex('releases_tags').insert(newAssociations.map(tagId => ({
tag_id: tagId, tag_id: tagId,
domain: 'releases', release_id: releaseId,
target_id: releaseId,
}))); })));
} }

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