Compare commits
18 Commits
36c5fa3b52
...
e2c2c9b4f0
Author | SHA1 | Date |
---|---|---|
|
e2c2c9b4f0 | |
|
2271577874 | |
|
30cf597ec9 | |
|
72b175e9e2 | |
|
70e27a6cd9 | |
|
e77dbca954 | |
|
5a6bf2b42f | |
|
1c43884102 | |
|
31aee71edb | |
|
9b17add4e2 | |
|
3845c3f52d | |
|
6950a76cb5 | |
|
f4c2e6c08c | |
|
577c03f9b7 | |
|
ce92d13327 | |
|
13b45e1709 | |
|
07a6c77ce2 | |
|
7ba716cd6f |
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -56,6 +56,7 @@ export default {
|
|||
}
|
||||
|
||||
.tiles {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(20rem, .33fr));
|
||||
grid-gap: 1rem;
|
||||
|
|
|
@ -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>
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -52,7 +52,7 @@ export default {
|
|||
object-fit: contain;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
filter: $logo-outline;
|
||||
filter: $logo-shadow;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
`);
|
||||
|
|
10
package.json
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 4.1 MiB |
After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 845 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 110 KiB |
After Width: | Height: | Size: 107 KiB |
After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 684 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 800 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 107 KiB |
After Width: | Height: | Size: 494 KiB |
After Width: | Height: | Size: 107 KiB |
After Width: | Height: | Size: 2.1 MiB |
After Width: | Height: | Size: 100 KiB |
After Width: | Height: | Size: 3.2 MiB |
After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 391 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 115 KiB |
After Width: | Height: | Size: 142 KiB |
After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 782 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 137 KiB |
After Width: | Height: | Size: 1.4 MiB |
After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 1.0 MiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 127 KiB |
After Width: | Height: | Size: 446 KiB |
After Width: | Height: | Size: 97 KiB |
After Width: | Height: | Size: 402 KiB |
After Width: | Height: | Size: 93 KiB |
After Width: | Height: | Size: 1.4 MiB |
After Width: | Height: | Size: 113 KiB |
After Width: | Height: | Size: 3.7 MiB |
After Width: | Height: | Size: 114 KiB |
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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(),
|
||||
]);
|
||||
}
|
||||
|
|
12
src/app.js
|
@ -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) {
|
||||
|
|
196
src/media.js
|
@ -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,
|
||||
};
|
||||
|
|
195
src/releases.js
|
@ -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 = {
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -54,13 +54,13 @@ module.exports = {
|
|||
actors: {
|
||||
// ordered by data priority
|
||||
xempire,
|
||||
julesjordan,
|
||||
brazzers,
|
||||
legalporno,
|
||||
pornhub,
|
||||
freeones,
|
||||
freeonesLegacy,
|
||||
kellymadison,
|
||||
julesjordan,
|
||||
ddfnetwork,
|
||||
},
|
||||
};
|
||||
|
|
12
src/tags.js
|
@ -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,
|
||||
})));
|
||||
}
|
||||
|
||||
|
|