Move tag posters and photos to media database.

This commit is contained in:
ThePendulum 2019-12-04 21:58:08 +01:00
parent cf81aa99e0
commit 55e3130062
51 changed files with 861 additions and 184 deletions

View File

@ -188,7 +188,6 @@
<p <p
v-if="actor.description" v-if="actor.description"
class="description" class="description"
@wheel.prevent="scrollDescription"
>{{ actor.description }}</p> >{{ actor.description }}</p>
<li <li
@ -268,10 +267,6 @@ function scrollPhotos(event) {
event.currentTarget.scrollLeft += event.deltaY; // eslint-disable-line no-param-reassign event.currentTarget.scrollLeft += event.deltaY; // eslint-disable-line no-param-reassign
} }
function scrollDescription(event) {
event.currentTarget.scrollTop += event.deltaY; // eslint-disable-line no-param-reassign
}
async function mounted() { async function mounted() {
[this.actor] = await Promise.all([ [this.actor] = await Promise.all([
this.$store.dispatch('fetchActors', { actorId: this.$route.params.actorSlug }), this.$store.dispatch('fetchActors', { actorId: this.$route.params.actorSlug }),
@ -305,7 +300,6 @@ export default {
mounted, mounted,
methods: { methods: {
fetchReleases, fetchReleases,
scrollDescription,
scrollPhotos, scrollPhotos,
}, },
}; };

View File

@ -58,7 +58,6 @@ export default {
} }
.photo-link { .photo-link {
height: 15rem;
} }
.photo { .photo {

View File

@ -13,7 +13,7 @@
class="nav-link" class="nav-link"
:class="{ active: active === 'actors' }" :class="{ active: active === 'actors' }"
> >
<Icon icon="stars" />Actors <Icon icon="stars" /><span class="nav-label">Actors</span>
</router-link> </router-link>
</li> </li>
@ -23,7 +23,7 @@
class="nav-link" class="nav-link"
:class="{ active: active === 'networks' }" :class="{ active: active === 'networks' }"
> >
<Icon icon="earth2" />Networks <Icon icon="earth2" /><span class="nav-label">Networks</span>
</router-link> </router-link>
</li> </li>
@ -33,7 +33,7 @@
class="nav-link" class="nav-link"
:class="{ active: active === 'tags' }" :class="{ active: active === 'tags' }"
> >
<Icon icon="price-tags" />Tags <Icon icon="price-tags" /><span class="nav-label">Tags</span>
</router-link> </router-link>
</li> </li>
</ul> </ul>
@ -83,8 +83,9 @@ export default {
} }
.nav-link { .nav-link {
display: inline-flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
padding: 1rem; padding: 1rem;
border-bottom: solid 5px transparent; border-bottom: solid 5px transparent;
color: $shadow; color: $shadow;
@ -114,4 +115,24 @@ export default {
} }
} }
} }
@media(max-width: $breakpoint0) {
.nav-label {
display: none;
}
.nav .nolist {
display: flex;
}
.nav,
.nav-item {
flex-grow: 1;
}
.nav-link {
}
}
</style> </style>

View File

@ -31,7 +31,7 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.networks { .networks {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, 15rem); grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
grid-gap: 1rem; grid-gap: 1rem;
padding: 1rem; padding: 1rem;
} }

View File

@ -1,39 +1,72 @@
<template> <template>
<div <div
v-if="tag" v-if="tag"
class="content tag" class="content"
> >
<FilterBar :fetch-releases="fetchReleases" /> <FilterBar :fetch-releases="fetchReleases" />
<div class="header"> <div class="tag">
<img <div class="sidebar">
:src="`/img/tags/${tag.slug}.jpg`" <a
class="poster" v-if="tag.poster"
> :href="`/media/${tag.poster.path}`"
:title="tag.poster.comment"
target="_blank"
rel="noopener noreferrer"
>
<img
:src="`/media/${tag.poster.thumbnail}`"
class="poster"
>
</a>
<span> <span>
<h2 class="title"> <h2 class="title">
<Icon icon="price-tag4" /> <Icon icon="price-tag4" />
{{ tag.name }} {{ tag.name }}
</h2> </h2>
<span class="description">{{ tag.description }}</span> <p
</span> class="description"
</div> v-html="description"
/>
</span>
<div class="content-inner"> <div class="photos">
<Releases <a
:releases="releases" v-for="photo in tag.photos"
:context="tag.name" :key="`photo-${photo.id}`"
/> :title="photo.comment"
:href="`/media/${photo.path}`"
target="_blank"
rel="noopener noreferrer"
>
<img
:src="`/media/${photo.thumbnail}`"
class="photo"
>
</a>
</div>
</div>
<div class="content-inner">
<Releases :releases="releases" />
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
/* eslint-disable no-v-html */
import { Converter } from 'showdown';
import escapeHtml from '../../../src/utils/escape-html';
import FilterBar from '../header/filter-bar.vue'; import FilterBar from '../header/filter-bar.vue';
import Releases from '../releases/releases.vue'; import Releases from '../releases/releases.vue';
const converter = new Converter();
async function fetchReleases() { async function fetchReleases() {
this.releases = await this.$store.dispatch('fetchTagReleases', this.$route.params.tagSlug); this.releases = await this.$store.dispatch('fetchTagReleases', this.$route.params.tagSlug);
} }
@ -44,6 +77,8 @@ async function mounted() {
this.fetchReleases(), this.fetchReleases(),
]); ]);
this.description = converter.makeHtml(escapeHtml(this.tag.description));
this.pageTitle = this.tag.name; this.pageTitle = this.tag.name;
} }
@ -66,27 +101,62 @@ export default {
}; };
</script> </script>
<style lang="scss">
@import 'theme';
.description a {
color: $link;
text-decoration: inherit;
&:hover {
color: $primary;
}
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
@import 'theme'; @import 'theme';
.header { .tag {
display: flex;
flex-direction: row;
flex-grow: 1;
justify-content: stretch;
}
.sidebar {
background: $profile;
color: $text-contrast;
width: 25rem;
box-sizing: border-box;
padding: 1rem;
} }
.poster { .poster {
width: 30rem; width: 100%;
height: 18rem; height: 15rem;
object-fit: cover; object-fit: cover;
} }
.title { .title {
display: inline-block; padding: 0;
padding: 1rem; margin: 1rem 0;
margin: 0 .5rem 0 0;
text-transform: capitalize; text-transform: capitalize;
.icon { .icon {
fill: $text-contrast;
width: 1.25rem; width: 1.25rem;
height: 1.25rem; height: 1.25rem;
} }
} }
.description {
padding: 0;
margin: 0 0 1rem 0;
line-height: 1.5;
}
.photo {
width: 100%;
}
</style> </style>

View File

@ -1,10 +1,44 @@
<template> <template>
<div class="tags"> <div class="tags">
<Tag <h3>Penetration</h3>
v-for="tag in tags"
:key="`tag-${tag.id}`" <div class="tiles">
:tag="tag" <Tag
/> v-for="tag in tags.penetration"
:key="`tag-${tag.id}`"
:tag="tag"
/>
</div>
<h3>Group</h3>
<div class="tiles">
<Tag
v-for="tag in tags.group"
:key="`tag-${tag.id}`"
:tag="tag"
/>
</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">
<Tag
v-for="tag in tags.finish"
:key="`tag-${tag.id}`"
:tag="tag"
/>
</div>
</div> </div>
</template> </template>
@ -12,7 +46,42 @@
import Tag from '../tile/tag.vue'; import Tag from '../tile/tag.vue';
async function mounted() { async function mounted() {
this.tags = await this.$store.dispatch('fetchTags', { priority: [9] }); const tags = await this.$store.dispatch('fetchTags', {
slug: [
'airtight',
'anal',
'double-anal',
'double-penetration',
'double-vaginal',
'da-tp',
'dv-tp',
'triple-anal',
'blowbang',
'gangbang',
'mff',
'mfm',
'orgy',
'asian',
'caucasian',
'ebony',
'interracial',
'latina',
'anal-creampie',
'bukkake',
'creampie',
'facial',
'oral-creampie',
'swallowing',
],
});
this.tags = tags.reduce((acc, tag) => {
if (acc[tag.group.slug]) {
return { ...acc, [tag.group.slug]: [...acc[tag.group.slug], tag] };
}
return { ...acc, [tag.group.slug]: [tag] };
}, {});
} }
export default { export default {
@ -21,7 +90,7 @@ export default {
}, },
data() { data() {
return { return {
tags: [], tags: {},
}; };
}, },
mounted, mounted,
@ -30,9 +99,12 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.tags { .tags {
padding: 1rem;
}
.tiles {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
grid-gap: .5rem; grid-gap: .5rem;
padding: 1rem;
} }
</style> </style>

View File

@ -4,15 +4,14 @@
:title="tag.name" :title="tag.name"
class="tile" class="tile"
> >
<span class="title">{{ tag.name }}</span>
<img <img
v-if="imageAvailable" v-if="tag.poster"
:src="`/img/tags/${tag.slug}_thumb.jpg`" :src="`/media/${tag.poster.thumbnail}`"
:alt="tag.name" :alt="tag.name"
class="poster" class="poster"
@error="imageAvailable = false"
> >
<span class="title">{{ tag.name }}</span>
</a> </a>
</template> </template>
@ -24,11 +23,6 @@ export default {
default: null, default: null,
}, },
}, },
data() {
return {
imageAvailable: true,
};
},
}; };
</script> </script>
@ -36,7 +30,8 @@ export default {
@import 'theme'; @import 'theme';
.tile { .tile {
background: $background; color: $text-contrast;
background: $profile;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -53,20 +48,13 @@ export default {
} }
.title { .title {
color: $text; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1rem; font-size: 1rem;
font-weight: bold;
padding: .5rem 1rem; padding: .5rem 1rem;
} font-weight: bold;
text-transform: capitalize;
.title {
color: $text;
height: 100%;
display: flex;
align-items: center;
margin: 0;
} }
</style> </style>

View File

@ -1,4 +1,5 @@
/* $primary: #ff886c; */ /* $primary: #ff886c; */
$breakpoint0: 540px;
$breakpoint: 720px; $breakpoint: 720px;
$breakpoint2: 900px; $breakpoint2: 900px;
$breakpoint3: 1200px; $breakpoint3: 1200px;

View File

@ -1,7 +1,8 @@
import config from 'config'; import config from 'config';
async function get(endpoint, query = {}) { async function get(endpoint, query = {}) {
const q = new URLSearchParams(query).toString(); const curatedQuery = Object.entries(query).reduce((acc, [key, value]) => (value ? { ...acc, [key]: value } : acc), {}); // remove empty values
const q = new URLSearchParams(curatedQuery).toString();
const res = await fetch(`${config.api.url}${endpoint}?${q}`, { const res = await fetch(`${config.api.url}${endpoint}?${q}`, {
method: 'GET', method: 'GET',

View File

@ -1,12 +1,23 @@
import { get } from '../api'; import { get } from '../api';
function initTagsActions(store, _router) { function initTagsActions(store, _router) {
async function fetchTags({ _commit }, { tagId, limit = 100, priority }) { async function fetchTags({ _commit }, {
tagId,
limit = 100,
slug,
group,
priority,
}) {
if (tagId) { if (tagId) {
return get(`/tags/${tagId}`); return get(`/tags/${tagId}`);
} }
return get('/tags', { limit, priority }); return get('/tags', {
limit,
slug,
priority,
group,
});
} }
async function fetchTagReleases({ _commit }, tagId) { async function fetchTagReleases({ _commit }, tagId) {

View File

@ -228,6 +228,7 @@ exports.up = knex => Promise.resolve()
table.string('quality', 6); table.string('quality', 6);
table.string('hash'); table.string('hash');
table.text('comment');
table.string('source', 1000); table.string('source', 1000);
table.unique(['domain', 'target_id', 'role', 'hash']); table.unique(['domain', 'target_id', 'role', 'hash']);

118
package-lock.json generated
View File

@ -9693,6 +9693,124 @@
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
}, },
"showdown": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/showdown/-/showdown-1.9.1.tgz",
"integrity": "sha512-9cGuS382HcvExtf5AHk7Cb4pAeQQ+h0eTr33V1mu+crYWV4KvWAw6el92bDrqGEk5d46Ai/fhbEUwqJ/mTCNEA==",
"requires": {
"yargs": "^14.2"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"requires": {
"locate-path": "^3.0.0"
}
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"p-limit": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
"integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==",
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"requires": {
"p-limit": "^2.0.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"requires": {
"ansi-regex": "^4.1.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"yargs": {
"version": "14.2.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.2.tgz",
"integrity": "sha512-/4ld+4VV5RnrynMhPZJ/ZpOCGSCeghMykZ3BhdFBDa9Wy/RH6uEGNWDJog+aUlq+9OM1CFTgtYRW5Is1Po9NOA==",
"requires": {
"cliui": "^5.0.0",
"decamelize": "^1.2.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^15.0.0"
}
},
"yargs-parser": {
"version": "15.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.0.tgz",
"integrity": "sha512-xLTUnCMc4JhxrPEPUYD5IBR1mWCK/aT6+RJ/K29JY2y1vD+FhtgKK0AXRWvI262q3QSffAQuTouFIKUuHX89wQ==",
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
"sigmund": { "sigmund": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",

View File

@ -86,6 +86,7 @@
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",
"sharp": "^0.23.2", "sharp": "^0.23.2",
"showdown": "^1.9.1",
"tough-cookie": "^3.0.1", "tough-cookie": "^3.0.1",
"tty-table": "^2.7.0", "tty-table": "^2.7.0",
"url-pattern": "^1.0.3", "url-pattern": "^1.0.3",

View File

@ -629,7 +629,7 @@
.networks[data-v-4709d404] { .networks[data-v-4709d404] {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, 15rem); grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
grid-gap: 1rem; grid-gap: 1rem;
padding: 1rem; padding: 1rem;
} }
@ -644,9 +644,6 @@
.photos .avatar-link[data-v-0a0430c7] { .photos .avatar-link[data-v-0a0430c7] {
display: none; display: none;
} }
.photo-link[data-v-0a0430c7] {
height: 15rem;
}
.photo[data-v-0a0430c7] { .photo[data-v-0a0430c7] {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -923,26 +920,57 @@
} }
/* $primary: #ff886c; */ /* $primary: #ff886c; */
.description a {
color: #cc4466;
text-decoration: inherit;
}
.description a:hover {
color: #ff6c88;
}
/* $primary: #ff886c; */
.tag[data-v-7f130e7f] {
display: flex;
flex-direction: row;
flex-grow: 1;
justify-content: stretch;
}
.sidebar[data-v-7f130e7f] {
background: #222;
color: #fff;
width: 25rem;
box-sizing: border-box;
padding: 1rem;
}
.poster[data-v-7f130e7f] { .poster[data-v-7f130e7f] {
width: 30rem; width: 100%;
height: 18rem; height: 15rem;
-o-object-fit: cover; -o-object-fit: cover;
object-fit: cover; object-fit: cover;
} }
.title[data-v-7f130e7f] { .title[data-v-7f130e7f] {
display: inline-block; padding: 0;
padding: 1rem; margin: 1rem 0;
margin: 0 .5rem 0 0;
text-transform: capitalize; text-transform: capitalize;
} }
.title .icon[data-v-7f130e7f] { .title .icon[data-v-7f130e7f] {
fill: #fff;
width: 1.25rem; width: 1.25rem;
height: 1.25rem; height: 1.25rem;
} }
.description[data-v-7f130e7f] {
padding: 0;
margin: 0 0 1rem 0;
line-height: 1.5;
}
.photo[data-v-7f130e7f] {
width: 100%;
}
/* $primary: #ff886c; */ /* $primary: #ff886c; */
.tile[data-v-602c6fd8] { .tile[data-v-602c6fd8] {
background: #fff; color: #fff;
background: #222;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@ -958,27 +986,23 @@
object-fit: cover; object-fit: cover;
} }
.title[data-v-602c6fd8] { .title[data-v-602c6fd8] {
color: #222; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1rem; font-size: 1rem;
font-weight: bold;
padding: .5rem 1rem; padding: .5rem 1rem;
} font-weight: bold;
.title[data-v-602c6fd8] { text-transform: capitalize;
color: #222;
height: 100%;
display: flex;
align-items: center;
margin: 0;
} }
.tags[data-v-66fa6284] { .tags[data-v-66fa6284] {
padding: 1rem;
}
.tiles[data-v-66fa6284] {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr)); grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
grid-gap: .5rem; grid-gap: .5rem;
padding: 1rem;
} }
/* $primary: #ff886c; */ /* $primary: #ff886c; */
@ -1146,8 +1170,9 @@ body {
display: inline-block; display: inline-block;
} }
.nav-link[data-v-10b7ec04] { .nav-link[data-v-10b7ec04] {
display: inline-flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
padding: 1rem; padding: 1rem;
border-bottom: solid 5px transparent; border-bottom: solid 5px transparent;
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
@ -1172,6 +1197,18 @@ body {
.nav-link:hover:not(.active) .icon[data-v-10b7ec04] { .nav-link:hover:not(.active) .icon[data-v-10b7ec04] {
fill: #ff6c88; fill: #ff6c88;
} }
@media (max-width: 540px) {
.nav-label[data-v-10b7ec04] {
display: none;
}
.nav .nolist[data-v-10b7ec04] {
display: flex;
}
.nav[data-v-10b7ec04],
.nav-item[data-v-10b7ec04] {
flex-grow: 1;
}
}
/* $primary: #ff886c; */ /* $primary: #ff886c; */
.container { .container {

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@ -17,6 +17,10 @@ const groups = [
slug: 'ethnicity', slug: 'ethnicity',
name: 'Ethnicity', name: 'Ethnicity',
}, },
{
slug: 'finish',
name: 'Finish',
},
{ {
slug: 'group', slug: 'group',
name: 'Group sex', name: 'Group sex',
@ -65,7 +69,7 @@ function getTags(groupsMap) {
name: 'airtight', name: 'airtight',
slug: 'airtight', slug: 'airtight',
alias_for: null, alias_for: null,
description: 'A cock in every penetrable hole (of a woman); one in the mouth, one in the vagina, and one in the asshole.', description: 'Stuffing one cock in her ass, one in her pussy, and one in her mouth, filling all of her penetrable holes and sealing her airtight like a figurative balloon. In other words, simultaneously getting [double penetrated](/tag/double-penetration), and giving a [blowjob](/tag/blowjob) or getting [facefucked](/tag/facefuck). Being airtight implies being [gangbanged](/tag/gangbang).',
priority: 9, priority: 9,
group_id: groupsMap.penetration, group_id: groupsMap.penetration,
}, },
@ -74,24 +78,21 @@ function getTags(groupsMap) {
slug: 'amateur', slug: 'amateur',
alias_for: null, alias_for: null,
}, },
{
name: 'american',
slug: 'american',
alias_for: null,
group_id: groupsMap.ethnicity,
},
{ {
name: 'anal creampie', name: 'anal creampie',
slug: 'anal-creampie', slug: 'anal-creampie',
priority: 7,
alias_for: null, alias_for: null,
description: 'Ejaculating into the asshole.', description: 'Ejaculating into the asshole.',
group_id: groupsMap.finish,
}, },
{ {
name: 'anal', name: 'anal',
slug: 'anal', slug: 'anal',
description: 'Penetrating the asshole with a (real) dick.', description: 'Taking a cock in the asshole.',
priority: 9, priority: 9,
alias_for: null, alias_for: null,
group_id: groupsMap.penetration,
}, },
{ {
name: 'ass fingering', name: 'ass fingering',
@ -102,6 +103,7 @@ function getTags(groupsMap) {
{ {
name: 'anal fisting', name: 'anal fisting',
slug: 'anal-fisting', slug: 'anal-fisting',
description: 'Shoving an entire hand into the asshole.',
alias_for: null, alias_for: null,
}, },
{ {
@ -112,11 +114,13 @@ function getTags(groupsMap) {
{ {
name: 'anal toys', name: 'anal toys',
slug: 'anal-toys', slug: 'anal-toys',
description: 'Stuffing a toy, such as a dildo or buttplug, into the ass',
alias_for: null, alias_for: null,
}, },
{ {
name: 'asian', name: 'asian',
slug: 'asian', slug: 'asian',
priority: 7,
alias_for: null, alias_for: null,
group_id: groupsMap.ethnicity, group_id: groupsMap.ethnicity,
}, },
@ -129,7 +133,8 @@ function getTags(groupsMap) {
{ {
name: 'ass to mouth', name: 'ass to mouth',
slug: 'ass-to-mouth', slug: 'ass-to-mouth',
priority: 8, 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, alias_for: null,
}, },
{ {
@ -157,6 +162,7 @@ function getTags(groupsMap) {
{ {
name: 'BDSM', name: 'BDSM',
slug: 'bdsm', slug: 'bdsm',
priority: 8,
alias_for: null, alias_for: null,
}, },
{ {
@ -203,11 +209,14 @@ function getTags(groupsMap) {
{ {
name: 'blowjob', name: 'blowjob',
slug: 'blowjob', slug: 'blowjob',
priority: 7,
alias_for: null, alias_for: null,
}, },
{ {
name: 'blowbang', name: 'blowbang',
slug: 'blowbang', slug: 'blowbang',
priority: 9,
description: 'Pleasuring a gang of three or more cocks by sucking and jerking off as many cocks as they can, often getting [facefucked](/tag/facefuck), groped and rubbed out, and followed by a [bukkake](/tag/bukkake). If they are getting fucked, it is a [gangbang](/tag/gangbang).',
alias_for: null, alias_for: null,
group_id: groupsMap.group, group_id: groupsMap.group,
}, },
@ -225,7 +234,10 @@ function getTags(groupsMap) {
{ {
name: 'bukkake', name: 'bukkake',
slug: 'bukkake', slug: 'bukkake',
priority: 8,
description: 'Getting ejaculated on the face by a group of three or more men, often following a [blowbang](/tag/blowbang) or [gangbang](/tag/gangbang).',
alias_for: null, alias_for: null,
group_id: groupsMap.finish,
}, },
{ {
name: 'cheerleader', name: 'cheerleader',
@ -256,7 +268,10 @@ function getTags(groupsMap) {
{ {
name: 'creampie', name: 'creampie',
slug: 'creampie', slug: 'creampie',
priority: 8,
description: 'Ejaculalating into her pussy, often shown visibly dripping out afterwards.',
alias_for: null, alias_for: null,
group_id: groupsMap.finish,
}, },
{ {
name: 'cum licking', name: 'cum licking',
@ -284,13 +299,25 @@ function getTags(groupsMap) {
alias_for: null, alias_for: null,
}, },
{ {
name: 'double anal penetration', name: 'double anal',
slug: 'double-anal', slug: 'double-anal',
description: 'Two cocks in the ass at the same time. If there\'s a third cock in her pussy, it is [double anal TP](/tag/da-tp).',
priority: 8,
alias_for: null, alias_for: null,
group_id: groupsMap.penetration,
},
{
name: 'triple anal',
slug: 'triple-anal',
description: 'Getting fucked in the ass by not one, two but *three* cocks at the same time.',
priority: 7,
alias_for: null,
group_id: groupsMap.penetration,
}, },
{ {
name: 'deepthroat', name: 'deepthroat',
slug: 'deepthroat', slug: 'deepthroat',
priority: 7,
alias_for: null, alias_for: null,
}, },
{ {
@ -298,6 +325,8 @@ function getTags(groupsMap) {
slug: 'double-penetration', slug: 'double-penetration',
priority: 9, priority: 9,
alias_for: null, alias_for: null,
description: 'Fucking two cocks at once, with one in her ass, and one in her pussy. If she has another cock in her mouth, she is [airtight](/tag/airtight).',
group_id: groupsMap.penetration,
}, },
{ {
name: 'dungeon', name: 'dungeon',
@ -305,9 +334,12 @@ function getTags(groupsMap) {
alias_for: null, alias_for: null,
}, },
{ {
name: 'double vaginal penetration', name: 'double vaginal',
slug: 'double-vaginal', slug: 'double-vaginal',
description: 'Fucking her pussy with two cocks at the same time. If there\'s a third cock in her asshole, it is [double vaginal TP](/tag/dv-tp).',
priority: 8,
alias_for: null, alias_for: null,
group_id: groupsMap.penetration,
}, },
{ {
name: 'double blowjob', name: 'double blowjob',
@ -328,6 +360,7 @@ function getTags(groupsMap) {
{ {
name: 'ebony', name: 'ebony',
slug: 'ebony', slug: 'ebony',
priority: 7,
alias_for: null, alias_for: null,
group_id: groupsMap.ethnicity, group_id: groupsMap.ethnicity,
}, },
@ -341,15 +374,10 @@ function getTags(groupsMap) {
slug: 'enhanced-boobs', slug: 'enhanced-boobs',
alias_for: null, alias_for: null,
}, },
{
name: 'European',
slug: 'european',
alias_for: null,
group_id: groupsMap.ethnicity,
},
{ {
name: 'facefuck', name: 'facefuck',
slug: 'facefuck', slug: 'facefuck',
priority: 9,
alias_for: null, alias_for: null,
group_id: groupsMap.position, group_id: groupsMap.position,
}, },
@ -363,6 +391,7 @@ function getTags(groupsMap) {
name: 'facial', name: 'facial',
slug: 'facial', slug: 'facial',
alias_for: null, alias_for: null,
group_id: groupsMap.finish,
}, },
{ {
name: 'feet', name: 'feet',
@ -385,8 +414,10 @@ function getTags(groupsMap) {
alias_for: null, alias_for: null,
}, },
{ {
name: 'FMF threesome', name: 'MFF threesome',
slug: 'fmf', slug: 'mff',
priority: 9,
description: 'A threesome with two women and one guy, in which the women have sex with eachother.',
alias_for: null, alias_for: null,
group_id: groupsMap.group, group_id: groupsMap.group,
}, },
@ -398,10 +429,19 @@ function getTags(groupsMap) {
{ {
name: 'gangbang', name: 'gangbang',
slug: 'gangbang', slug: 'gangbang',
description: 'A group of three or more guys fucking a woman, at least two at the same time, often but not necessarily involving a [blowbang](/tag/blowbang), [double penetration](/tag/airtight) and [airtight](/tag/airtight). If she only gets fucked by one guy at a time, it might be considered a [trainbang](/tag/trainbang) instead. In a reverse gangbang, multiple women fuck one man.',
alias_for: null, alias_for: null,
priority: 9, priority: 9,
group_id: groupsMap.group, group_id: groupsMap.group,
}, },
{
name: 'trainbang',
slug: 'trainbang',
description: 'A group of three or more guys fucking a woman as in a [gangbang](/tag/gangbang), but one after the other, and never at the same time.',
priority: 7,
alias_for: null,
group_id: groupsMap.group,
},
{ {
name: 'gapes', name: 'gapes',
slug: 'gapes', slug: 'gapes',
@ -435,12 +475,6 @@ function getTags(groupsMap) {
alias_for: null, alias_for: null,
group_id: groupsMap.clothing, group_id: groupsMap.clothing,
}, },
{
name: 'hungarian',
slug: 'hungarian',
alias_for: null,
group_id: groupsMap.ethnicity,
},
{ {
name: 'humiliation', name: 'humiliation',
slug: 'humiliation', slug: 'humiliation',
@ -456,6 +490,7 @@ function getTags(groupsMap) {
slug: 'interracial', slug: 'interracial',
priority: 9, priority: 9,
alias_for: null, alias_for: null,
group_id: groupsMap.ethnicity,
}, },
{ {
name: 'kissing', name: 'kissing',
@ -470,7 +505,9 @@ function getTags(groupsMap) {
{ {
name: 'Latina', name: 'Latina',
slug: 'latina', slug: 'latina',
priority: 7,
alias_for: null, alias_for: null,
group_id: groupsMap.ethnicity,
}, },
{ {
name: 'leather', name: 'leather',
@ -480,6 +517,7 @@ function getTags(groupsMap) {
{ {
name: 'lesbian', name: 'lesbian',
slug: 'lesbian', slug: 'lesbian',
priority: 9,
alias_for: null, alias_for: null,
}, },
{ {
@ -513,6 +551,8 @@ function getTags(groupsMap) {
{ {
name: 'MFM threesome', name: 'MFM threesome',
slug: 'mfm', slug: 'mfm',
priority: 9,
description: 'Two men fucking one woman, but not eachother. Typically involves a \'spitroast\', where one guy gets a blowjob and the other fucks her pussy.',
alias_for: null, alias_for: null,
group_id: groupsMap.group, group_id: groupsMap.group,
}, },
@ -542,11 +582,15 @@ function getTags(groupsMap) {
{ {
name: 'oral creampie', name: 'oral creampie',
slug: 'oral-creampie', slug: 'oral-creampie',
priority: 7,
alias_for: null, alias_for: null,
group_id: groupsMap.finish,
}, },
{ {
name: 'orgy', name: 'orgy',
slug: 'orgy', slug: 'orgy',
priority: 9,
description: 'A group of (at least four) people having sex with eachother. If only one person is getting fucked, it is probably a [gangbang](/tag/gangbang).',
alias_for: null, alias_for: null,
group_id: groupsMap.group, group_id: groupsMap.group,
}, },
@ -610,14 +654,9 @@ function getTags(groupsMap) {
{ {
name: 'rough', name: 'rough',
slug: 'rough', slug: 'rough',
priority: 7,
alias_for: null, alias_for: null,
}, },
{
name: 'russian',
slug: 'russian',
alias_for: null,
group_id: groupsMap.ethnicity,
},
{ {
name: 'saliva', name: 'saliva',
slug: 'saliva', slug: 'saliva',
@ -729,6 +768,7 @@ function getTags(groupsMap) {
name: 'swallowing', name: 'swallowing',
slug: 'swallowing', slug: 'swallowing',
alias_for: null, alias_for: null,
group_id: groupsMap.finish,
}, },
{ {
name: 'tattoo', name: 'tattoo',
@ -764,9 +804,27 @@ function getTags(groupsMap) {
priority: 10, priority: 10,
alias_for: null, alias_for: null,
}, },
{
name: 'double anal TP',
slug: 'da-tp',
priority: 7,
description: 'Triple penetration with two cocks in the ass, and one in the pussy. Also see [double vaginal TP](/tag/dv-tp).',
group_id: groupsMap.penetration,
alias_for: null,
},
{
name: 'double vaginal TP',
slug: 'dv-tp',
priority: 7,
description: 'Triple penetration with two cocks in the pussy, and one in the ass. Also see [double anal TP](/tag/da-tp).',
group_id: groupsMap.penetration,
alias_for: null,
},
{ {
name: 'triple penetration', name: 'triple penetration',
slug: 'triple-penetration', slug: 'triple-penetration',
priority: 7,
description: 'Three cocks fucking her from behind at the same time. This can be either [double anal TP](/tag/da-tp), or [double vaginal TP](/tag/dv-tp).',
alias_for: null, alias_for: null,
}, },
{ {
@ -795,8 +853,9 @@ function getTags(groupsMap) {
alias_for: null, alias_for: null,
}, },
{ {
name: 'white', name: 'caucasian',
slug: 'white', slug: 'caucasian',
priority: 7,
alias_for: null, alias_for: null,
group_id: groupsMap.ethnicity, group_id: groupsMap.ethnicity,
}, },
@ -878,11 +937,11 @@ function getTagAliases(tagsMap) {
}, },
{ {
name: 'fmf', name: 'fmf',
alias_for: tagsMap.fmf, alias_for: tagsMap.mff,
}, },
{ {
name: 'ffm', name: 'ffm',
alias_for: tagsMap.fmf, alias_for: tagsMap.mff,
}, },
{ {
name: 'bgb', name: 'bgb',
@ -1112,6 +1171,10 @@ function getTagAliases(tagsMap) {
name: 'double anal penetration (dap)', name: 'double anal penetration (dap)',
alias_for: tagsMap['double-anal'], alias_for: tagsMap['double-anal'],
}, },
{
name: 'tap',
alias_for: tagsMap['triple-anal'],
},
{ {
name: 'dpp', name: 'dpp',
alias_for: tagsMap['double-vaginal'], alias_for: tagsMap['double-vaginal'],
@ -1436,6 +1499,10 @@ function getTagAliases(tagsMap) {
name: 'whipping', name: 'whipping',
alias_for: tagsMap['corporal-punishment'], alias_for: tagsMap['corporal-punishment'],
}, },
{
name: 'white',
alias_for: tagsMap.caucasian,
},
{ {
name: 'work', name: 'work',
alias_for: tagsMap.office, alias_for: tagsMap.office,

220
seeds/04_media.js Normal file
View File

@ -0,0 +1,220 @@
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.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/poster.jpeg',
target_id: tagsMap['da-tp'],
role: 'poster',
comment: 'Ninel Mojado aka Mira Cuckold in GIO063 for LegalPorno',
},
{
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/dv-tp/poster.jpeg',
target_id: tagsMap['dv-tp'],
role: 'poster',
comment: 'Juelz Ventura in "Gangbanged 5" for Elegant Angel',
},
{
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/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',
}));
}
/* 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 duplicatesByPath = duplicates.reduce((acc, file) => ({ ...acc, [file.path]: file }), {});
const tagsMap = tags.reduce((acc, { id, slug }) => ({ ...acc, [slug]: id }), {});
const media = getMedia(tagsMap);
return upsert('media', media, duplicatesByPath, 'path', knex);
});

View File

@ -375,28 +375,31 @@ async function scrapeBasicActors() {
return scrapeActors(basicActors.map(actor => actor.name)); return scrapeActors(basicActors.map(actor => actor.name));
} }
async function associateActors(release, releaseId) { async function associateActors(mappedActors, releases) {
const actorEntries = await knex('actors').whereIn('name', release.actors); const [existingActorEntries, existingAssociationEntries] = await Promise.all([
knex('actors').whereIn('name', Object.keys(mappedActors)),
const newActors = release.actors knex('actors_associated').whereIn('release_id', releases.map(release => release.id)),
.map(actorName => actorName.trim())
.filter(actorName => !actorEntries.some(actor => actor.name === actorName));
const [newActorEntries, associatedActors] = await Promise.all([
Promise.all(newActors.map(async actorName => storeActor({ name: actorName }))),
knex('actors_associated').where('release_id', releaseId),
]); ]);
const newlyAssociatedActors = actorEntries const associations = await Promise.map(Object.entries(mappedActors), async ([actorName, releaseIds]) => {
.concat(newActorEntries) const actorEntry = existingActorEntries.find(actor => actor.name === actorName)
.filter(actorEntry => !associatedActors.some(actor => actorEntry.id === actor.id)) || await storeActor({ name: actorName });
.map(actor => ({
release_id: releaseId,
actor_id: actor.id,
}));
await knex('actors_associated') return releaseIds
.insert(newlyAssociatedActors); .map(releaseId => ({
release_id: releaseId,
actor_id: actorEntry.id,
}))
.filter(association => !existingAssociationEntries
// remove associations already in database
.some(associationEntry => associationEntry.actor_id === association.actor_id
&& associationEntry.release_id === association.release_id));
});
await Promise.all([
knex('actors_associated').insert(associations.flat()),
scrapeBasicActors(),
]);
} }
module.exports = { module.exports = {

View File

@ -29,7 +29,7 @@ async function getThumbnail(buffer) {
} }
async function createReleaseMediaDirectory(release, releaseId) { async function createReleaseMediaDirectory(release, releaseId) {
if (release.poster || (release.photos && release.photos.length)) { if (release.poster || (release.photos && release.photos.length) || release.trailer) {
await fs.mkdir( await fs.mkdir(
path.join(config.media.path, 'releases', release.site.network.slug, release.site.slug, releaseId.toString()), path.join(config.media.path, 'releases', release.site.network.slug, release.site.slug, releaseId.toString()),
{ recursive: true }, { recursive: true },
@ -133,7 +133,7 @@ async function storePhotos(release, releaseId) {
return null; return null;
} }
}, { }, {
concurrency: 2, concurrency: 10,
}); });
await knex('media') await knex('media')
@ -225,7 +225,7 @@ async function storeAvatars(profile, actor) {
return null; return null;
} }
}, { }, {
concurrency: 2, concurrency: 10,
}); });
const avatars = files.filter(file => file); const avatars = files.filter(file => file);

View File

@ -197,7 +197,6 @@ async function storeReleaseAssets(release, releaseId) {
await createReleaseMediaDirectory(release, releaseId); await createReleaseMediaDirectory(release, releaseId);
await Promise.all([ await Promise.all([
associateActors(release, releaseId),
associateTags(release, releaseId), associateTags(release, releaseId),
storePhotos(release, releaseId), storePhotos(release, releaseId),
storePoster(release, releaseId), storePoster(release, releaseId),
@ -222,36 +221,59 @@ async function storeRelease(release) {
}) })
.returning('*'); .returning('*');
await storeReleaseAssets(release, existingRelease.id); // await storeReleaseAssets(release, existingRelease.id);
console.log(`Updated release "${release.title}" (${existingRelease.id}, ${release.site.name})`); console.log(`Updated release "${release.title}" (${existingRelease.id}, ${release.site.name})`);
return updatedRelease || existingRelease; return updatedRelease ? updatedRelease.id : existingRelease.id;
} }
const [releaseEntry] = await knex('releases') const [releaseEntry] = await knex('releases')
.insert(curatedRelease) .insert(curatedRelease)
.returning('*'); .returning('*');
await storeReleaseAssets(release, releaseEntry.id); // await storeReleaseAssets(release, releaseEntry.id);
console.log(`Stored release "${release.title}" (${releaseEntry.id}, ${release.site.name})`); console.log(`Stored release "${release.title}" (${releaseEntry.id}, ${release.site.name})`);
return releaseEntry.id; return releaseEntry.id;
} }
async function storeReleases(releases) { async function storeReleases(releases) {
return Promise.map(releases, async (release) => { const storedReleases = await Promise.map(releases, async (release) => {
try { try {
const releaseId = await storeRelease(release); const releaseId = await storeRelease(release);
return releaseId; return {
id: releaseId,
...release,
};
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return null; return null;
} }
}, { }, {
concurrency: 2, concurrency: 10,
}); });
const actors = storedReleases.reduce((acc, release) => {
release.actors.forEach((actor) => {
const trimmedActor = actor.trim();
if (acc[trimmedActor]) {
acc[trimmedActor] = acc[trimmedActor].concat(release.id);
return;
}
acc[trimmedActor] = [release.id];
});
return acc;
}, {});
await Promise.all([
associateActors(actors, storedReleases),
Promise.all(storedReleases.map(async release => storeReleaseAssets(release, release.id))),
]);
} }
module.exports = { module.exports = {

View File

@ -7,7 +7,6 @@ const scrapers = require('./scrapers/scrapers');
const { storeReleases } = require('./releases'); const { storeReleases } = require('./releases');
const { findSiteByUrl } = require('./sites'); const { findSiteByUrl } = require('./sites');
const { findNetworkByUrl } = require('./networks'); const { findNetworkByUrl } = require('./networks');
const { scrapeBasicActors } = require('./actors');
async function findSite(url, release) { async function findSite(url, release) {
const site = (release && release.site) || await findSiteByUrl(url); const site = (release && release.site) || await findSiteByUrl(url);
@ -50,7 +49,6 @@ async function scrapeRelease(url, release, deep = false) {
if (!deep && argv.save) { if (!deep && argv.save) {
// don't store release when called by site scraper // don't store release when called by site scraper
const [releaseId] = await storeReleases([scene]); const [releaseId] = await storeReleases([scene]);
await scrapeBasicActors();
console.log(`http://${config.web.host}:${config.web.port}/scene/${releaseId}`); console.log(`http://${config.web.host}:${config.web.port}/scene/${releaseId}`);
} }

View File

@ -9,7 +9,6 @@ const { fetchIncludedSites } = require('./sites');
const scrapers = require('./scrapers/scrapers'); const scrapers = require('./scrapers/scrapers');
const scrapeRelease = require('./scrape-release'); const scrapeRelease = require('./scrape-release');
const { storeReleases } = require('./releases'); const { storeReleases } = require('./releases');
const { scrapeBasicActors } = require('./actors');
function getAfterDate() { function getAfterDate() {
return moment return moment
@ -103,40 +102,39 @@ async function scrapeSiteReleases(scraper, site) {
} }
async function scrapeReleases() { async function scrapeReleases() {
const sites = await fetchIncludedSites(); const networks = await fetchIncludedSites();
console.log(`Found ${sites.length} sites in database`); const scrapedReleases = await Promise.map(networks, async network => Promise.map(network.sites, async (site) => {
await Promise.map(sites, async (site) => {
const scraper = scrapers.releases[site.slug] || scrapers.releases[site.network.slug]; const scraper = scrapers.releases[site.slug] || scrapers.releases[site.network.slug];
if (!scraper) { if (!scraper) {
console.warn(`No scraper found for '${site.name}' (${site.slug})`); console.warn(`No scraper found for '${site.name}' (${site.slug})`);
return; return [];
} }
try { try {
const siteReleases = await scrapeSiteReleases(scraper, site); return await scrapeSiteReleases(scraper, site);
const siteActors = siteReleases.reduce((acc, release) => [...acc, ...release.actors], []);
console.log(siteActors);
if (argv.save) {
await storeReleases(siteReleases);
}
} catch (error) { } catch (error) {
if (argv.debug) { if (argv.debug) {
console.error(`${site.id}: Failed to scrape releases`, error); console.error(`${site.id}: Failed to scrape releases`, error);
return;
} }
console.warn(`${site.id}: Failed to scrape releases`); console.warn(`${site.id}: Failed to scrape releases`);
return [];
} }
}, { }, {
// 2 network sites at a time
concurrency: 2, concurrency: 2,
}),
{
// 5 networks at a time
concurrency: 5,
}); });
await scrapeBasicActors(); if (argv.save) {
await storeReleases(scrapedReleases.flat(2));
}
} }
module.exports = scrapeReleases; module.exports = scrapeReleases;

View File

@ -72,6 +72,25 @@ async function findSiteByUrl(url) {
return null; return null;
} }
function sitesByNetwork(sites) {
const networks = sites.reduce((acc, site) => {
if (acc[site.network.slug]) {
acc[site.network.slug].sites = acc[site.network.slug].sites.concat(site);
return acc;
}
acc[site.network.slug] = {
...site.network,
sites: [site],
};
return acc;
}, {});
return Object.values(networks);
}
async function fetchSitesFromArgv() { async function fetchSitesFromArgv() {
const rawSites = await knex('sites') const rawSites = await knex('sites')
.select('sites.*', 'networks.name as network_name', 'networks.slug as network_slug', 'networks.parameters as network_parameters') .select('sites.*', 'networks.name as network_name', 'networks.slug as network_slug', 'networks.parameters as network_parameters')
@ -79,7 +98,10 @@ async function fetchSitesFromArgv() {
.orWhereIn('networks.slug', argv.networks || []) .orWhereIn('networks.slug', argv.networks || [])
.leftJoin('networks', 'sites.network_id', 'networks.id'); .leftJoin('networks', 'sites.network_id', 'networks.id');
return curateSites(rawSites, true); const curatedSites = await curateSites(rawSites, true);
console.log(`Found ${curatedSites.length} sites in database`);
return sitesByNetwork(curatedSites);
} }
async function fetchSitesFromConfig() { async function fetchSitesFromConfig() {
@ -94,7 +116,10 @@ async function fetchSitesFromConfig() {
.orWhereIn('network_id', networkIds) .orWhereIn('network_id', networkIds)
.leftJoin('networks', 'sites.network_id', 'networks.id'); .leftJoin('networks', 'sites.network_id', 'networks.id');
return curateSites(rawSites, true); const curatedSites = await curateSites(rawSites, true);
console.log(`Found ${curatedSites.length} sites in database`);
return sitesByNetwork(curatedSites);
} }
async function fetchIncludedSites() { async function fetchIncludedSites() {

View File

@ -4,13 +4,21 @@ const knex = require('./knex');
const whereOr = require('./utils/where-or'); const whereOr = require('./utils/where-or');
async function curateTag(tag) { async function curateTag(tag) {
const aliases = await knex('tags').where({ alias_for: tag.id }); const [aliases, media] = await Promise.all([
knex('tags').where({ alias_for: tag.id }),
knex('media')
.where('domain', 'tags')
.andWhere('target_id', tag.id)
.orderBy('index'),
]);
return { return {
id: tag.id, id: tag.id,
name: tag.name, name: tag.name,
slug: tag.slug, slug: tag.slug,
description: tag.description, description: tag.description,
poster: media.find(photo => photo.role === 'poster'),
photos: media.filter(photo => photo.role === 'photo'),
group: { group: {
id: tag.group_id, id: tag.group_id,
name: tag.group_name, name: tag.group_name,
@ -31,15 +39,20 @@ async function associateTags(release, releaseId) {
return; return;
} }
await knex('tags_associated').insert(release.tags.map(tagId => ({ try {
tag_id: tagId, await knex('tags_associated').insert(release.tags.map(tagId => ({
release_id: releaseId, tag_id: tagId,
}))); release_id: releaseId,
})));
} catch (error) {
console.log(release, error);
}
} }
async function fetchTags(queryObject, limit = 100) { async function fetchTags(queryObject, groupsQueryObject, limit = 100) {
const tags = await knex('tags') const tags = await knex('tags')
.where(builder => whereOr(queryObject, 'tags', builder)) .where(builder => whereOr(queryObject, 'tags', builder))
.orWhere(builder => whereOr(groupsQueryObject, 'tags_groups', builder))
.andWhere({ 'tags.alias_for': null }) .andWhere({ 'tags.alias_for': null })
.select( .select(
'tags.*', 'tags.*',

10
src/utils/escape-html.js Normal file
View File

@ -0,0 +1,10 @@
function escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
module.exports = escapeHtml;

View File

@ -10,7 +10,7 @@ async function fetchTagsApi(req, res) {
const tags = await fetchTags({ const tags = await fetchTags({
id: tagId, id: tagId,
slug: tagSlug, slug: tagSlug,
}, req.query.limit); }, null, req.query.limit);
if (tags.length > 0) { if (tags.length > 0) {
res.send(tags[0]); res.send(tags[0]);
@ -21,9 +21,16 @@ async function fetchTagsApi(req, res) {
return; return;
} }
const tags = await fetchTags({ const query = {};
priority: req.query.priority.split(','), const groupsQuery = {};
}, req.query.limit);
if (req.query.priority) query.priority = req.query.priority.split(',');
if (req.query.slug) query.slug = req.query.slug.split(',');
if (req.query.group) {
groupsQuery.slug = req.query.group.split(',');
}
const tags = await fetchTags(query, groupsQuery, req.query.limit);
res.send(tags); res.send(tags);
} }