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

View File

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

View File

@ -1,20 +1,6 @@
<template>
<div class="filter-bar noselect">
<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">
<input
:id="`${_uid}-new`"
@ -42,6 +28,20 @@
class="range-button"
>Upcoming</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>

View File

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

View File

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

View File

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

View File

@ -56,6 +56,7 @@ export default {
}
.tiles {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, .33fr));
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
:src="`/img/${tag.poster.thumbnail}`"
:alt="tag.poster.comment"
class="poster"
>
</a>
@ -43,6 +44,7 @@
>
<img
:src="`/img/${photo.thumbnail}`"
:alt="photo.comment"
class="photo"
>
</a>
@ -50,7 +52,7 @@
</div>
<div class="content-inner">
<Releases :releases="releases" />
<Releases :releases="tag.releases" />
</div>
</div>
</div>
@ -68,17 +70,13 @@ import Releases from '../releases/releases.vue';
const converter = new Converter();
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() {
[this.tag] = await Promise.all([
this.$store.dispatch('fetchTags', { tagId: this.$route.params.tagSlug }),
this.fetchReleases(),
]);
this.description = converter.makeHtml(escapeHtml(this.tag.description));
this.tag = await this.$store.dispatch('fetchTags', { tagSlug: this.$route.params.tagSlug });
this.description = this.tag.description && converter.makeHtml(escapeHtml(this.tag.description));
this.pageTitle = this.tag.name;
}
@ -90,6 +88,7 @@ export default {
data() {
return {
tag: null,
description: null,
releases: null,
pageTitle: null,
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,8 @@ $highlight-strong: rgba(255, 255, 255, .7);
$highlight-weak: rgba(255, 255, 255, .2);
$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;

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) {
async function fetchActors({ _commit }, { actorId, limit = 100 }) {
if (actorId) {
return get(`/actors/${actorId}`, { limit });
async function fetchActorBySlug(actorSlug, limit = 100) {
const { actor } = await graphql(`
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) {

View File

@ -21,7 +21,7 @@ async function get(endpoint, query = {}) {
async function post(endpoint, data) {
const res = await fetch(`${config.api.url}${endpoint}`, {
method: 'GET',
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
@ -39,4 +39,33 @@ async function post(endpoint, data) {
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) {
async function fetchNetworks({ _commit }, networkId) {
const networks = await get(`/networks/${networkId || ''}`, {
});
return networks;
}
async function fetchNetworkReleases({ _commit }, networkId) {
const releases = await get(`/networks/${networkId}/releases`, {
filter: store.state.ui.filter,
async function fetchNetworkBySlug(networkSlug, limit = 100) {
const { network } = await graphql(`
query Network(
$networkSlug: String!
$limit:Int = 1000,
$after:Date = "1900-01-01",
$before:Date = "2100-01-01",
) {
network: networkBySlug(slug: $networkSlug) {
id
name
slug
url
sites {
id
name
slug
url
${releasesFragment}
network {
id
name
slug
url
}
}
}
}
`, {
networkSlug,
limit,
after: store.getters.after,
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 {
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) {
async function fetchReleases({ _commit }) {
const releases = await get('/releases', {
filter: store.state.ui.filter,
async function fetchReleases({ _commit }, { limit = 100 }) {
console.log(store.state.ui.filter, store.getters.after, store.getters.before);
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,
before: store.getters.before,
});
return releases;
return releases.map(release => curateRelease(release));
}
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 {

View File

@ -3,7 +3,7 @@ import VueRouter from 'vue-router';
import Home from '../components/home/home.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 Networks from '../components/networks/networks.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) {
async function fetchSites({ _commit }, siteId) {
const sites = await get(`/sites/${siteId || ''}`);
async function fetchSiteBySlug(siteSlug, limit = 100) {
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;
}
/*
async function fetchSiteReleases({ _commit }, siteId) {
const releases = await get(`/sites/${siteId}/releases`, {
filter: store.state.ui.filter,
@ -16,10 +73,11 @@ function initSitesActions(store, _router) {
return releases;
}
*/
return {
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) {
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 }, {
tagId,
tagSlug,
limit = 100,
slug,
group,
priority,
slugs = [],
_group,
_priority,
}) {
if (tagId) {
return get(`/tags/${tagId}`);
if (tagSlug) {
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,
slug,
priority,
group,
});
return tags.map(tag => curateTag(tag));
}
async function fetchTagReleases({ _commit }, tagId) {
@ -33,6 +107,7 @@ function initTagsActions(store, _router) {
return {
fetchTags,
fetchTagReleases,
fetchTagBySlug,
};
}

View File

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

View File

@ -17,6 +17,184 @@ exports.up = knex => Promise.resolve()
table.integer('priority', 2)
.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) => {
table.increments('id', 12);
@ -70,6 +248,48 @@ exports.up = knex => Promise.resolve()
table.datetime('scraped_at');
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) => {
table.increments('id', 12);
@ -84,92 +304,6 @@ exports.up = knex => Promise.resolve()
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('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) => {
table.increments('id', 16);
@ -193,14 +327,10 @@ exports.up = knex => Promise.resolve()
table.date('date');
table.text('description');
table.integer('director', 12)
.references('id')
.inTable('directors');
table.integer('duration')
.unsigned();
table.integer('parent', 16)
table.integer('parent_id', 16)
.references('id')
.inTable('releases');
@ -209,46 +339,7 @@ exports.up = knex => Promise.resolve()
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('media', (table) => {
table.increments('id', 16);
table.string('path');
table.string('thumbnail');
table.integer('index');
table.string('mime');
table.string('domain');
table.integer('target_id', 16);
table.string('role');
table.string('quality', 6);
table.string('hash');
table.text('comment');
table.string('source', 1000);
table.unique(['domain', 'target_id', 'role', 'hash']);
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('social', (table) => {
table.increments('id', 16);
table.string('url');
table.string('platform');
table.string('domain');
table.integer('target_id', 16);
table.unique(['url', 'domain', 'target_id']);
table.datetime('created_at')
.defaultTo(knex.fn.now());
}))
.then(() => knex.schema.createTable('actors_associated', (table) => {
table.increments('id', 16);
.then(() => knex.schema.createTable('releases_actors', (table) => {
table.integer('release_id', 16)
.notNullable()
.references('id')
@ -261,9 +352,7 @@ exports.up = knex => Promise.resolve()
table.unique(['release_id', 'actor_id']);
}))
.then(() => knex.schema.createTable('directors_associated', (table) => {
table.increments('id', 16);
.then(() => knex.schema.createTable('releases_directors', (table) => {
table.integer('release_id', 16)
.notNullable()
.references('id')
@ -276,30 +365,131 @@ exports.up = knex => Promise.resolve()
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)
.notNullable()
.references('id')
.inTable('tags');
table.string('domain');
table.integer('target_id', 16);
table.integer('release_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'))
.then(() => knex.schema.dropTable('directors_associated'))
.then(() => knex.schema.dropTable('actors_associated'))
.then(() => knex.schema.dropTable('tags'))
.then(() => knex.schema.dropTable('tags_groups'))
.then(() => knex.schema.dropTable('media'))
.then(() => knex.schema.dropTable('social'))
.then(() => knex.schema.dropTable('actors'))
.then(() => knex.schema.dropTable('releases'))
.then(() => knex.schema.dropTable('sites'))
.then(() => knex.schema.dropTable('studios'))
.then(() => knex.schema.dropTable('directors'))
.then(() => knex.schema.dropTable('networks'))
.then(() => knex.schema.dropTable('countries'));
/*
CREATE VIEW releases_actors_sortable AS
SELECT releases_actors.*, actors.gender, actors.name, actors.birthdate FROM releases_actors
JOIN actors ON releases_actors.actor_id = actors.id;
CREATE VIEW releases_tags_sortable AS
SELECT releases_tags.*, tags.name, tags.priority FROM releases_tags
JOIN tags ON releases_tags.tag_id = tags.id;
CREATE VIEW actors_releases_sortable AS
SELECT releases_actors.*, releases.date FROM releases_actors
JOIN releases ON releases_actors.release_id = releases.id;
COMMENT ON VIEW releases_actors_sortable IS E'@foreignKey (release_id) references releases (id)\n@foreignKey (actor_id) references actors (id)';
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",
"version": "1.43.1",
"version": "1.44.0",
"description": "All the latest porn releases in one place",
"main": "src/app.js",
"scripts": {
@ -39,12 +39,11 @@
"@babel/core": "^7.7.5",
"@babel/plugin-proposal-optional-chaining": "^7.7.5",
"@babel/preset-env": "^7.7.6",
"@babel/register": "^7.7.4",
"autoprefixer": "^9.7.3",
"babel-cli": "^6.26.0",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"babel-preset-airbnb": "^3.3.2",
"babel-register": "^6.26.0",
"css-loader": "^2.1.1",
"eslint": "^5.16.0",
"eslint-config-airbnb": "^17.1.1",
@ -67,6 +66,8 @@
"webpack-cli": "^3.3.10"
},
"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",
"babel-polyfill": "^6.26.0",
"bhttp": "^1.2.4",
@ -83,6 +84,7 @@
"express-react-views": "^0.11.0",
"face-api.js": "^0.21.0",
"fs-extra": "^7.0.1",
"graphile-utils": "^4.5.6",
"jsdom": "^15.2.1",
"knex": "^0.16.5",
"knex-migrate": "^1.7.4",
@ -90,6 +92,8 @@
"moment": "^2.24.0",
"opn": "^5.5.0",
"pg": "^7.14.0",
"postgraphile": "^4.5.5",
"postgraphile-plugin-connection-filter": "^1.1.3",
"prop-types": "^15.7.2",
"react": "^16.12.0",
"react-dom": "^16.12.0",

View File

@ -188,14 +188,21 @@
text-decoration: none;
}
.title[data-v-3abcf101] {
color: #222;
display: -webkit-box;
display: flex;
-webkit-box-align: center;
align-items: center;
margin: 0 .25rem .25rem 0;
color: #222;
font-size: 1rem;
max-height: 3rem;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.title .icon[data-v-3abcf101] {
margin: 0 .25rem 0 0;
}
.network[data-v-3abcf101] {
color: #555;
margin: 0 .25rem 0 0;
@ -208,9 +215,8 @@
line-height: 1.5rem;
}
.tags[data-v-3abcf101] {
max-height: 2.5rem;
max-height: .5rem;
padding: .25rem .5rem 1rem .5rem;
line-height: 1.5rem;
word-wrap: break-word;
overflow-y: hidden;
}
@ -255,6 +261,7 @@
text-transform: capitalize;
}
.tiles[data-v-22ffe3e4] {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 0.33fr));
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; */
.actor[data-v-6989dc6f] {
width: 10rem;
@ -317,6 +349,8 @@
width: 100%;
display: -webkit-box;
display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: justify;
justify-content: space-between;
box-sizing: border-box;
@ -330,31 +364,6 @@
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; */
.column[data-v-d4b03dc2] {
width: 1200px;
@ -510,28 +519,28 @@
}
/* $primary: #ff886c; */
.header[data-v-3e57cf44] {
.header[data-v-194630f6] {
display: -webkit-box;
display: flex;
flex-wrap: wrap;
-webkit-box-pack: justify;
justify-content: space-between;
}
.title[data-v-3e57cf44] {
.title[data-v-194630f6] {
display: -webkit-inline-box;
display: inline-flex;
-webkit-box-align: top;
align-items: top;
margin: 0 1rem 0 0;
}
.title:hover .icon[data-v-3e57cf44] {
.title:hover .icon[data-v-194630f6] {
fill: #ff6c88;
}
.heading[data-v-3e57cf44] {
.heading[data-v-194630f6] {
padding: 0;
margin: 0 0 1rem 0;
}
.link[data-v-3e57cf44] {
.link[data-v-194630f6] {
display: -webkit-box;
display: flex;
flex-shrink: 0;
@ -541,20 +550,20 @@
-webkit-box-align: end;
align-items: flex-end;
}
.logo[data-v-3e57cf44] {
.logo[data-v-194630f6] {
width: 20rem;
max-height: 8rem;
-o-object-fit: contain;
object-fit: contain;
margin: 0 .5rem 1rem 0;
}
.networklogo-container[data-v-3e57cf44] {
.networklogo-container[data-v-194630f6] {
display: -webkit-box;
display: flex;
-webkit-box-align: center;
align-items: center;
}
.networklogo[data-v-3e57cf44] {
.networklogo[data-v-194630f6] {
color: #222;
width: 15rem;
max-height: 6rem;
@ -565,13 +574,13 @@
object-position: 100% 0;
margin: 0 0 0 .5rem;
}
.sites[data-v-3e57cf44],
.scenes[data-v-3e57cf44] {
.sites[data-v-194630f6],
.scenes[data-v-194630f6] {
display: grid;
grid-gap: 1rem;
margin: 0 0 1rem 0;
}
.sites[data-v-3e57cf44] {
.sites[data-v-194630f6] {
grid-template-columns: repeat(auto-fit, 15rem);
}
@ -623,44 +632,111 @@
}
/* $primary: #ff886c; */
.header[data-v-e2e12602] {
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] {
.sites[data-v-7bebaa3e] {
display: grid;
grid-gap: 1rem;
margin: 0 0 2rem 0;
padding: 1rem;
margin: 0;
grid-template-columns: 1fr;
overflow-y: auto;
}
.sites[data-v-e2e12602] {
grid-template-columns: repeat(auto-fit, 15rem);
.sites.compact[data-v-7bebaa3e] {
display: -webkit-box;
display: flex;
overflow-x: auto;
}
.sites.compact .tile[data-v-7bebaa3e] {
width: 15rem;
margin: 0 1rem 0 0;
}
/* $primary: #ff886c; */
@media (max-width: 1200px) {
.releases .tiles {
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
}
}
/* $primary: #ff886c; */
.network[data-v-e2e12602] {
display: -webkit-box;
display: 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) {
.sites[data-v-e2e12602] {
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
.header[data-v-e2e12602] {
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()
.then(async () => {
// find network IDs
const duplicates = await knex('networks').select('*');
const duplicatesBySlug = duplicates.reduce((acc, network) => ({ ...acc, [network.slug]: network }), {});
return upsert('networks', networks, duplicatesBySlug, 'slug', knex);
});
.then(async () => upsert('networks', networks, 'slug', knex));

View File

@ -2428,15 +2428,10 @@ function getSites(networksMap) {
/* eslint-disable max-len */
exports.seed = knex => Promise.resolve()
.then(async () => {
const [duplicates, networks] = await Promise.all([
knex('sites').select('*'),
knex('networks').select('*'),
]);
const duplicatesBySlug = duplicates.reduce((acc, site) => ({ ...acc, [site.slug]: site }), {});
const networks = await knex('networks').select('*');
const networksMap = networks.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
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');
function getStudios(networksMap) {
@ -9,133 +7,133 @@ function getStudios(networksMap) {
slug: 'gonzocom',
name: 'Gonzo.com',
url: 'https://www.legalporno.com/studios/gonzo_com',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'giorgiograndi',
name: 'Giorgio Grandi',
url: 'https://www.legalporno.com/studios/giorgio-grandi',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'hardpornworld',
name: 'Hard Porn World',
url: 'https://www.legalporno.com/studios/hard-porn-world',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'interracialvision',
name: 'Interracial Vision',
url: 'https://www.legalporno.com/studios/interracial-vision',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'giorgioslab',
name: 'Giorgio\'s Lab',
url: 'https://www.legalporno.com/studios/giorgio--s-lab',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'americananal',
name: 'American Anal',
url: 'https://www.legalporno.com/studios/american-anal',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'assablanca',
name: 'Assablanca',
url: 'https://www.legalporno.com/studios/assablanca',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'focus',
name: 'Focus',
url: 'https://www.legalporno.com/studios/focus',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'analforever',
name: 'Anal Forever',
url: 'https://www.legalporno.com/studios/anal-forever',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'gonzoinbrazil',
name: 'Gonzo in Brazil',
url: 'https://www.legalporno.com/studios/gonzo-in-brazil',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'mranal',
name: 'Mr Anal',
url: 'https://www.legalporno.com/studios/mr-anal',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'tarrawhite',
name: 'Tarra White',
url: 'https://www.legalporno.com/studios/tarra-white',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'sineplexsos',
name: 'Sineplex SOS',
url: 'https://www.legalporno.com/studios/sineplex-sos',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'fmodels',
name: 'F Models',
url: 'https://www.legalporno.com/studios/f-models',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'sineplexcz',
name: 'Sineplex CZ',
url: 'https://www.legalporno.com/studios/sineplex-cz',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'gg',
name: 'GG',
url: 'https://www.legalporno.com/studios/gg',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'firstgape',
name: 'First Gape',
url: 'https://www.legalporno.com/studios/first-gape',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'omargalantiproductions',
name: 'Omar Galanti Productions',
url: 'https://www.legalporno.com/studios/omar-galanti-productions',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'norestfortheass',
name: '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',
name: 'Hairy Gonzo',
url: 'https://www.legalporno.com/studios/hairy-gonzo',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'sineplexclassic',
name: 'Sineplex Classic',
url: 'https://www.legalporno.com/studios/sineplex-classic',
network_id: networksMap['legalporno'],
network_id: networksMap.legalporno,
},
{
slug: 'sinemale',
name: '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 */
exports.seed = knex => Promise.resolve()
.then(async () => {
const [duplicates, networks] = await Promise.all([
knex('studios').select('*'),
knex('networks').select('*'),
]);
const duplicatesBySlug = duplicates.reduce((acc, studio) => ({ ...acc, [studio.slug]: studio }), {});
const networks = await knex('networks').select('*');
const networksMap = networks.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
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',
name: 'Location',
},
{
slug: 'oral',
name: 'Oral',
},
{
slug: 'orientation',
name: 'Orientation',
@ -69,7 +73,7 @@ function getTags(groupsMap) {
name: 'airtight',
slug: 'airtight',
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,
group_id: groupsMap.penetration,
},
@ -136,16 +140,19 @@ function getTags(groupsMap) {
priority: 6,
description: 'Sucking off a cock right after anal, giving your own or someone else`s asshole a second hand taste.',
alias_for: null,
group_id: groupsMap.oral,
},
{
name: 'ass eating',
slug: 'ass-eating',
alias_for: null,
group_id: groupsMap.oral,
},
{
name: 'ball licking',
slug: 'ball-licking',
alias_for: null,
group_id: groupsMap.oral,
},
{
name: 'ballerina',
@ -211,6 +218,7 @@ function getTags(groupsMap) {
slug: 'blowjob',
priority: 7,
alias_for: null,
group_id: groupsMap.oral,
},
{
name: 'blowbang',
@ -319,6 +327,7 @@ function getTags(groupsMap) {
slug: 'deepthroat',
priority: 7,
alias_for: null,
group_id: groupsMap.oral,
},
{
name: 'double penetration',
@ -345,11 +354,13 @@ function getTags(groupsMap) {
name: 'double blowjob',
slug: 'double-blowjob',
alias_for: null,
group_id: groupsMap.oral,
},
{
name: 'doggy style',
slug: 'doggy-style',
alias_for: null,
group_id: groupsMap.position,
},
{
name: 'dress',
@ -379,7 +390,7 @@ function getTags(groupsMap) {
slug: 'facefuck',
priority: 9,
alias_for: null,
group_id: groupsMap.position,
group_id: groupsMap.oral,
},
{
name: 'facesitting',
@ -429,7 +440,7 @@ function getTags(groupsMap) {
{
name: '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,
priority: 9,
group_id: groupsMap.group,
@ -645,6 +656,7 @@ function getTags(groupsMap) {
name: 'pussy eating',
slug: 'pussy-eating',
alias_for: null,
group_id: groupsMap.oral,
},
{
name: 'redhead',
@ -1542,35 +1554,20 @@ function getTagAliases(tagsMap) {
}
exports.seed = knex => Promise.resolve()
.then(async () => upsert('tags_groups', groups, 'slug', knex))
.then(async () => {
const duplicates = await knex('tags_groups').select('*');
const duplicatesBySlug = duplicates.reduce((acc, group) => ({ ...acc, [group.slug]: group }), {});
return upsert('tags_groups', groups, duplicatesBySlug, 'slug', knex);
})
.then(async () => {
const [duplicates, groupEntries] = await Promise.all([
knex('tags').select('*'),
knex('tags_groups').select('*'),
]);
const duplicatesBySlug = duplicates.reduce((acc, tag) => ({ ...acc, [tag.slug]: tag }), {});
const groupEntries = await knex('tags_groups').select('*');
const groupsMap = groupEntries.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
const tags = getTags(groupsMap);
return upsert('tags', tags, duplicatesBySlug, 'slug', knex);
return upsert('tags', tags, 'slug', knex);
})
.then(async () => {
const [duplicates, tags] = await Promise.all([
knex('tags').select('*').whereNotNull('alias_for'),
knex('tags').select('*').where({ alias_for: null }),
]);
const duplicatesByName = duplicates.reduce((acc, tag) => ({ ...acc, [tag.name]: tag }), {});
const tags = await knex('tags').select('*').where({ alias_for: null });
const tagsMap = tags.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
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');
function getMedia(tagsMap) {
return [
{
path: 'tags/airtight/poster.jpeg',
target_id: tagsMap.airtight,
role: 'poster',
comment: 'Jynx Maze in "Pump My Ass Full of Cum 3" for Jules Jordan',
},
{
path: 'tags/airtight/2.jpeg',
target_id: tagsMap.airtight,
comment: 'Dakota Skye in "Dakota Goes Nuts" for ArchAngel',
},
{
path: 'tags/airtight/1.jpeg',
target_id: tagsMap.airtight,
comment: 'Chloe Amour in "DP Masters 4" for Jules Jordan',
},
{
path: 'tags/airtight/0/poster.jpeg',
domain: 'tags',
target_id: tagsMap.airtight,
comment: 'Sheena Shaw in "Ass Worship 14" for Jules Jordan',
},
{
path: 'tags/anal/poster.jpeg',
target_id: tagsMap.anal,
role: 'poster',
comment: '',
},
{
path: 'tags/double-penetration/poster.jpeg',
target_id: tagsMap['double-penetration'],
role: 'poster',
comment: '',
},
{
path: 'tags/double-anal/poster.jpeg',
target_id: tagsMap['double-anal'],
role: 'poster',
comment: '',
},
{
path: 'tags/double-vaginal/poster.jpeg',
target_id: tagsMap['double-vaginal'],
role: 'poster',
comment: '',
},
{
path: 'tags/da-tp/0.jpeg',
target_id: tagsMap['da-tp'],
role: 'poster',
comment: 'Natasha Teen in LegalPorno SZ2164',
},
{
path: 'tags/da-tp/3.jpeg',
target_id: tagsMap['da-tp'],
role: 'photo',
comment: 'Evelina Darling in GIO294',
},
{
path: 'tags/da-tp/1.jpeg',
target_id: tagsMap['da-tp'],
role: 'photo',
comment: 'Francys Belle in SZ1702 for LegalPorno',
},
{
path: 'tags/da-tp/2.jpeg',
target_id: tagsMap['da-tp'],
role: 'photo',
comment: 'Angel Smalls in GIO408 for LegalPorno',
},
{
path: 'tags/da-tp/4.jpeg',
target_id: tagsMap['da-tp'],
role: 'photo',
comment: 'Ninel Mojado aka Mira Cuckold in GIO063 for LegalPorno',
},
{
path: 'tags/dv-tp/poster.jpeg',
target_id: tagsMap['dv-tp'],
role: 'poster',
comment: 'Juelz Ventura in "Gangbanged 5" for Elegant Angel',
},
{
path: 'tags/dv-tp/0.jpeg',
target_id: tagsMap['dv-tp'],
role: 'photo',
comment: 'Luna Rival in LegalPorno SZ1490',
},
{
path: 'tags/tattoo/poster.jpeg',
target_id: tagsMap.tattoo,
role: 'poster',
comment: 'Kali Roses in "Goes All In For Anal" for Hussie Pass',
},
{
path: 'tags/triple-anal/poster.jpeg',
target_id: tagsMap['triple-anal'],
role: 'poster',
comment: 'Kristy Black in SZ1986 for LegalPorno',
},
{
path: 'tags/triple-anal/1.jpeg',
target_id: tagsMap['triple-anal'],
role: 'photo',
comment: 'Natasha Teen in SZ2098 for LegalPorno',
},
{
path: 'tags/triple-anal/2.jpeg',
target_id: tagsMap['triple-anal'],
role: 'photo',
comment: 'Kira Thorn in GIO1018 for LegalPorno',
},
{
path: 'tags/blowbang/poster.jpeg',
target_id: tagsMap.blowbang,
role: 'poster',
comment: '',
},
{
path: 'tags/gangbang/poster.jpeg',
target_id: tagsMap.gangbang,
role: 'poster',
comment: '',
},
{
path: 'tags/gangbang/1.jpeg',
target_id: tagsMap.gangbang,
role: 'photo',
comment: 'Ginger Lynn in "Gangbang Mystique", a photoset shot by Suze Randall for Puritan No. 10, 1984. This photo pushed the boundaries of pornography at the time, as depicting a woman \'fully occupied\' was unheard of.',
},
{
path: 'tags/gangbang/2.jpeg',
target_id: tagsMap.gangbang,
role: 'photo',
comment: 'Riley Reid\'s double anal in "The Gangbang of Riley Reid" for Jules Jordan',
},
{
path: 'tags/gangbang/3.jpeg',
target_id: tagsMap.gangbang,
role: 'photo',
comment: 'Kelsi Monroe in "Brazzers House 2, Day 2" for Brazzers',
},
{
path: 'tags/mff/poster.jpeg',
target_id: tagsMap.mff,
role: 'poster',
comment: '',
},
{
path: 'tags/mfm/poster.jpeg',
target_id: tagsMap.mfm,
role: 'poster',
comment: '',
},
{
path: 'tags/orgy/poster.jpeg',
target_id: tagsMap.orgy,
role: 'poster',
comment: '',
},
{
path: 'tags/asian/poster.jpeg',
target_id: tagsMap.asian,
role: 'poster',
comment: '',
},
{
path: 'tags/caucasian/poster.jpeg',
target_id: tagsMap.caucasian,
role: 'poster',
comment: '',
},
{
path: 'tags/ebony/poster.jpeg',
target_id: tagsMap.ebony,
role: 'poster',
comment: '',
},
{
path: 'tags/latina/poster.jpeg',
target_id: tagsMap.latina,
role: 'poster',
comment: '',
},
{
path: 'tags/interracial/poster.jpeg',
target_id: tagsMap.interracial,
role: 'poster',
comment: '',
},
{
path: 'tags/facial/poster.jpeg',
target_id: tagsMap.facial,
role: 'poster',
comment: '',
},
{
path: 'tags/bukkake/poster.jpeg',
target_id: tagsMap.bukkake,
role: 'poster',
comment: '',
},
{
path: 'tags/swallowing/poster.jpeg',
target_id: tagsMap.swallowing,
role: 'poster',
comment: '',
},
{
path: 'tags/creampie/poster.jpeg',
target_id: tagsMap.creampie,
role: 'poster',
comment: '',
},
{
path: 'tags/anal-creampie/poster.jpeg',
target_id: tagsMap['anal-creampie'],
role: 'poster',
comment: '',
},
{
path: 'tags/oral-creampie/poster.jpeg',
target_id: tagsMap['oral-creampie'],
role: 'poster',
comment: '',
},
]
.map((file, index) => ({
...file,
thumbnail: file.thumbnail || file.path.replace('.jpeg', '_thumb.jpeg'),
mime: 'image/jpeg',
index,
domain: file.domain || 'tags',
role: file.role || 'photo',
}));
}
const tagPosters = [
{
path: 'tags/airtight/poster.jpeg',
tagSlug: 'airtight',
comment: 'Jynx Maze in "Pump My Ass Full of Cum 3" for Jules Jordan',
},
{
path: 'tags/anal/poster.jpeg',
tagSlug: 'anal',
comment: 'Jynx Maze in "Anal Buffet 6" for Evil Angel',
},
{
path: 'tags/ass-to-mouth/poster.jpeg',
tagSlug: 'ass-to-mouth',
comment: 'Alysa Gap and Logan in "Anal Buffet 4" for Evil Angel',
},
{
path: 'tags/gapes/poster.jpeg',
tagSlug: 'gapes',
comment: 'Paulina in "Anal Buffet 4" for Evil Angel',
},
{
path: 'tags/da-tp/0.jpeg',
tagSlug: 'da-tp',
comment: 'Natasha Teen in LegalPorno SZ2164',
},
{
path: 'tags/double-penetration/poster.jpeg',
tagSlug: 'double-penetration',
comment: 'Mia Malkova in "DP!" for HardX',
},
{
path: 'tags/double-anal/poster.jpeg',
tagSlug: 'double-anal',
comment: 'Haley Reed in "Young Hot Ass" for Evil Angel',
},
{
path: 'tags/double-vaginal/poster.jpeg',
tagSlug: 'double-vaginal',
comment: '',
},
{
path: 'tags/dv-tp/poster.jpeg',
tagSlug: 'dv-tp',
comment: 'Juelz Ventura in "Gangbanged 5" for Elegant Angel',
},
{
path: 'tags/tattoo/poster.jpeg',
tagSlug: 'tattoo',
comment: 'Kali Roses in "Goes All In For Anal" for Hussie Pass',
},
{
path: 'tags/triple-anal/poster.jpeg',
tagSlug: 'triple-anal',
comment: 'Kristy Black in SZ1986 for LegalPorno',
},
{
path: 'tags/blowbang/poster.jpeg',
tagSlug: 'blowbang',
comment: '',
},
{
path: 'tags/gangbang/poster.jpeg',
tagSlug: 'gangbang',
comment: 'Kristen Scott in "Interracial Gangbang!" for Jules Jordan',
},
{
path: 'tags/mff/poster.jpeg',
tagSlug: 'mff',
comment: '',
},
{
path: 'tags/mfm/poster.jpeg',
tagSlug: 'mfm',
comment: '',
},
{
path: 'tags/orgy/poster.jpeg',
tagSlug: 'orgy',
comment: '',
},
{
path: 'tags/asian/poster.jpeg',
tagSlug: 'asian',
comment: 'Vina Sky in "Young and Glamorous 10" for Jules Jordan',
},
{
path: 'tags/caucasian/poster.jpeg',
tagSlug: 'caucasian',
comment: '',
},
{
path: 'tags/ebony/poster.jpeg',
tagSlug: 'ebony',
comment: '',
},
{
path: 'tags/latina/poster.jpeg',
tagSlug: 'latina',
comment: '',
},
{
path: 'tags/interracial/poster.jpeg',
tagSlug: 'interracial',
comment: '',
},
{
path: 'tags/facial/poster.jpeg',
tagSlug: 'facial',
comment: '',
},
{
path: 'tags/trainbang/poster.jpeg',
tagSlug: 'trainbang',
comment: 'Kali Roses in "Passing Me Around" for Blacked',
},
{
path: 'tags/bukkake/poster.jpeg',
tagSlug: 'bukkake',
comment: '',
},
{
path: 'tags/swallowing/poster.jpeg',
tagSlug: 'swallowing',
comment: '',
},
{
path: 'tags/creampie/poster.jpeg',
tagSlug: 'creampie',
comment: '',
},
{
path: 'tags/anal-creampie/poster.jpeg',
tagSlug: 'anal-creampie',
comment: '',
},
{
path: 'tags/oral-creampie/poster.jpeg',
tagSlug: 'oral-creampie',
comment: '',
},
]
.map((file, index) => ({
...file,
thumbnail: file.thumbnail || file.path.replace('.jpeg', '_thumb.jpeg'),
mime: 'image/jpeg',
index,
}));
const tagPhotos = [
{
path: 'tags/airtight/3.jpeg',
tagSlug: 'airtight',
comment: 'Anita Bellini in "Triple Dick Gangbang" for Hands On Hardcore (DDF Network)',
},
{
path: 'tags/airtight/2.jpeg',
tagSlug: 'airtight',
comment: 'Dakota Skye in "Dakota Goes Nuts" for ArchAngel',
},
{
path: 'tags/airtight/1.jpeg',
tagSlug: 'airtight',
comment: 'Chloe Amour in "DP Masters 4" for Jules Jordan',
},
{
path: 'tags/airtight/0.jpeg',
domain: 'tags',
tagSlug: 'airtight',
comment: 'Sheena Shaw in "Ass Worship 14" for Jules Jordan',
},
{
path: 'tags/anal/0.jpeg',
tagSlug: 'anal',
comment: '',
},
{
path: 'tags/double-anal/1.jpeg',
tagSlug: 'double-anal',
comment: 'Ria Sunn in SZ1801 for LegalPorno',
},
{
path: 'tags/double-anal/0.jpeg',
tagSlug: 'double-anal',
comment: 'Nicole Black doing double anal during a gangbang in GIO971 for LegalPorno',
},
{
path: 'tags/da-tp/3.jpeg',
tagSlug: 'da-tp',
comment: 'Evelina Darling in GIO294',
},
{
path: 'tags/da-tp/1.jpeg',
tagSlug: 'da-tp',
comment: 'Francys Belle in SZ1702 for LegalPorno',
},
{
path: 'tags/da-tp/2.jpeg',
tagSlug: 'da-tp',
comment: 'Angel Smalls in GIO408 for LegalPorno',
},
{
path: 'tags/da-tp/4.jpeg',
tagSlug: 'da-tp',
comment: 'Ninel Mojado aka Mira Cuckold in GIO063 for LegalPorno',
},
{
path: 'tags/dv-tp/0.jpeg',
tagSlug: 'dv-tp',
comment: 'Luna Rival in LegalPorno SZ1490',
},
{
path: 'tags/double-penetration/0.jpeg',
tagSlug: 'double-penetration',
comment: '',
},
{
path: 'tags/gapes/0.jpeg',
tagSlug: 'gapes',
comment: 'McKenzee Miles in "Anal Buffet 4" for Evil Angel',
},
{
path: 'tags/trainbang/0.jpeg',
tagSlug: 'trainbang',
comment: 'Nicole Black in GIO971 for LegalPorno',
},
{
path: 'tags/triple-anal/1.jpeg',
tagSlug: 'triple-anal',
comment: 'Natasha Teen in SZ2098 for LegalPorno',
},
{
path: 'tags/triple-anal/2.jpeg',
tagSlug: 'triple-anal',
comment: 'Kira Thorn in GIO1018 for LegalPorno',
},
{
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 */
exports.seed = knex => Promise.resolve()
.then(async () => {
const [duplicates, tags] = await Promise.all([
knex('media').where('domain', 'tags'),
knex('tags').where('alias_for', null),
const tagMedia = tagPosters.concat(tagPhotos);
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')
.then(async () => {
const duplicates = await knex('countries').select('*');
const duplicatesByAlpha2 = duplicates.reduce((acc, country) => ({ ...acc, [country.alpha2]: country }), {});
return upsert('countries', countries, duplicatesByAlpha2, 'alpha2', knex);
});
.then(async () => upsert('countries', countries, 'alpha2', knex));

View File

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

View File

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

View File

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

View File

@ -15,6 +15,40 @@ const {
} = require('./media');
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) {
const [actors, tags, media] = await Promise.all([
knex('actors_associated')
@ -49,8 +83,9 @@ async function curateRelease(release) {
.orderBy(['role', 'index']),
]);
return {
const curatedRelease = {
id: release.id,
type: release.type,
title: release.title,
date: release.date,
dateAdded: release.created_at,
@ -108,33 +143,51 @@ async function curateRelease(release) {
url: release.network_url,
},
};
return curatedRelease;
}
function curateReleases(releases) {
return Promise.all(releases.map(async release => curateRelease(release)));
}
async function getChannelSite(release) {
try {
const site = await findSiteByUrl(release.channel);
return site || null;
} catch (error) {
const [site] = await fetchSites({
name: release.channel,
slug: release.channel,
});
return site || null;
async function attachChannelSite(release) {
if (!release.site.isFallback) {
return release;
}
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 = {
site_id: release.site.id,
studio_id: release.studio ? release.studio.id : null,
shoot_id: release.shootId || null,
entry_id: release.entryId || null,
parent_id: release.parentId,
type: release.type,
url: release.url,
title: release.title,
date: release.date,
@ -147,52 +200,9 @@ async function curateScrapedRelease(release) {
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;
}
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 = {}) {
const releases = await knex('releases')
.modify(commonQuery, options)
@ -244,6 +254,40 @@ async function fetchTagReleases(queryObject, options = {}) {
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) {
const subpath = `${release.site.network.slug}/${release.site.slug}/${release.id}/`;
const identifier = `"${release.title}" (${releaseId})`;
@ -279,7 +323,7 @@ async function storeReleaseAssets(release, releaseId) {
async function storeRelease(release) {
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) {
return existingRelease.id;
@ -317,18 +361,13 @@ async function storeRelease(release) {
async function storeReleases(releases) {
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 {
const releaseId = await storeRelease(release);
const releaseWithChannelSite = await attachChannelSite(release);
const releaseId = await storeRelease(releaseWithChannelSite);
return {
id: releaseId,
...release,
...releaseWithChannelSite,
};
} catch (error) {
console.error(error);
@ -339,22 +378,8 @@ async function storeReleases(releases) {
concurrency: 10,
}).filter(release => release);
const actors = storedReleases.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;
}, {});
const actors = accumulateActors(storedReleases);
const movies = accumulateMovies(storedReleases);
await Promise.all([
associateActors(actors, storedReleases),
@ -363,7 +388,11 @@ async function storeReleases(releases) {
}),
]);
return storedReleases;
return {
releases: storedReleases,
actors,
movies,
};
}
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 { fetchIncludedSites } = require('./sites');
const scrapers = require('./scrapers/scrapers');
const scrapeRelease = require('./scrape-release');
const { scrapeRelease } = require('./scrape-releases');
const { storeReleases } = require('./releases');
function getAfterDate() {
@ -70,7 +70,7 @@ async function deepFetchReleases(baseReleases) {
return Promise.map(baseReleases, async (release) => {
if (release.url) {
try {
const fullRelease = await scrapeRelease(release.url, release, true);
const fullRelease = await scrapeRelease(release.url, release, 'scene');
return {
...release,
@ -111,10 +111,10 @@ async function scrapeSiteReleases(scraper, site) {
return baseReleases;
}
async function scrapeReleases() {
async function scrapeSites() {
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];
if (!scraper) {
@ -143,8 +143,8 @@ async function scrapeReleases() {
});
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 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`)}`;
return getPhoto(pageUrl);

View File

@ -50,7 +50,9 @@ function scrapeProfile(html, actorName) {
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.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;
}

View File

@ -16,7 +16,7 @@ async function scrapeProfile(html, _url, actorName) {
const { document } = new JSDOM(html).window;
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 = {
name: actorName,

View File

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

View File

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

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