Improved actor scraping and display.

This commit is contained in:
ThePendulum 2020-05-18 01:22:56 +02:00
parent af5543190a
commit 8733fdc657
28 changed files with 1033 additions and 793 deletions

View File

@ -44,7 +44,7 @@
class="avatar-link"
>
<img
:src="`/media/${actor.avatar.thumbnail}`"
:src="sfw ? `/img/${actor.avatar.sfw.thumbnail}` : `/media/${actor.avatar.thumbnail}`"
:title="actor.avatar.copyright && `© ${actor.avatar.copyright}`"
class="avatar"
>
@ -153,7 +153,7 @@
<Icon
v-if="actor.naturalBoobs === false"
v-tooltip="'Enhanced boobs'"
icon="star"
icon="magic-wand2"
class="enhanced"
/>{{ actor.bust || '??' }}{{ actor.cup || '?' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}
</span>
@ -271,6 +271,10 @@ async function fetchActor() {
});
}
function sfw() {
return this.$store.state.ui.sfw;
}
async function route() {
await this.fetchActor();
}
@ -303,6 +307,9 @@ export default {
expanded: false,
};
},
computed: {
sfw,
},
watch: {
$route: route,
},
@ -495,6 +502,7 @@ export default {
.enhanced.icon {
fill: $primary;
padding: 0 .5rem;
transform: scaleX(-1);
}
.ethnicity {

View File

@ -1,60 +1,60 @@
<template>
<div class="actors">
<nav class="filter">
<ul class="genders nolist">
<li class="gender">
<router-link
:to="{ name: 'actors', params: { gender: 'female', letter } }"
:class="{ selected: gender === 'female' }"
class="gender-link female"
><Gender gender="female" /></router-link>
</li>
<li class="gender">
<router-link
:to="{ name: 'actors', params: { gender: 'male', letter } }"
:class="{ selected: gender === 'male' }"
class="gender-link male"
><Gender gender="male" /></router-link>
</li>
<li class="gender">
<router-link
:to="{ name: 'actors', params: { gender: 'trans', letter } }"
:class="{ selected: gender === 'trans' }"
class="gender-link transsexual"
><Gender gender="transsexual" /></router-link>
</li>
<li class="gender">
<router-link
:to="{ name: 'actors', params: { gender: 'other', letter } }"
:class="{ selected: gender === 'other' }"
class="gender-link other"
><Icon icon="question5" /></router-link>
</li>
</ul>
<div class="actors">
<nav class="filter">
<ul class="genders nolist">
<li class="gender">
<router-link
:to="{ name: 'actors', params: { gender: 'female', letter } }"
:class="{ selected: gender === 'female' }"
class="gender-link female"
><Gender gender="female" /></router-link>
</li>
<li class="gender">
<router-link
:to="{ name: 'actors', params: { gender: 'male', letter } }"
:class="{ selected: gender === 'male' }"
class="gender-link male"
><Gender gender="male" /></router-link>
</li>
<li class="gender">
<router-link
:to="{ name: 'actors', params: { gender: 'trans', letter } }"
:class="{ selected: gender === 'trans' }"
class="gender-link transsexual"
><Gender gender="transsexual" /></router-link>
</li>
<li class="gender">
<router-link
:to="{ name: 'actors', params: { gender: 'other', letter } }"
:class="{ selected: gender === 'other' }"
class="gender-link other"
><Icon icon="question5" /></router-link>
</li>
</ul>
<ul class="letters nolist">
<li
v-for="letterX in letters"
:key="letterX"
class="letter"
>
<router-link
:to="{ name: 'actors', params: { gender, letter: letterX } }"
:class="{ selected: letterX === letter }"
class="letter-link"
>{{ letterX || 'All' }}</router-link>
</li>
</ul>
</nav>
<ul class="letters nolist">
<li
v-for="letterX in letters"
:key="letterX"
class="letter"
>
<router-link
:to="{ name: 'actors', params: { gender, letter: letterX } }"
:class="{ selected: letterX === letter }"
class="letter-link"
>{{ letterX || 'All' }}</router-link>
</li>
</ul>
</nav>
<div class="tiles">
<Actor
v-for="actor in actors"
:key="`actor-${actor.id}`"
:actor="actor"
/>
</div>
</div>
<div class="tiles">
<Actor
v-for="actor in actors"
:key="`actor-${actor.id}`"
:actor="actor"
/>
</div>
</div>
</template>
<script>
@ -62,56 +62,56 @@ import Actor from '../tile/actor.vue';
import Gender from './gender.vue';
async function fetchActors() {
const curatedGender = this.gender.replace('trans', 'transsexual');
const curatedGender = this.gender.replace('trans', 'transsexual');
this.actors = await this.$store.dispatch('fetchActors', {
limit: 1000,
letter: this.letter.replace('all', ''),
gender: curatedGender === 'other' ? null : curatedGender,
});
this.actors = await this.$store.dispatch('fetchActors', {
limit: 1000,
letter: this.letter.replace('all', ''),
gender: curatedGender === 'other' ? null : curatedGender,
});
}
function letter() {
return this.$route.params.letter || 'all';
return this.$route.params.letter || 'all';
}
function gender() {
return this.$route.params.gender || 'female';
return this.$route.params.gender || 'female';
}
async function route() {
await this.fetchActors();
await this.fetchActors();
}
async function mounted() {
this.pageTitle = 'Actors';
this.pageTitle = 'Actors';
await this.fetchActors();
await this.fetchActors();
}
export default {
components: {
Actor,
Gender,
},
data() {
return {
actors: [],
pageTitle: null,
letters: ['all'].concat(Array.from({ length: 26 }, (value, index) => String.fromCharCode(index + 97).toUpperCase())),
};
},
computed: {
letter,
gender,
},
watch: {
$route: route,
},
mounted,
methods: {
fetchActors,
},
components: {
Actor,
Gender,
},
data() {
return {
actors: [],
pageTitle: null,
letters: ['all'].concat(Array.from({ length: 26 }, (value, index) => String.fromCharCode(index + 97).toUpperCase())),
};
},
computed: {
letter,
gender,
},
watch: {
$route: route,
},
mounted,
methods: {
fetchActors,
},
};
</script>
@ -226,7 +226,7 @@ export default {
}
}
@media(max-width: $breakpoint) {
@media(max-width: $breakpoint0) {
.tiles {
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
}

View File

@ -11,7 +11,7 @@
class="avatar-link photo-link"
>
<img
:src="`/media/${actor.avatar.thumbnail}`"
:src="sfw ? `/img/${actor.avatar.sfw.thumbnail}` : `/media/${actor.avatar.thumbnail}`"
:title="actor.avatar.copyright && `© ${actor.avatar.copyright}`"
class="avatar photo"
>
@ -26,7 +26,7 @@
class="photo-link"
>
<img
:src="`/media/${photo.thumbnail}`"
:src="sfw ? `/img/${photo.sfw.thumbnail}` : `/media/${photo.thumbnail}`"
:title="photo.copyright && `© ${photo.copyright}`"
class="photo"
>
@ -35,6 +35,10 @@
</template>
<script>
function sfw() {
return this.$store.state.ui.sfw;
}
export default {
props: {
actor: {
@ -42,6 +46,9 @@ export default {
default: null,
},
},
computed: {
sfw,
},
};
</script>

View File

@ -1,128 +1,128 @@
<template>
<header class="header">
<div class="header-nav">
<Icon
icon="menu"
class="sidebar-toggle"
@click.native.stop="toggleSidebar"
/>
<header class="header">
<div class="header-nav">
<Icon
icon="menu"
class="sidebar-toggle"
@click.native.stop="toggleSidebar"
/>
<router-link
to="/"
class="logo-link"
><h1 class="header-logo">
<div
class="logo"
v-html="logo"
/>
</h1></router-link>
<router-link
to="/"
class="logo-link"
><h1 class="header-logo">
<div
class="logo"
v-html="logo"
/>
</h1></router-link>
<nav class="nav">
<ul class="nav-list nolist">
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
to="/actors"
>
<a
class="nav-link"
:href="href"
:class="{ active: isActive }"
@click="navigate"
>Actors</a>
</router-link>
</li>
<nav class="nav">
<ul class="nav-list nolist">
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
to="/actors"
>
<a
class="nav-link"
:href="href"
:class="{ active: isActive }"
@click="navigate"
>Actors</a>
</router-link>
</li>
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
to="/networks"
>
<a
class="nav-link"
:href="href"
:class="{ active: isActive }"
@click="navigate"
>Sites</a>
</router-link>
</li>
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
to="/networks"
>
<a
class="nav-link"
:href="href"
:class="{ active: isActive }"
@click="navigate"
>Sites</a>
</router-link>
</li>
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
to="/tags"
>
<a
class="nav-link"
:href="href"
:class="{ active: isActive }"
@click="navigate"
>Tags</a>
</router-link>
</li>
</ul>
</nav>
</div>
<li class="nav-item">
<router-link
v-slot="{ href, isActive, navigate }"
to="/tags"
>
<a
class="nav-link"
:href="href"
:class="{ active: isActive }"
@click="navigate"
>Tags</a>
</router-link>
</li>
</ul>
</nav>
</div>
<div class="header-section">
<div class="header-toggles">
<Icon
v-show="!sfw"
v-tooltip="'Hit S to use SFW mode'"
icon="flower"
class="toggle noselect"
@click.native="setSfw(true)"
/>
<div class="header-section">
<div class="header-toggles">
<Icon
v-show="!sfw"
v-tooltip="'Hit S to use SFW mode'"
icon="flower"
class="toggle noselect"
@click.native="setSfw(true)"
/>
<Icon
v-show="sfw"
v-tooltip="'Hit N to use NSFW mode'"
icon="flower"
class="toggle active noselect"
@click.native="setSfw(false)"
/>
<Icon
v-show="sfw"
v-tooltip="'Hit N to use NSFW mode'"
icon="flower"
class="toggle active noselect"
@click.native="setSfw(false)"
/>
<Icon
v-show="theme === 'light'"
v-tooltip="'Hit D to use dark theme'"
icon="moon"
class="toggle noselect"
@click.native="setTheme('dark')"
/>
<Icon
v-show="theme === 'light'"
v-tooltip="'Hit D to use dark theme'"
icon="moon"
class="toggle noselect"
@click.native="setTheme('dark')"
/>
<Icon
v-show="theme === 'dark'"
v-tooltip="'Hit L to use light theme'"
icon="sun"
class="toggle noselect"
@click.native="setTheme('light')"
/>
</div>
<Icon
v-show="theme === 'dark'"
v-tooltip="'Hit L to use light theme'"
icon="sun"
class="toggle noselect"
@click.native="setTheme('light')"
/>
</div>
<Search class="search-full" />
<Search class="search-full" />
<v-popover
class="search-compact"
:open="searching"
@show="searching = true"
@hide="searching = false"
>
<button
type="button"
class="search-button"
><Icon
icon="search"
/></button>
<v-popover
class="search-compact"
:open="searching"
@show="searching = true"
@hide="searching = false"
>
<button
type="button"
class="search-button"
><Icon
icon="search"
/></button>
<Search
slot="popover"
:searching="searching"
class="compact"
@search="searching = false"
/>
</v-popover>
</div>
</header>
<Search
slot="popover"
:searching="searching"
class="compact"
@search="searching = false"
/>
</v-popover>
</div>
</header>
</template>
<script>
@ -133,47 +133,47 @@ import Search from './search.vue';
import logo from '../../img/logo.svg';
function sfw(state) {
return state.ui.sfw;
return state.ui.sfw;
}
function theme(state) {
return state.ui.theme;
return state.ui.theme;
}
function setTheme(newTheme) {
this.$store.dispatch('setTheme', newTheme);
this.$store.dispatch('setTheme', newTheme);
}
function setSfw(enabled) {
this.$store.dispatch('setSfw', enabled);
this.$store.dispatch('setSfw', enabled);
}
export default {
components: {
Search,
},
props: {
toggleSidebar: {
type: Function,
default: null,
},
},
data() {
return {
logo,
searching: false,
};
},
computed: {
...mapState({
sfw,
theme,
}),
},
methods: {
setSfw,
setTheme,
},
components: {
Search,
},
props: {
toggleSidebar: {
type: Function,
default: null,
},
},
data() {
return {
logo,
searching: false,
};
},
computed: {
...mapState({
sfw,
theme,
}),
},
methods: {
setSfw,
setTheme,
},
};
</script>

View File

@ -1,57 +1,64 @@
<template>
<form
class="search"
@submit.prevent="search"
>
<input
ref="search"
v-model="query"
type="search"
class="search-input"
placeholder="Search..."
>
<button
type="submit"
class="search-button"
><Icon
icon="search"
/></button>
</form>
<form
class="search"
@submit.prevent="search"
>
<input
ref="search"
v-model="query"
type="search"
class="search-input"
placeholder="Search..."
>
<button
type="submit"
class="search-button"
><Icon
icon="search"
/></button>
</form>
</template>
<script>
async function search() {
this.$router.push({ name: 'search', query: { q: this.query } });
this.$emit('search');
this.$router.push({ name: 'search', query: { q: this.query } });
this.$emit('search');
}
function searching(to) {
if (to) {
setTimeout(() => {
// nextTick does not seem to work
this.$refs.search.focus();
}, 20);
}
if (to) {
setTimeout(() => {
// nextTick does not seem to work
this.$refs.search.focus();
}, 20);
}
}
function route(to) {
if (to.name !== 'search') {
this.query = null;
}
}
export default {
props: {
searching: {
type: Boolean,
default: false,
},
},
data() {
return {
query: this.$route.query ? this.$route.query.q : null,
};
},
watch: {
searching,
},
methods: {
search,
},
props: {
searching: {
type: Boolean,
default: false,
},
},
data() {
return {
query: this.$route.query ? this.$route.query.q : null,
};
},
watch: {
$route: route,
searching,
},
methods: {
search,
},
};
</script>

View File

@ -1,72 +1,72 @@
<template>
<div class="releases">
<h3
v-if="context"
class="heading"
><span class="range">{{ range }}</span> releases for '{{ context }}'</h3>
<div class="releases">
<h3
v-if="context"
class="heading"
><span class="range">{{ range }}</span> releases for '{{ context }}'</h3>
<ul
:key="sfw"
v-lazy-container="{ selector: '.thumbnail' }"
class="nolist tiles"
>
<li
v-for="(release, index) in releases"
:key="`release-${release.id}`"
>
<ReleaseTile
:release="release"
:referer="referer"
:index="index"
/>
</li>
</ul>
<ul
:key="sfw"
v-lazy-container="{ selector: '.thumbnail' }"
class="nolist tiles"
>
<li
v-for="(release, index) in releases"
:key="`release-${release.id}`"
>
<ReleaseTile
:release="release"
:referer="referer"
:index="index"
/>
</li>
</ul>
<span
v-if="releases.length === 0 && range !== 'all'"
class="empty"
>No {{ range }} releases</span>
<span
v-if="releases.length === 0 && range !== 'all'"
class="empty"
>No {{ range }} releases</span>
<span
v-else-if="releases.length === 0"
class="empty"
>No recent or upcoming releases</span>
</div>
<span
v-else-if="releases.length === 0"
class="empty"
>No recent or upcoming releases</span>
</div>
</template>
<script>
import ReleaseTile from '../tile/release.vue';
function range() {
return this.$route.params.range;
return this.$route.params.range;
}
function sfw() {
return this.$store.state.ui.sfw;
return this.$store.state.ui.sfw;
}
export default {
components: {
ReleaseTile,
},
props: {
releases: {
type: Array,
default: () => [],
},
context: {
type: String,
default: null,
},
referer: {
type: String,
default: null,
},
},
computed: {
range,
sfw,
},
components: {
ReleaseTile,
},
props: {
releases: {
type: Array,
default: () => [],
},
context: {
type: String,
default: null,
},
referer: {
type: String,
default: null,
},
},
computed: {
range,
sfw,
},
};
</script>

View File

@ -1,47 +1,89 @@
<template>
<div class="content-inner">
<span
v-if="!loading"
class="summary"
>Found {{ releases.length }} results for '{{ query }}'</span>
<div class="content-inner">
<span
v-if="loading"
class="summary"
>Searching...</span>
<span
v-else
class="summary"
>Searching...</span>
<div v-if="!loading && actors.length > 0">
<span
v-if="!loading"
class="summary"
>Found {{ actors.length }} actors for '{{ query }}'</span>
<Releases :releases="releases" />
</div>
<div class="tiles">
<Actor
v-for="actor in actors"
:key="`actor-${actor.id}`"
:actor="actor.aliasFor || actor"
:alias="actor.aliasFor && actor"
/>
</div>
</div>
<div v-if="!loading && actors.length > 0">
<span
v-if="!loading"
class="summary"
>Found {{ releases.length }} releases for '{{ query }}'</span>
<Releases :releases="releases" />
</div>
</div>
</template>
<script>
import Actor from '../tile/actor.vue';
import Releases from '../releases/releases.vue';
async function search() {
const results = await this.$store.dispatch('search', {
query: this.query,
limit: 100,
});
this.loading = false;
if (results) {
this.actors = results.actors;
this.releases = results.releases;
}
}
function query() {
return this.$route.query.query || this.$route.query.q;
}
async function mounted() {
const results = await this.$store.dispatch('searchReleases', {
query: this.query,
limit: 100,
});
await this.search();
}
this.loading = false;
if (results) {
this.releases = results;
}
async function watchQuery() {
await this.search();
}
export default {
components: {
Releases,
},
data() {
return {
loading: true,
releases: [],
query: this.$route.query.query || this.$route.query.q,
};
},
mounted,
components: {
Actor,
Releases,
},
data() {
return {
loading: true,
actors: [],
releases: [],
};
},
computed: {
query,
},
watch: {
query: watchQuery,
},
mounted,
methods: {
search,
},
};
</script>
@ -54,4 +96,12 @@ export default {
color: $shadow;
font-weight: bold;
}
.tiles {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
grid-gap: 0 .5rem;
flex-grow: 1;
margin: 0 0 1rem 0;
}
</style>

View File

@ -8,29 +8,37 @@
class="link"
>
<span
v-if="actor.network"
v-tooltip.top="`${actor.name} (${actor.network.name})`"
class="handle"
>
<img
:src="`/img/logos/${actor.network.slug}/favicon.png`"
<span
v-tooltip.top="actor.name"
class="name"
>{{ actor.name }}</span>
<router-link
v-if="actor.network"
v-tooltip="actor.network.name"
:to="{ name: 'network', params: { networkSlug: actor.network.slug } }"
class="favicon"
>
<span class="name">{{ actor.name }}</span>
</span>
<img
:src="`/img/logos/${actor.network.slug}/favicon.png`"
class="favicon-icon"
>
</router-link>
<span
v-else
v-tooltip.top="actor.name"
class="handle"
>
<span class="name">{{ actor.name }}</span>
<Icon
v-if="alias"
v-tooltip="`Alias for ${alias.name}`"
icon="users3"
class="favicon alias"
/>
</span>
<div class="avatar-container">
<img
v-if="actor.avatar"
:src="`/media/${actor.avatar.thumbnail || actor.avatar}`"
:src="sfw ? `/img/${actor.avatar.sfw.thumbnail}` : `/media/${actor.avatar.thumbnail}`"
class="avatar"
>
@ -45,7 +53,9 @@
<span
class="details"
>
<span class="age">
<span class="gender-age">
<Gender :gender="actor.gender" />
<span
v-if="actor.age"
v-tooltip="`Born on ${formatDate(actor.birthdate, 'MMMM D, YYYY')}`"
@ -59,8 +69,6 @@
>{{ actor.ageThen }}</span>
</span>
<Gender :gender="actor.gender" />
<span
v-if="actor.origin"
v-tooltip="`Born in ${actor.origin.country.alias || actor.origin.country.name}`"
@ -86,6 +94,10 @@
<script>
import Gender from '../actors/gender.vue';
function sfw() {
return this.$store.state.ui.sfw;
}
export default {
components: {
Gender,
@ -95,6 +107,13 @@ export default {
type: Object,
default: null,
},
alias: {
type: Object,
default: null,
},
},
computed: {
sfw,
},
};
</script>
@ -137,18 +156,29 @@ export default {
display: flex;
align-items: center;
justify-content: center;
padding: .5rem;
font-weight: bold;
.name {
padding: .5rem;
}
.alias {
fill: var(--highlight);
}
}
.favicon {
font-size: 0;
padding: .5rem .25rem;
&:last-child {
padding: .5rem;
}
}
.favicon-icon {
width: 1rem;
height: 1rem;
margin: 0 .5rem 0 0;
& + .name {
padding: 0 1rem 0 0;
}
}
.name {
@ -156,13 +186,13 @@ export default {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
.avatar-container {
display: flex;
flex-grow: 1;
position: relative;
overflow: hidden;
}
.avatar {
@ -191,6 +221,7 @@ export default {
height: 1.75rem;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
padding: .5rem;
position: absolute;
@ -199,14 +230,13 @@ export default {
font-weight: bold;
}
.age,
.country,
.gender {
flex: 1;
.gender-age {
display: flex;
align-items: center;
}
.gender {
text-align: center;
margin: .25rem .25rem 0 0;
}
.country {
@ -216,7 +246,7 @@ export default {
}
.flag {
height: 1rem;
height: .75rem;
margin: 0 0 0 .5rem;
}

View File

@ -1,175 +1,175 @@
<template>
<div
:id="`${release.type}-${release.id}`"
:class="{ [release.type]: true }"
class="tile"
>
<span class="poster">
<span class="details">
<router-link
v-if="release.site && release.site.independent"
:to="`/network/${release.network.slug}`"
class="site site-link"
><img
:src="`/img/logos/${release.network.slug}/favicon.png`"
class="favicon"
>{{ release.network.name }}</router-link>
<div
:id="`${release.type}-${release.id}`"
:class="{ [release.type]: true }"
class="tile"
>
<span class="poster">
<span class="details">
<router-link
v-if="release.site && release.site.independent"
:to="`/network/${release.network.slug}`"
class="site site-link"
><img
:src="`/img/logos/${release.network.slug}/favicon.png`"
class="favicon"
>{{ release.network.name }}</router-link>
<span
v-else-if="release.network"
class="site"
>
<router-link
v-tooltip.bottom="`Part of ${release.network.name}`"
:title="`Part of ${release.network.name}`"
:to="`/network/${release.network.slug}`"
class="site-link"
><img
:src="`/img/logos/${release.network.slug}/favicon.png`"
class="favicon"
></router-link>
<span
v-else-if="release.network"
class="site"
>
<router-link
v-tooltip.bottom="`Part of ${release.network.name}`"
:title="`Part of ${release.network.name}`"
:to="`/network/${release.network.slug}`"
class="site-link"
><img
:src="`/img/logos/${release.network.slug}/favicon.png`"
class="favicon"
></router-link>
<router-link
v-tooltip.bottom="`More from ${release.site.name}`"
:title="`More from ${release.site.name}`"
:to="`/site/${release.site.slug}`"
class="site-link"
>{{ release.site.name }}</router-link>
</span>
<router-link
v-tooltip.bottom="`More from ${release.site.name}`"
:title="`More from ${release.site.name}`"
:to="`/site/${release.site.slug}`"
class="site-link"
>{{ release.site.name }}</router-link>
</span>
<span v-else />
<span v-else />
<a
v-if="release.date"
v-tooltip.bottom="release.url && `View scene on ${release.site.name}`"
:title="release.url && `View scene on ${release.site.name}`"
:href="release.url"
:class="{ upcoming: isAfter(release.date, new Date()), new: release.isNew }"
target="_blank"
rel="noopener noreferrer"
class="date"
>{{ formatDate(release.date, 'MMM D, YYYY') }}</a>
<a
v-if="release.date"
v-tooltip.bottom="release.url && `View scene on ${release.site.name}`"
:title="release.url && `View scene on ${release.site.name}`"
:href="release.url"
:class="{ upcoming: isAfter(release.date, new Date()), new: release.isNew }"
target="_blank"
rel="noopener noreferrer"
class="date"
>{{ formatDate(release.date, 'MMM D, YYYY') }}</a>
<a
v-else
:href="release.url"
:class="{ upcoming: isAfter(release.date, new Date()), new: release.isNew }"
title="Scene date N/A, showing date added"
target="_blank"
rel="noopener noreferrer"
class="date"
>{{ `(${formatDate(release.dateAdded, 'MMM D, YYYY')})` }}</a>
</span>
<a
v-else
:href="release.url"
:class="{ upcoming: isAfter(release.date, new Date()), new: release.isNew }"
title="Scene date N/A, showing date added"
target="_blank"
rel="noopener noreferrer"
class="date"
>{{ `(${formatDate(release.dateAdded, 'MMM D, YYYY')})` }}</a>
</span>
<a
:href="`/${release.type || 'scene'}/${release.id}/${release.slug}`"
target="_blank"
rel="noopener noreferrer"
class="link"
>
<img
v-if="release.poster"
:data-src="sfw ? `/img/${release.poster.sfw.thumbnail}` : `/media/${release.poster.thumbnail}`"
:data-loading="sfw ? `/img/${release.poster.sfw.lazy}` : `/media/${release.poster.lazy}`"
:alt="release.title"
class="thumbnail"
>
<a
:href="`/${release.type || 'scene'}/${release.id}/${release.slug}`"
target="_blank"
rel="noopener noreferrer"
class="link"
>
<img
v-if="release.poster"
:data-src="sfw ? `/img/${release.poster.sfw.thumbnail}` : `/media/${release.poster.thumbnail}`"
:data-loading="sfw ? `/img/${release.poster.sfw.lazy}` : `/media/${release.poster.lazy}`"
:alt="release.title"
class="thumbnail"
>
<span
v-else-if="release.covers && release.covers.length > 0"
class="covers"
>
<img
v-for="cover in release.covers"
:key="cover.id"
:data-src="sfw ? `/img/${cover.sfw.thumbnail}` : `/media/${cover.thumbnail}`"
:data-loading="sfw ? `/img/${cover.sfw.lazy}` : `/media/${cover.lazy}`"
:alt="release.title"
class="thumbnail cover"
>
</span>
<span
v-else-if="release.covers && release.covers.length > 0"
class="covers"
>
<img
v-for="cover in release.covers"
:key="cover.id"
:data-src="sfw ? `/img/${cover.sfw.thumbnail}` : `/media/${cover.thumbnail}`"
:data-loading="sfw ? `/img/${cover.sfw.lazy}` : `/media/${cover.lazy}`"
:alt="release.title"
class="thumbnail cover"
>
</span>
<div
v-else
:title="release.title"
class="thumbnail"
>No thumbnail available</div>
</a>
</span>
<div
v-else
:title="release.title"
class="thumbnail"
>No thumbnail available</div>
</a>
</span>
<div class="info">
<a
:href="`/${release.type || 'scene'}/${release.id}/${release.slug}`"
target="_blank"
rel="noopener noreferrer"
class="row link"
>
<h3
v-tooltip.top="release.title"
:title="release.title"
class="title"
>
<Icon
v-if="release.type === 'movie'"
icon="film"
/>{{ release.title }}
</h3>
</a>
<div class="info">
<a
:href="`/${release.type || 'scene'}/${release.id}/${release.slug}`"
target="_blank"
rel="noopener noreferrer"
class="row link"
>
<h3
v-tooltip.top="release.title"
:title="release.title"
class="title"
>
<Icon
v-if="release.type === 'movie'"
icon="film"
/>{{ release.title }}
</h3>
</a>
<span class="row">
<ul class="actors nolist">
<li
v-for="actor in release.actors"
:key="actor.id"
class="actor"
>
<a
:href="`/actor/${actor.slug}`"
class="actor-link"
>{{ actor.name }}</a>
</li>
</ul>
</span>
<span class="row">
<ul class="actors nolist">
<li
v-for="actor in release.actors"
:key="actor.id"
class="actor"
>
<a
:href="`/actor/${actor.slug}`"
class="actor-link"
>{{ actor.name }}</a>
</li>
</ul>
</span>
<ul
v-if="release.tags.length > 0"
:title="release.tags.map(tag => tag.name).join(', ')"
class="tags nolist"
>
<li
v-for="tag in release.tags"
:key="`tag-${tag.slug}`"
class="tag"
>
<router-link
:to="`/tag/${tag.slug}`"
class="tag-link"
>{{ tag.name }}</router-link>
</li>
</ul>
</div>
</div>
<ul
v-if="release.tags.length > 0"
:title="release.tags.map(tag => tag.name).join(', ')"
class="tags nolist"
>
<li
v-for="tag in release.tags"
:key="`tag-${tag.slug}`"
class="tag"
>
<router-link
:to="`/tag/${tag.slug}`"
class="tag-link"
>{{ tag.name }}</router-link>
</li>
</ul>
</div>
</div>
</template>
<script>
function sfw() {
return this.$store.state.ui.sfw;
return this.$store.state.ui.sfw;
}
export default {
props: {
release: {
type: Object,
default: null,
},
referer: {
type: String,
default: null,
},
},
computed: {
sfw,
},
props: {
release: {
type: Object,
default: null,
},
referer: {
type: String,
default: null,
},
},
computed: {
sfw,
},
};
</script>

View File

@ -1,68 +1,68 @@
<template>
<router-link
:to="{ name: 'tag', params: { tagSlug: tag.slug } }"
:title="tag.name"
class="tile"
>
<span class="title">{{ tag.name }}</span>
<router-link
:to="{ name: 'tag', params: { tagSlug: tag.slug } }"
:title="tag.name"
class="tile"
>
<span class="title">{{ tag.name }}</span>
<template v-if="tag.poster">
<img
v-if="!lazy && !sfw"
:src="`/img/${tag.poster.thumbnail}`"
:title="tag.poster.comment"
:alt="tag.name"
class="poster"
>
<template v-if="tag.poster">
<img
v-if="!lazy && !sfw"
:src="`/img/${tag.poster.thumbnail}`"
:title="tag.poster.comment"
:alt="tag.name"
class="poster"
>
<img
v-if="!lazy && sfw"
:src="`/img/${tag.poster.sfw.thumbnail}`"
:title="tag.poster.sfw.comment"
:alt="tag.name"
class="poster"
>
<img
v-if="!lazy && sfw"
:src="`/img/${tag.poster.sfw.thumbnail}`"
:title="tag.poster.sfw.comment"
:alt="tag.name"
class="poster"
>
<img
v-if="lazy && !sfw"
:data-src="`/img/${tag.poster.thumbnail}`"
:data-loading="`/img/${tag.poster.lazy}`"
:title="tag.poster.comment"
:alt="tag.name"
class="poster"
>
<img
v-if="lazy && !sfw"
:data-src="`/img/${tag.poster.thumbnail}`"
:data-loading="`/img/${tag.poster.lazy}`"
:title="tag.poster.comment"
:alt="tag.name"
class="poster"
>
<img
v-if="lazy && sfw"
:data-src="`/img/${tag.poster.sfw.thumbnail}`"
:data-loading="`/img/${tag.poster.sfw.lazy}`"
:title="tag.poster.sfw.comment"
:alt="tag.name"
class="poster"
>
</template>
</router-link>
<img
v-if="lazy && sfw"
:data-src="`/img/${tag.poster.sfw.thumbnail}`"
:data-loading="`/img/${tag.poster.sfw.lazy}`"
:title="tag.poster.sfw.comment"
:alt="tag.name"
class="poster"
>
</template>
</router-link>
</template>
<script>
function sfw() {
return this.$store.state.ui.sfw;
return this.$store.state.ui.sfw;
}
export default {
props: {
tag: {
type: Object,
default: null,
},
lazy: {
type: Boolean,
default: false,
},
},
computed: {
sfw,
},
props: {
tag: {
type: Object,
default: null,
},
lazy: {
type: Boolean,
default: false,
},
},
computed: {
sfw,
},
};
</script>

View File

@ -0,0 +1,6 @@
<svg width="1.5975in" height="1.5153in" version="1.1" viewBox="0 0 40.577 38.489" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-84.419 -88.927)">
<circle cx="103.05" cy="110.94" r="3.4834" stroke-width="8.851"/>
<path d="m95.441 88.928c-1.3031 1.1603-2.5644 2.3924-3.75 3.7148l3.7227 3.3359c1.0397-1.1598 2.1655-2.2623 3.3516-3.3184l-3.3242-3.7324zm-7.041 7.9551c-0.99554 1.5152-1.874 3.1441-2.5547 4.8848l4.6562 1.8203c0.53361-1.3647 1.2441-2.6896 2.0781-3.959l-4.1797-2.7461zm-3.9004 10.396c-0.18456 1.9266-0.060203 3.8935 0.41211 5.8027l4.8535-1.1992c-0.32852-1.328-0.42035-2.7361-0.28711-4.127l-4.9785-0.47656zm35.586 0.21289c-0.29417 1.5186-0.69253 2.9964-1.2227 4.4043l4.6797 1.7617c0.64472-1.7122 1.1135-3.4638 1.4531-5.2168l-4.9102-0.94922zm-28.658 8.1797-4.1855 2.7363c1.061 1.6226 2.3708 3.052 3.8398 4.2617l3.1777-3.8594c-1.1031-0.90839-2.0682-1.9706-2.832-3.1387zm25.445 0.19531c-0.79748 1.2167-1.7417 2.3212-2.8106 3.2461l3.2715 3.7793c1.4528-1.2571 2.6916-2.7131 3.7207-4.2832l-4.1816-2.7422zm-18.939 5.2109-2.0449 4.5625c1.7182 0.77028 3.5357 1.3174 5.3965 1.6211l0.80468-4.9356c-1.4288-0.23319-2.833-0.65486-4.1562-1.248zm12.557 0.28907c-1.2891 0.5601-2.6795 0.92116-4.0957 1.0781l0.55078 4.9707c1.8962-0.21017 3.7684-0.6964 5.5371-1.4648l-1.9922-4.584z" color="#000000" color-rendering="auto" dominant-baseline="auto" image-rendering="auto" shape-rendering="auto" solid-color="#000000" stop-color="#000000" style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-east-asian:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;font-variation-settings:normal;inline-size:0;isolation:auto;mix-blend-mode:normal;shape-margin:0;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>forward</title>
<path d="M4.096 0c-1.777 3.219-2.076 8.13 4.904 7.966v-3.966l6 6-6 6v-3.881c-8.359 0.218-9.29-7.378-4.904-12.119z"></path>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>redo2</title>
<path d="M9 3.881v-3.881l6 6-6 6v-3.966c-6.98-0.164-6.681 4.747-4.904 7.966-4.386-4.741-3.455-12.337 4.904-12.119z"></path>
</svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@ -0,0 +1,6 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16">
<title>reply-all</title>
<path d="M14.184 0c1.777 3.219 2.297 8.13-4.684 7.966v-3.466l-5.5 5.5 5.5 5.5v-3.381c7 0 9.070-7.378 4.684-12.119z"></path>
<path d="M6.5 5.5l-1-1-5.5 5.5 5 5 1-1-4-4z"></path>
</svg>

After

Width:  |  Height:  |  Size: 340 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>reply</title>
<path d="M7 12.119v3.881l-6-6 6-6v3.966c6.98 0.164 6.681-4.747 4.904-7.966 4.386 4.741 3.455 12.337-4.904 12.119z"></path>
</svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>undo2</title>
<path d="M11.904 16c1.777-3.219 2.076-8.13-4.904-7.966v3.966l-6-6 6-6v3.881c8.359-0.218 9.29 7.378 4.904 12.119z"></path>
</svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@ -0,0 +1,31 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="18" height="16" viewBox="0 0 18 16">
<title>users2</title>
<path d="M12 12.041v-0.825c1.101-0.621 2-2.168 2-3.716 0-2.485 0-4.5-3-4.5s-3 2.015-3 4.5c0 1.548 0.898 3.095 2 3.716v0.825c-3.392 0.277-6 1.944-6 3.959h14c0-2.015-2.608-3.682-6-3.959z"></path>
<path d="M3.242 9.625c-0.212 0.077-0.416 0.161-0.611 0.25h4.782c-0.038-0.083-0.075-0.166-0.109-0.25h-4.062z"></path>
<path d="M0.425 11.625c-0.053 0.082-0.101 0.165-0.144 0.25h5.834c0.192-0.089 0.389-0.172 0.593-0.25h-6.284z"></path>
<path d="M1.367 10.625c-0.105 0.081-0.204 0.164-0.299 0.25h6.933c-0.060-0.081-0.118-0.165-0.174-0.25h-6.461z"></path>
<path d="M5.261 9.125c-0.424 0.062-0.833 0.146-1.222 0.25h3.17c-0.030-0.083-0.057-0.166-0.083-0.25h-1.864z"></path>
<path d="M6 8.625v0.25h1.054c-0.022-0.083-0.041-0.166-0.059-0.25h-0.995z"></path>
<path d="M5.851 8.125c0.049 0.032 0.099 0.063 0.149 0.091v0.159h0.947c-0.014-0.083-0.025-0.167-0.035-0.25h-1.062z"></path>
<path d="M0.030 12.625c-0.013 0.083-0.022 0.166-0.027 0.25h4.501c0.102-0.085 0.209-0.169 0.321-0.25h-4.795z"></path>
<path d="M1.719 10.375h5.955c-0.024-0.042-0.048-0.084-0.072-0.127-0.022-0.041-0.044-0.082-0.066-0.123h-5.402c-0.145 0.080-0.283 0.163-0.415 0.25z"></path>
<path d="M4.005 3.625c-0.002 0.083-0.003 0.166-0.003 0.25h3.339c0.004-0.009 0.008-0.018 0.012-0.028 0.035-0.076 0.073-0.15 0.112-0.222h-3.46z"></path>
<path d="M0.168 12.125c-0.032 0.082-0.060 0.166-0.083 0.25h5.109c0.136-0.087 0.277-0.17 0.423-0.25h-5.449z"></path>
<path d="M4.643 0.875h4.713c-0.070-0.089-0.148-0.173-0.234-0.25h-4.245c-0.086 0.077-0.164 0.161-0.234 0.25z"></path>
<path d="M0.602 11.375h6.845c0.256-0.076 0.519-0.145 0.787-0.204-0.013-0.015-0.025-0.030-0.038-0.046h-7.381c-0.076 0.082-0.147 0.165-0.213 0.25z"></path>
<path d="M5.255 7.625c0.085 0.089 0.173 0.173 0.263 0.25h1.371c-0.006-0.083-0.010-0.167-0.012-0.25h-1.622z"></path>
<path d="M4.848 7.125c0.060 0.086 0.123 0.17 0.188 0.25h1.84c0-0.084 0-0.167 0-0.25h-2.027z"></path>
<path d="M4.255 1.625c-0.027 0.081-0.050 0.164-0.072 0.25h5.632c-0.021-0.086-0.045-0.169-0.072-0.25h-5.489z"></path>
<path d="M4.13 2.125c-0.015 0.082-0.029 0.165-0.041 0.25h4.625c0.179-0.098 0.372-0.181 0.578-0.25h-5.162z"></path>
<path d="M4.022 3.125c-0.004 0.082-0.008 0.166-0.010 0.25h3.605c0.058-0.087 0.12-0.17 0.185-0.25h-3.78z"></path>
<path d="M4.059 2.625c-0.008 0.082-0.016 0.165-0.022 0.25h3.995c0.092-0.089 0.189-0.173 0.292-0.25h-4.265z"></path>
<path d="M5.866 0.125c-0.243 0.062-0.455 0.146-0.638 0.25h3.544c-0.184-0.104-0.396-0.188-0.639-0.25h-2.266z"></path>
<path d="M4.544 6.625c0.045 0.085 0.092 0.169 0.141 0.25h2.192c0.001-0.084 0.002-0.167 0.003-0.25h-2.336z"></path>
<path d="M4.475 1.125c-0.046 0.080-0.088 0.163-0.125 0.25h5.3c-0.037-0.087-0.079-0.17-0.125-0.25h-5.050z"></path>
<path d="M4.152 5.625c0.022 0.084 0.048 0.167 0.074 0.25h2.683c0.005-0.084 0.012-0.167 0.019-0.25h-2.777z"></path>
<path d="M4.316 6.125c0.033 0.085 0.068 0.168 0.105 0.25h2.464c0.003-0.084 0.006-0.167 0.009-0.25h-2.579z"></path>
<path d="M4 4.125c-0 0.083-0 0.166-0 0.25h3.155c0.025-0.085 0.053-0.168 0.084-0.25h-3.238z"></path>
<path d="M4.002 4.625c0.003 0.083 0.008 0.167 0.016 0.25h3.014c0.016-0.084 0.035-0.168 0.055-0.25h-3.085z"></path>
<path d="M4.048 5.125c0.013 0.084 0.027 0.167 0.045 0.25h2.862c0.010-0.084 0.021-0.167 0.034-0.25h-2.941z"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,6 @@
<!-- 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>users3</title>
<path d="M15.477 16h-10.954c-0.14 0-0.274-0.059-0.369-0.163s-0.141-0.242-0.129-0.382c0.11-1.22 0.585-2.363 1.373-3.305 0.697-0.832 1.59-1.452 2.602-1.809l0-0.475c-0.562-0.385-1.037-0.926-1.385-1.582-0.402-0.758-0.615-1.634-0.615-2.535 0-1.251 0.405-2.431 1.139-3.323 0.758-0.92 1.774-1.427 2.861-1.427s2.103 0.507 2.861 1.427c0.735 0.892 1.139 2.072 1.139 3.323 0 0.901-0.213 1.777-0.615 2.535-0.348 0.655-0.823 1.197-1.385 1.582l0 0.475c1.012 0.357 1.905 0.977 2.602 1.809 0.788 0.942 1.263 2.084 1.373 3.305 0.013 0.14-0.034 0.279-0.129 0.382s-0.229 0.163-0.369 0.163zM5.1 15h9.8c-0.164-0.81-0.527-1.565-1.064-2.208-0.649-0.776-1.504-1.33-2.471-1.604-0.215-0.061-0.364-0.257-0.364-0.481l-0-1.116c0-0.179 0.095-0.344 0.25-0.433 1.063-0.613 1.75-1.951 1.75-3.408 0-2.068-1.346-3.75-3-3.75s-3 1.682-3 3.75c0 1.457 0.687 2.795 1.75 3.408 0.155 0.089 0.25 0.254 0.25 0.433l-0 1.116c0 0.224-0.149 0.42-0.364 0.481-0.967 0.274-1.822 0.828-2.471 1.604-0.538 0.642-0.9 1.397-1.064 2.208z"></path>
<path d="M4.535 11.428c0.517-0.617 1.131-1.14 1.814-1.548-0.277-0.322-0.522-0.68-0.727-1.068-0.488-0.919-0.746-1.978-0.746-3.063 0-1.511 0.496-2.945 1.396-4.038 0.429-0.521 0.926-0.939 1.469-1.244-0.535-0.306-1.126-0.468-1.74-0.468-1.087 0-2.103 0.507-2.861 1.427-0.735 0.892-1.139 2.072-1.139 3.323 0 0.901 0.213 1.778 0.615 2.535 0.348 0.655 0.823 1.197 1.385 1.582l-0 0.475c-1.012 0.357-1.905 0.977-2.602 1.809-0.788 0.942-1.263 2.084-1.373 3.305-0.013 0.14 0.034 0.279 0.129 0.382s0.229 0.163 0.369 0.163h2.423c0.185-1.316 0.73-2.545 1.59-3.572z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -4,53 +4,9 @@ import {
releaseActorsFragment,
releaseTagsFragment,
} from '../fragments';
import { curateRelease } from '../curate';
import { curateActor, curateRelease } from '../curate';
import getDateRange from '../get-date-range';
function curateActor(actor) {
if (!actor) {
return null;
}
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,
},
scrapedAt: new Date(actor.createdAt),
updatedAt: new Date(actor.updatedAt),
};
if (actor.profiles && actor.profiles.length > 0) {
const photos = actor.profiles
.map(profile => profile.avatar)
.filter(avatar => avatar && (!curatedActor.avatar || avatar.hash !== curatedActor.avatar.hash));
curatedActor.photos = Object.values(photos.reduce((acc, photo) => ({ ...acc, [photo.hash]: photo }), {}));
}
if (actor.releases) {
curatedActor.releases = actor.releases.map(release => curateRelease(release.release));
}
return curatedActor;
}
function initActorActions(store, _router) {
async function fetchActorBySlug({ _commit }, { actorSlug, limit = 100, range = 'latest' }) {
const { before, after, orderBy } = getDateRange(range);
@ -110,6 +66,12 @@ function initActorActions(store, _router) {
hash
comment
copyright
sfw: sfwMedia {
id
thumbnail
path
comment
}
}
profiles: actorsProfiles {
description
@ -121,6 +83,12 @@ function initActorActions(store, _router) {
hash
comment
copyright
sfw: sfwMedia {
id
thumbnail
path
comment
}
}
}
birthCity
@ -202,7 +170,7 @@ function initActorActions(store, _router) {
exclude: store.state.ui.filter,
});
return curateActor(actor);
return curateActor(actor, null, curateRelease);
}
async function fetchActors({ _commit }, {
@ -223,13 +191,16 @@ function initActorActions(store, _router) {
first:$limit,
orderBy: NAME_ASC,
filter: {
aliasFor: {
isNull: true
}
name: {
startsWith: $letter
},
}
gender: {
${genderFilter}
},
},
}
}
) {
id
name
@ -249,17 +220,13 @@ function initActorActions(store, _router) {
lazy
comment
copyright
sfw: sfwMedia {
id
thumbnail
path
comment
}
}
actorsProfiles {
actorsAvatarByProfileId {
media {
id
path
thumbnail
copyright
}
}
}
birthCountry: countryByBirthCountryAlpha2 {
alpha2
name

View File

@ -1,17 +1,50 @@
import dayjs from 'dayjs';
function curateActor(actor, release) {
function curateActor(actor, release, curateActorRelease) {
if (!actor) {
return null;
}
const curatedActor = {
...actor,
origin: actor.originCountry && {
country: actor.originCountry,
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,
},
scrapedAt: new Date(actor.createdAt),
updatedAt: new Date(actor.updatedAt),
};
if (actor.profiles && actor.profiles.length > 0) {
const photos = actor.profiles
.map(profile => profile.avatar)
.filter(avatar => avatar && (!curatedActor.avatar || avatar.hash !== curatedActor.avatar.hash));
curatedActor.photos = Object.values(photos.reduce((acc, photo) => ({ ...acc, [photo.hash]: photo }), {}));
}
if (release && release.date && curatedActor.birthdate) {
curatedActor.ageThen = dayjs(release.date).diff(actor.birthdate, 'year');
}
if (actor.releases) {
curatedActor.releases = actor.releases.map(actorRelease => curateActorRelease(actorRelease.release));
}
return curatedActor;
}

View File

@ -28,75 +28,6 @@ function initReleasesActions(store, _router) {
return releases.map(release => curateRelease(release));
}
async function searchReleases({ _commit }, { query, limit = 20 }) {
const res = await graphql(`
query SearchReleases(
$query: String!
$limit: Int = 20
) {
releases: searchReleases(
query: $query
first: $limit
) {
id
title
slug
date
url
type
isNew
site {
id
slug
name
url
network {
id
slug
name
url
}
}
actors: releasesActors {
actor {
id
slug
name
}
}
tags: releasesTags(orderBy: TAG_BY_TAG_ID__PRIORITY_DESC) {
tag {
id
name
slug
}
}
poster: releasesPosterByReleaseId {
media {
id
thumbnail
lazy
}
}
covers: releasesCovers {
media {
id
thumbnail
lazy
}
}
}
}
`, {
query,
limit,
});
if (!res) return [];
return res.releases.map(release => curateRelease(release));
}
async function fetchReleaseById({ _commit }, releaseId) {
// const release = await get(`/releases/${releaseId}`);
@ -114,7 +45,6 @@ function initReleasesActions(store, _router) {
return {
fetchReleases,
fetchReleaseById,
searchReleases,
};
}

View File

@ -1,3 +1,6 @@
import { graphql } from '../api';
import { curateRelease, curateActor } from '../curate';
function initUiActions(_store, _router) {
function setFilter({ commit }, filter) {
commit('setFilter', filter);
@ -23,7 +26,133 @@ function initUiActions(_store, _router) {
localStorage.setItem('sfw', sfw);
}
async function search({ _commit }, { query, limit = 20 }) {
const res = await graphql(`
query SearchReleases(
$query: String!
$limit: Int = 20
) {
releases: searchReleases(
query: $query
first: $limit
) {
id
title
slug
date
url
type
isNew
site {
id
slug
name
url
network {
id
slug
name
url
}
}
actors: releasesActors {
actor {
id
slug
name
}
}
tags: releasesTags(orderBy: TAG_BY_TAG_ID__PRIORITY_DESC) {
tag {
id
name
slug
}
}
poster: releasesPosterByReleaseId {
media {
id
thumbnail
lazy
}
}
covers: releasesCovers {
media {
id
thumbnail
lazy
}
}
}
actors: searchActors(
search: $query,
first: $limit
) {
id
name
slug
age
dateOfBirth
gender
aliasFor: actorByAliasFor {
id
name
slug
age
dateOfBirth
gender
network {
id
name
slug
}
avatar: avatarMedia {
id
path
thumbnail
lazy
comment
copyright
}
birthCountry: countryByBirthCountryAlpha2 {
alpha2
name
alias
}
}
network {
id
name
slug
}
avatar: avatarMedia {
id
path
thumbnail
lazy
comment
copyright
}
birthCountry: countryByBirthCountryAlpha2 {
alpha2
name
alias
}
}
}
`, {
query,
limit,
});
return {
releases: res.releases.map(release => curateRelease(release)),
actors: res.actors.map(actor => curateActor(actor)),
};
}
return {
search,
setFilter,
setRange,
setBatch,

View File

@ -270,8 +270,6 @@ exports.up = knex => Promise.resolve()
.references('id')
.inTable('networks');
table.unique(['slug', 'network_id']);
table.integer('alias_for', 12)
.references('id')
.inTable('actors');
@ -794,6 +792,9 @@ exports.up = knex => Promise.resolve()
ALTER TABLE releases_search
ADD COLUMN document tsvector;
CREATE UNIQUE INDEX unique_actor_slugs_network ON actors (slug, network_id);
CREATE UNIQUE INDEX unique_actor_slugs ON actors (slug, (network_id IS NULL));
CREATE TEXT SEARCH DICTIONARY traxxx_dict (
TEMPLATE = pg_catalog.simple,
stopwords = traxxx
@ -825,6 +826,12 @@ exports.up = knex => Promise.resolve()
url ILIKE ('%' || search || '%')
$$ LANGUAGE SQL STABLE;
CREATE FUNCTION search_actors(search text, min_length numeric DEFAULT 2) RETURNS SETOF actors AS $$
SELECT * FROM actors
WHERE length(search) >= min_length
AND name ILIKE ('%' || search || '%')
$$ LANGUAGE SQL STABLE;
CREATE FUNCTION releases_is_new(release releases) RETURNS boolean AS $$
SELECT NOT EXISTS(SELECT true FROM batches WHERE batches.id = release.created_batch_id + 1 LIMIT 1);
$$ LANGUAGE sql STABLE;
@ -894,8 +901,8 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style
DROP TABLE IF EXISTS countries CASCADE;
DROP TABLE IF EXISTS networks CASCADE;
DROP FUNCTION IF EXISTS releases_by_tag_slugs;
DROP FUNCTION IF EXISTS search_sites;
DROP FUNCTION IF EXISTS search_actors;
DROP FUNCTION IF EXISTS get_random_sfw_media_id;
DROP TEXT SEARCH CONFIGURATION IF EXISTS traxxx;

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -357,7 +357,6 @@ const sfw = Object.entries({
['NI_fJ15rIfI', 'Szabo Viktor'],
['LymVMRIUwPQ', 'Happy Films'],
['mrNVnLEphdo', 'Greg Nunes'],
['FKvoEKSV2LY', 'zhou yu'],
['CKLF34baCTQ', 'Willian Justen de Vasconcellos'],
['7uGCN9qshsY', 'Siora Photography'],
['xBTnaTgleQE', 'Glen Carrie'],

View File

@ -127,6 +127,10 @@ function curateProfileEntry(profile) {
}
async function curateProfile(profile) {
if (!profile) {
return null;
}
try {
const curatedProfile = {
id: profile.id,
@ -161,7 +165,7 @@ async function curateProfile(profile) {
curatedProfile.dateOfDeath = Number.isNaN(Number(profile.dateOfDeath)) ? null : profile.dateOfDeath;
curatedProfile.cup = profile.cup || profile.bust?.match(/[a-zA-Z]+/)?.[0] || null;
curatedProfile.cup = profile.cup || (typeof profile.bust === 'string' && profile.bust?.match(/[a-zA-Z]+/)?.[0]) || null;
curatedProfile.bust = Number(profile.bust) || profile.bust?.match(/\d+/)?.[0] || null;
curatedProfile.waist = Number(profile.waist) || profile.waist?.match(/\d+/)?.[0] || null;
curatedProfile.hip = Number(profile.hip) || profile.hip?.match(/\d+/)?.[0] || null;
@ -257,6 +261,7 @@ async function interpolateProfiles(actors) {
profile.date_of_birth = getMostFrequentDate(valuesByProperty.date_of_birth);
profile.date_of_death = getMostFrequentDate(valuesByProperty.date_of_death);
// TODO: fix city, state and country not matching
profile.birth_city = getMostFrequent(valuesByProperty.birth_city);
profile.birth_state = getMostFrequent(valuesByProperty.birth_state);
profile.birth_country_alpha2 = getMostFrequent(valuesByProperty.birth_country_alpha2);
@ -300,51 +305,6 @@ async function interpolateProfiles(actors) {
.catch(transaction.rollback);
}
async function scrapeProfiles(actor, sources, networksBySlug, sitesBySlug) {
const profiles = Promise.map(sources, async (source) => {
try {
return await [].concat(source).reduce(async (outcome, scraperSlug) => outcome.catch(async () => {
const scraper = scrapers[scraperSlug];
const siteOrNetwork = networksBySlug[scraperSlug] || sitesBySlug[scraperSlug];
if (!scraper?.fetchProfile) {
logger.warn(`No profile profile scraper available for ${scraperSlug}`);
throw new Error(`No profile profile scraper available for ${scraperSlug}`);
}
if (!siteOrNetwork) {
logger.warn(`No site or network found for ${scraperSlug}`);
throw new Error(`No site or network found for ${scraperSlug}`);
}
logger.verbose(`Searching profile for '${actor.name}' on '${scraperSlug}'`);
const profile = await scraper.fetchProfile(actor.name, scraperSlug, siteOrNetwork, include);
if (!profile || typeof profile === 'number') { // scraper returns HTTP code on request failure
logger.verbose(`Profile for '${actor.name}' not available on ${scraperSlug}, scraper returned ${profile}`);
throw Object.assign(new Error(`Profile for '${actor.name}' not available on ${scraperSlug}`), { code: 'PROFILE_NOT_AVAILABLE' });
}
return {
...actor,
...profile,
scraper: scraperSlug,
site: siteOrNetwork,
};
}), Promise.reject(new Error()));
} catch (error) {
if (error.code !== 'PROFILE_NOT_AVAILABLE') {
logger.error(`Failed to fetch profile for '${actor.name}': ${error.message}`);
}
}
return null;
});
return profiles.filter(Boolean);
}
async function upsertProfiles(profiles) {
const curatedProfileEntries = profiles.map(profile => curateProfileEntry(profile));
@ -403,6 +363,51 @@ async function upsertProfiles(profiles) {
}
}
async function scrapeProfiles(actor, sources, networksBySlug, sitesBySlug) {
const profiles = Promise.map(sources, async (source) => {
try {
return await [].concat(source).reduce(async (outcome, scraperSlug) => outcome.catch(async () => {
const scraper = scrapers[scraperSlug];
const siteOrNetwork = networksBySlug[scraperSlug] || sitesBySlug[scraperSlug];
if (!scraper?.fetchProfile) {
logger.warn(`No profile profile scraper available for ${scraperSlug}`);
throw new Error(`No profile profile scraper available for ${scraperSlug}`);
}
if (!siteOrNetwork) {
logger.warn(`No site or network found for ${scraperSlug}`);
throw new Error(`No site or network found for ${scraperSlug}`);
}
logger.verbose(`Searching profile for '${actor.name}' on '${scraperSlug}'`);
const profile = await scraper.fetchProfile(actor.name, scraperSlug, siteOrNetwork, include);
if (!profile || typeof profile === 'number') { // scraper returns HTTP code on request failure
logger.verbose(`Profile for '${actor.name}' not available on ${scraperSlug}, scraper returned ${profile}`);
throw Object.assign(new Error(`Profile for '${actor.name}' not available on ${scraperSlug}`), { code: 'PROFILE_NOT_AVAILABLE' });
}
return {
...actor,
...profile,
scraper: scraperSlug,
site: siteOrNetwork,
};
}), Promise.reject(new Error()));
} catch (error) {
if (error.code !== 'PROFILE_NOT_AVAILABLE') {
logger.error(`Failed to fetch profile for '${actor.name}': ${error.message}`);
}
}
return null;
});
return profiles.filter(Boolean);
}
async function scrapeActors(actorNames) {
const baseActors = toBaseActors(actorNames);
@ -438,7 +443,8 @@ async function scrapeActors(actorNames) {
{ concurrency: 10 },
);
const profiles = await Promise.all(profilesPerActor.flat().map(profile => curateProfile(profile)));
const curatedProfiles = await Promise.all(profilesPerActor.flat().map(profile => curateProfile(profile)));
const profiles = curatedProfiles.filter(Boolean);
if (argv.inspect) {
console.log(profiles);
@ -495,31 +501,25 @@ async function associateActors(releases, batchId) {
return null;
}
const baseActorsBySlugAndNetworkId = baseActors.reduce((acc, baseActor) => ({
const baseActorsBySlug = baseActors.reduce((acc, baseActor) => ({
...acc,
[baseActor.slug]: {
...acc[baseActor.slug],
[baseActor.network.id]: baseActor,
},
[baseActor.slug]: baseActor,
}), {});
const uniqueBaseActors = Object.values(baseActorsBySlugAndNetworkId).map(baseActorsByNetworkId => Object.values(baseActorsByNetworkId)).flat();
const uniqueBaseActors = Object.values(baseActorsBySlug);
const actors = await getOrCreateActors(uniqueBaseActors, batchId);
const actorIdsBySlugAndNetworkId = actors.reduce((acc, actor) => ({
const actorIdsBySlug = actors.reduce((acc, actor) => ({
...acc,
[actor.network_id]: {
...acc[actor.network_id],
[actor.slug]: actor.alias_for || actor.id,
},
[actor.slug]: actor.alias_for || actor.id,
}), {});
const releaseActorAssociations = Object.entries(baseActorsByReleaseId)
.map(([releaseId, releaseActors]) => releaseActors
.map(releaseActor => ({
release_id: releaseId,
actor_id: actorIdsBySlugAndNetworkId[releaseActor.network.id]?.[releaseActor.slug] || actorIdsBySlugAndNetworkId.null[releaseActor.slug],
actor_id: actorIdsBySlug[releaseActor.slug],
})))
.flat();

View File

@ -252,9 +252,13 @@ async function fetchProfile(actorName) {
}, { encodeJSON: true });
if (res.ok) {
const actor = res.body.hits.hits.find(hit => hit._source.name === actorName);
const actor = res.body.hits.hits.find(hit => hit._source.name.toLowerCase() === actorName.toLowerCase());
return scrapeProfile(actor._source);
if (actor) {
return scrapeProfile(actor._source);
}
return null;
}
return res.status;