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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,47 +1,89 @@
<template> <template>
<div class="content-inner"> <div class="content-inner">
<span <span
v-if="!loading" v-if="loading"
class="summary" class="summary"
>Found {{ releases.length }} results for '{{ query }}'</span> >Searching...</span>
<span <div v-if="!loading && actors.length > 0">
v-else <span
class="summary" v-if="!loading"
>Searching...</span> class="summary"
>Found {{ actors.length }} actors for '{{ query }}'</span>
<Releases :releases="releases" /> <div class="tiles">
</div> <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> </template>
<script> <script>
import Actor from '../tile/actor.vue';
import Releases from '../releases/releases.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() { async function mounted() {
const results = await this.$store.dispatch('searchReleases', { await this.search();
query: this.query, }
limit: 100,
});
this.loading = false; async function watchQuery() {
await this.search();
if (results) {
this.releases = results;
}
} }
export default { export default {
components: { components: {
Releases, Actor,
}, Releases,
data() { },
return { data() {
loading: true, return {
releases: [], loading: true,
query: this.$route.query.query || this.$route.query.q, actors: [],
}; releases: [],
}, };
mounted, },
computed: {
query,
},
watch: {
query: watchQuery,
},
mounted,
methods: {
search,
},
}; };
</script> </script>
@ -54,4 +96,12 @@ export default {
color: $shadow; color: $shadow;
font-weight: bold; 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> </style>

View File

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

View File

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

View File

@ -1,68 +1,68 @@
<template> <template>
<router-link <router-link
:to="{ name: 'tag', params: { tagSlug: tag.slug } }" :to="{ name: 'tag', params: { tagSlug: tag.slug } }"
:title="tag.name" :title="tag.name"
class="tile" class="tile"
> >
<span class="title">{{ tag.name }}</span> <span class="title">{{ tag.name }}</span>
<template v-if="tag.poster"> <template v-if="tag.poster">
<img <img
v-if="!lazy && !sfw" v-if="!lazy && !sfw"
:src="`/img/${tag.poster.thumbnail}`" :src="`/img/${tag.poster.thumbnail}`"
:title="tag.poster.comment" :title="tag.poster.comment"
:alt="tag.name" :alt="tag.name"
class="poster" class="poster"
> >
<img <img
v-if="!lazy && sfw" v-if="!lazy && sfw"
:src="`/img/${tag.poster.sfw.thumbnail}`" :src="`/img/${tag.poster.sfw.thumbnail}`"
:title="tag.poster.sfw.comment" :title="tag.poster.sfw.comment"
:alt="tag.name" :alt="tag.name"
class="poster" class="poster"
> >
<img <img
v-if="lazy && !sfw" v-if="lazy && !sfw"
:data-src="`/img/${tag.poster.thumbnail}`" :data-src="`/img/${tag.poster.thumbnail}`"
:data-loading="`/img/${tag.poster.lazy}`" :data-loading="`/img/${tag.poster.lazy}`"
:title="tag.poster.comment" :title="tag.poster.comment"
:alt="tag.name" :alt="tag.name"
class="poster" class="poster"
> >
<img <img
v-if="lazy && sfw" v-if="lazy && sfw"
:data-src="`/img/${tag.poster.sfw.thumbnail}`" :data-src="`/img/${tag.poster.sfw.thumbnail}`"
:data-loading="`/img/${tag.poster.sfw.lazy}`" :data-loading="`/img/${tag.poster.sfw.lazy}`"
:title="tag.poster.sfw.comment" :title="tag.poster.sfw.comment"
:alt="tag.name" :alt="tag.name"
class="poster" class="poster"
> >
</template> </template>
</router-link> </router-link>
</template> </template>
<script> <script>
function sfw() { function sfw() {
return this.$store.state.ui.sfw; return this.$store.state.ui.sfw;
} }
export default { export default {
props: { props: {
tag: { tag: {
type: Object, type: Object,
default: null, default: null,
}, },
lazy: { lazy: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}, },
computed: { computed: {
sfw, sfw,
}, },
}; };
</script> </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, releaseActorsFragment,
releaseTagsFragment, releaseTagsFragment,
} from '../fragments'; } from '../fragments';
import { curateRelease } from '../curate'; import { curateActor, curateRelease } from '../curate';
import getDateRange from '../get-date-range'; 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) { function initActorActions(store, _router) {
async function fetchActorBySlug({ _commit }, { actorSlug, limit = 100, range = 'latest' }) { async function fetchActorBySlug({ _commit }, { actorSlug, limit = 100, range = 'latest' }) {
const { before, after, orderBy } = getDateRange(range); const { before, after, orderBy } = getDateRange(range);
@ -110,6 +66,12 @@ function initActorActions(store, _router) {
hash hash
comment comment
copyright copyright
sfw: sfwMedia {
id
thumbnail
path
comment
}
} }
profiles: actorsProfiles { profiles: actorsProfiles {
description description
@ -121,6 +83,12 @@ function initActorActions(store, _router) {
hash hash
comment comment
copyright copyright
sfw: sfwMedia {
id
thumbnail
path
comment
}
} }
} }
birthCity birthCity
@ -202,7 +170,7 @@ function initActorActions(store, _router) {
exclude: store.state.ui.filter, exclude: store.state.ui.filter,
}); });
return curateActor(actor); return curateActor(actor, null, curateRelease);
} }
async function fetchActors({ _commit }, { async function fetchActors({ _commit }, {
@ -223,13 +191,16 @@ function initActorActions(store, _router) {
first:$limit, first:$limit,
orderBy: NAME_ASC, orderBy: NAME_ASC,
filter: { filter: {
aliasFor: {
isNull: true
}
name: { name: {
startsWith: $letter startsWith: $letter
}, }
gender: { gender: {
${genderFilter} ${genderFilter}
}, }
}, }
) { ) {
id id
name name
@ -249,17 +220,13 @@ function initActorActions(store, _router) {
lazy lazy
comment comment
copyright copyright
sfw: sfwMedia {
id
thumbnail
path
comment
}
} }
actorsProfiles {
actorsAvatarByProfileId {
media {
id
path
thumbnail
copyright
}
}
}
birthCountry: countryByBirthCountryAlpha2 { birthCountry: countryByBirthCountryAlpha2 {
alpha2 alpha2
name name

View File

@ -1,17 +1,50 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
function curateActor(actor, release) { function curateActor(actor, release, curateActorRelease) {
if (!actor) {
return null;
}
const curatedActor = { const curatedActor = {
...actor, ...actor,
origin: actor.originCountry && { height: actor.heightMetric && {
country: actor.originCountry, 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) { if (release && release.date && curatedActor.birthdate) {
curatedActor.ageThen = dayjs(release.date).diff(actor.birthdate, 'year'); curatedActor.ageThen = dayjs(release.date).diff(actor.birthdate, 'year');
} }
if (actor.releases) {
curatedActor.releases = actor.releases.map(actorRelease => curateActorRelease(actorRelease.release));
}
return curatedActor; return curatedActor;
} }

View File

@ -28,75 +28,6 @@ function initReleasesActions(store, _router) {
return releases.map(release => curateRelease(release)); 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) { async function fetchReleaseById({ _commit }, releaseId) {
// const release = await get(`/releases/${releaseId}`); // const release = await get(`/releases/${releaseId}`);
@ -114,7 +45,6 @@ function initReleasesActions(store, _router) {
return { return {
fetchReleases, fetchReleases,
fetchReleaseById, fetchReleaseById,
searchReleases,
}; };
} }

View File

@ -1,3 +1,6 @@
import { graphql } from '../api';
import { curateRelease, curateActor } from '../curate';
function initUiActions(_store, _router) { function initUiActions(_store, _router) {
function setFilter({ commit }, filter) { function setFilter({ commit }, filter) {
commit('setFilter', filter); commit('setFilter', filter);
@ -23,7 +26,133 @@ function initUiActions(_store, _router) {
localStorage.setItem('sfw', sfw); 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 { return {
search,
setFilter, setFilter,
setRange, setRange,
setBatch, setBatch,

View File

@ -270,8 +270,6 @@ exports.up = knex => Promise.resolve()
.references('id') .references('id')
.inTable('networks'); .inTable('networks');
table.unique(['slug', 'network_id']);
table.integer('alias_for', 12) table.integer('alias_for', 12)
.references('id') .references('id')
.inTable('actors'); .inTable('actors');
@ -794,6 +792,9 @@ exports.up = knex => Promise.resolve()
ALTER TABLE releases_search ALTER TABLE releases_search
ADD COLUMN document tsvector; 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 ( CREATE TEXT SEARCH DICTIONARY traxxx_dict (
TEMPLATE = pg_catalog.simple, TEMPLATE = pg_catalog.simple,
stopwords = traxxx stopwords = traxxx
@ -825,6 +826,12 @@ exports.up = knex => Promise.resolve()
url ILIKE ('%' || search || '%') url ILIKE ('%' || search || '%')
$$ LANGUAGE SQL STABLE; $$ 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 $$ 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); SELECT NOT EXISTS(SELECT true FROM batches WHERE batches.id = release.created_batch_id + 1 LIMIT 1);
$$ LANGUAGE sql STABLE; $$ 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 countries CASCADE;
DROP TABLE IF EXISTS networks 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_sites;
DROP FUNCTION IF EXISTS search_actors;
DROP FUNCTION IF EXISTS get_random_sfw_media_id; DROP FUNCTION IF EXISTS get_random_sfw_media_id;
DROP TEXT SEARCH CONFIGURATION IF EXISTS traxxx; 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'], ['NI_fJ15rIfI', 'Szabo Viktor'],
['LymVMRIUwPQ', 'Happy Films'], ['LymVMRIUwPQ', 'Happy Films'],
['mrNVnLEphdo', 'Greg Nunes'], ['mrNVnLEphdo', 'Greg Nunes'],
['FKvoEKSV2LY', 'zhou yu'],
['CKLF34baCTQ', 'Willian Justen de Vasconcellos'], ['CKLF34baCTQ', 'Willian Justen de Vasconcellos'],
['7uGCN9qshsY', 'Siora Photography'], ['7uGCN9qshsY', 'Siora Photography'],
['xBTnaTgleQE', 'Glen Carrie'], ['xBTnaTgleQE', 'Glen Carrie'],

View File

@ -127,6 +127,10 @@ function curateProfileEntry(profile) {
} }
async function curateProfile(profile) { async function curateProfile(profile) {
if (!profile) {
return null;
}
try { try {
const curatedProfile = { const curatedProfile = {
id: profile.id, id: profile.id,
@ -161,7 +165,7 @@ async function curateProfile(profile) {
curatedProfile.dateOfDeath = Number.isNaN(Number(profile.dateOfDeath)) ? null : profile.dateOfDeath; 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.bust = Number(profile.bust) || profile.bust?.match(/\d+/)?.[0] || null;
curatedProfile.waist = Number(profile.waist) || profile.waist?.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; 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_birth = getMostFrequentDate(valuesByProperty.date_of_birth);
profile.date_of_death = getMostFrequentDate(valuesByProperty.date_of_death); 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_city = getMostFrequent(valuesByProperty.birth_city);
profile.birth_state = getMostFrequent(valuesByProperty.birth_state); profile.birth_state = getMostFrequent(valuesByProperty.birth_state);
profile.birth_country_alpha2 = getMostFrequent(valuesByProperty.birth_country_alpha2); profile.birth_country_alpha2 = getMostFrequent(valuesByProperty.birth_country_alpha2);
@ -300,51 +305,6 @@ async function interpolateProfiles(actors) {
.catch(transaction.rollback); .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) { async function upsertProfiles(profiles) {
const curatedProfileEntries = profiles.map(profile => curateProfileEntry(profile)); 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) { async function scrapeActors(actorNames) {
const baseActors = toBaseActors(actorNames); const baseActors = toBaseActors(actorNames);
@ -438,7 +443,8 @@ async function scrapeActors(actorNames) {
{ concurrency: 10 }, { 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) { if (argv.inspect) {
console.log(profiles); console.log(profiles);
@ -495,31 +501,25 @@ async function associateActors(releases, batchId) {
return null; return null;
} }
const baseActorsBySlugAndNetworkId = baseActors.reduce((acc, baseActor) => ({ const baseActorsBySlug = baseActors.reduce((acc, baseActor) => ({
...acc, ...acc,
[baseActor.slug]: { [baseActor.slug]: baseActor,
...acc[baseActor.slug],
[baseActor.network.id]: baseActor,
},
}), {}); }), {});
const uniqueBaseActors = Object.values(baseActorsBySlugAndNetworkId).map(baseActorsByNetworkId => Object.values(baseActorsByNetworkId)).flat(); const uniqueBaseActors = Object.values(baseActorsBySlug);
const actors = await getOrCreateActors(uniqueBaseActors, batchId); const actors = await getOrCreateActors(uniqueBaseActors, batchId);
const actorIdsBySlugAndNetworkId = actors.reduce((acc, actor) => ({ const actorIdsBySlug = actors.reduce((acc, actor) => ({
...acc, ...acc,
[actor.network_id]: { [actor.slug]: actor.alias_for || actor.id,
...acc[actor.network_id],
[actor.slug]: actor.alias_for || actor.id,
},
}), {}); }), {});
const releaseActorAssociations = Object.entries(baseActorsByReleaseId) const releaseActorAssociations = Object.entries(baseActorsByReleaseId)
.map(([releaseId, releaseActors]) => releaseActors .map(([releaseId, releaseActors]) => releaseActors
.map(releaseActor => ({ .map(releaseActor => ({
release_id: releaseId, release_id: releaseId,
actor_id: actorIdsBySlugAndNetworkId[releaseActor.network.id]?.[releaseActor.slug] || actorIdsBySlugAndNetworkId.null[releaseActor.slug], actor_id: actorIdsBySlug[releaseActor.slug],
}))) })))
.flat(); .flat();

View File

@ -252,9 +252,13 @@ async function fetchProfile(actorName) {
}, { encodeJSON: true }); }, { encodeJSON: true });
if (res.ok) { 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; return res.status;