Compare commits
21 Commits
343325440e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f58f989f7 | |||
| 96b1a99e04 | |||
| 1bc7dd3a43 | |||
| a048970be6 | |||
| e3171e5693 | |||
| d463b3df5c | |||
| 1ae7befa4b | |||
| dc80e1e199 | |||
| 35ffc2b0f7 | |||
| 383844dda8 | |||
| 77fb6595a2 | |||
| aa3adbe634 | |||
| 59a700c2f3 | |||
| 18f5a6f476 | |||
| 63a178ca57 | |||
| 0ae949a616 | |||
| edc9720623 | |||
| bbc3fbb0a5 | |||
| 1fc468efac | |||
| 143c415797 | |||
| e79a4d48e1 |
@@ -19,6 +19,9 @@
|
||||
|
||||
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1.0,interactive-widget=resizes-content" />
|
||||
|
||||
<!-- RTA restricted to adults label -->
|
||||
<meta name="RATING" content="RTA-5042-1996-1400-1577-RTA" />
|
||||
|
||||
<title>traxxx - Consent</title>
|
||||
<style>
|
||||
:root {
|
||||
@@ -156,6 +159,12 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.rta {
|
||||
position: fixed;
|
||||
bottom: .5rem;
|
||||
right: .5rem;
|
||||
}
|
||||
|
||||
@media(max-width: 800px) {
|
||||
.heading {
|
||||
font-size: 1.25rem;
|
||||
@@ -219,5 +228,12 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src="/img/rta.gif"
|
||||
alt="RTA Restricted To Adults"
|
||||
title="RTA Restricted To Adults"
|
||||
class="rta"
|
||||
>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
2
common
2
common
Submodule common updated: ec4b15ce33...e4d6ff6ad1
@@ -80,10 +80,17 @@
|
||||
<span
|
||||
v-if="actor.origin.city"
|
||||
class="city"
|
||||
>{{ actor.origin.city }}</span><span
|
||||
>{{ actor.origin.city }}</span>
|
||||
|
||||
<span
|
||||
v-if="actor.origin.state && (!actor.origin.city || (actor.origin.country && actor.origin.country.alpha2 === 'US'))"
|
||||
class="state"
|
||||
>{{ actor.origin.city ? `, ${actor.origin.state}` : actor.origin.state }}</span>
|
||||
>
|
||||
{{ actor.origin.city
|
||||
? [',', actor.origin.state].join(' ')
|
||||
: actor.origin.state
|
||||
}}
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="actor.origin.country"
|
||||
@@ -105,7 +112,7 @@
|
||||
class="bio-item residence"
|
||||
:class="{ hideable: !!actor.origin }"
|
||||
>
|
||||
<dfn class="bio-label"><Icon icon="location" />Lives in</dfn>
|
||||
<dfn class="bio-label"><Icon icon="location" />{{ actor.dateOfDeath ? 'Lived' : 'Lives' }} in</dfn>
|
||||
|
||||
<span>
|
||||
<span
|
||||
@@ -312,10 +319,10 @@
|
||||
<a
|
||||
v-for="social in socials"
|
||||
:key="`social-${social.id}`"
|
||||
v-tooltip="social.platform ? `${social.platform} ${env.socials.prefix[social.platform] || env.socials.prefix.default}${social.handle}` : social.url"
|
||||
:href="getSocialUrl(social)"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
:title="social.platform || social.url"
|
||||
class="social ellipsis"
|
||||
>
|
||||
<Icon
|
||||
@@ -339,7 +346,9 @@
|
||||
:class="`icon-social icon-${social.platform} icon-generic`"
|
||||
/>
|
||||
|
||||
<!--
|
||||
<template v-if="social.platform">{{ env.socials.prefix[social.platform] || env.socials.prefix.default }}</template>{{ social.handle }}
|
||||
-->
|
||||
</a>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -363,10 +372,22 @@
|
||||
target="_blank"
|
||||
class="link"
|
||||
>Revisions</a>
|
||||
|
||||
<span
|
||||
v-if="user && user.role !== 'user'"
|
||||
class="link"
|
||||
@click="showMergeDialog = true"
|
||||
>Merge</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<Merge
|
||||
v-if="showMergeDialog"
|
||||
:actor="actor"
|
||||
@close="showMergeDialog = false"
|
||||
/>
|
||||
|
||||
<div class="descriptions-container">
|
||||
<div
|
||||
v-if="descriptions.length > 0"
|
||||
@@ -427,6 +448,8 @@ import formatTemplate from 'template-format';
|
||||
import getPath from '#/src/get-path.js';
|
||||
import { formatDate } from '#/utils/format.js';
|
||||
|
||||
import Merge from '#/components/actors/merge.vue';
|
||||
|
||||
const expanded = ref(false);
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
@@ -498,12 +521,14 @@ function getSocialUrl(social) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const socials = props.actor.socials.map((social) => ({
|
||||
const socials = props.actor.socials.slice(0, 10).map((social) => ({
|
||||
...social,
|
||||
handle: social.url
|
||||
? new URL(social.url).hostname
|
||||
: social.handle,
|
||||
}));
|
||||
|
||||
const showMergeDialog = ref(false);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -735,19 +760,23 @@ const socials = props.actor.socials.map((social) => ({
|
||||
}
|
||||
|
||||
.socials {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
/*
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-gap: 0 0;
|
||||
overflow: hidden;
|
||||
*/
|
||||
gap: .25rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.social {
|
||||
display: flex;
|
||||
height: 2rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: .1rem .5rem;
|
||||
justify-content: center;
|
||||
padding: .75rem .75rem;
|
||||
border-radius: .25rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
@@ -755,10 +784,6 @@ const socials = props.actor.socials.map((social) => ({
|
||||
font-weight: normal;
|
||||
background: var(--highlight-weak-40);
|
||||
|
||||
.icon {
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.icon-generic {
|
||||
fill: var(--highlight);
|
||||
}
|
||||
@@ -783,6 +808,7 @@ const socials = props.actor.socials.map((social) => ({
|
||||
.link {
|
||||
color: inherit;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
186
components/actors/merge.vue
Normal file
186
components/actors/merge.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<Dialog
|
||||
:title="`Merge '${actor.name}'`"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<div class="dialog-body">
|
||||
<strong class="source">#{{ actor.id }} {{ actor.name }}<span v-if="actor.entity"> ({{ actor.entity.name }})</span></strong>
|
||||
|
||||
<span class="path">merging into</span>
|
||||
|
||||
<div
|
||||
v-if="targetActor"
|
||||
class="target"
|
||||
>
|
||||
<strong class="target-name">
|
||||
<span class="target-id">#{{ targetActor.id }}</span>
|
||||
{{ targetActor.name }}
|
||||
</strong>
|
||||
|
||||
<Icon
|
||||
icon="cross2"
|
||||
@click="targetActor = null"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<VDropdown
|
||||
:triggers="[]"
|
||||
:shown="actorResults.length > 0"
|
||||
:auto-hide="false"
|
||||
>
|
||||
<input
|
||||
ref="actorInput"
|
||||
v-model="actorQuery"
|
||||
class="input"
|
||||
placeholder="Search target actor"
|
||||
@input="searchActors"
|
||||
>
|
||||
|
||||
<template #popper>
|
||||
<ul
|
||||
class="results nolist"
|
||||
>
|
||||
<li
|
||||
v-for="actorResult in actorResults"
|
||||
:key="`actor-result-${actorResult.id}`"
|
||||
v-close-popper
|
||||
class="result-item"
|
||||
@click="selectActor(actorResult)"
|
||||
>
|
||||
<div class="result-label">
|
||||
<span class="result-id">#{{ actorResult.id }}</span> {{ actorResult.name }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</template>
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
type="submit"
|
||||
class="button button-primary"
|
||||
:disabled="!targetActor"
|
||||
@click="merge"
|
||||
>Merge</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import Dialog from '#/components/dialog/dialog.vue';
|
||||
|
||||
import { get, post } from '#/src/api.js';
|
||||
|
||||
const props = defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const targetActor = ref(null);
|
||||
const actorInput = ref(null);
|
||||
const actorQuery = ref('');
|
||||
const actorResults = ref([]);
|
||||
|
||||
async function searchActors() {
|
||||
const res = await get('/actors', {
|
||||
q: `${actorQuery.value}*`, // return partial matches
|
||||
limit: 10,
|
||||
global: true,
|
||||
});
|
||||
|
||||
actorResults.value = res.actors;
|
||||
}
|
||||
|
||||
async function merge() {
|
||||
await post(`/actors/${targetActor.value.id}/merge/${props.actor.id}`, null, {
|
||||
successFeedback: `Merged ${props.actor.entity ? `${props.actor.name} (${props.actor.entity.name})` : props.actor.name} into ${targetActor.value.name}`,
|
||||
errorFeedback: `Failed to merge ${props.actor.entity ? `${props.actor.name} (${props.actor.entity.name})` : props.actor.name} into ${targetActor.value.name}`,
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
emit('close');
|
||||
}
|
||||
|
||||
function selectActor(actor) {
|
||||
targetActor.value = actor;
|
||||
actorQuery.value = '';
|
||||
actorResults.value = [];
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
actorInput.value.focus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
.button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.path {
|
||||
color: var(--glass-strong-20);
|
||||
}
|
||||
|
||||
.target {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
padding: .25rem .75rem;
|
||||
fill: var(--glass);
|
||||
|
||||
&:hover {
|
||||
fill: var(--error);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.target-id {
|
||||
font-family: monospace;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.results {
|
||||
padding: .25rem 0;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
padding: .25rem .5rem;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.result-id {
|
||||
font-family: monospace;
|
||||
font-size: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<span
|
||||
v-if="actor.ageAtDeath"
|
||||
:title="`Passed ${formatDate(actor.ageAtDeath, 'MMMM d, yyyy')}`"
|
||||
:title="`Passed ${formatDate(actor.dateOfDeath, 'MMMM d, yyyy')}`"
|
||||
class="age age-death"
|
||||
>{{ actor.ageAtDeath }}</span>
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "traxxx-web",
|
||||
"version": "0.49.2",
|
||||
"version": "0.50.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "0.49.2",
|
||||
"version": "0.50.0",
|
||||
"dependencies": {
|
||||
"@brillout/json-serializer": "^0.5.8",
|
||||
"@dicebear/collection": "^7.0.5",
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
"overrides": {
|
||||
"vite": "$vite"
|
||||
},
|
||||
"version": "0.49.2",
|
||||
"version": "0.50.0",
|
||||
"imports": {
|
||||
"#/*": "./*.js"
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export default '/actor/edit/@actorId/*';
|
||||
@@ -7,12 +7,12 @@
|
||||
<template v-if="apply">Your revision has been submitted. Thank you for your contribution!</template>
|
||||
<template v-else>Your revision has been submitted for review. Thank you for your contribution!</template>
|
||||
|
||||
<ul>
|
||||
<ul v-if="actor">
|
||||
<li>
|
||||
<a
|
||||
:href="`/actor/${actor.id}/${actor.slug}`"
|
||||
class="link"
|
||||
>Return to actor</a>
|
||||
>{{ creating ? 'Go' : 'Return' }} to actor</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
@@ -22,21 +22,21 @@
|
||||
>Make another edit</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<li v-if="!creating">
|
||||
<a
|
||||
:href="`/actor/revs/${actor.id}/${actor.slug}`"
|
||||
class="link"
|
||||
>Go to actor revisions</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<li v-if="!creating">
|
||||
<a
|
||||
:href="`/user/${user.username}/revisions/actors`"
|
||||
class="link"
|
||||
>Go to user revisions</a>
|
||||
</li>
|
||||
|
||||
<li v-if="user.role !== 'user'">
|
||||
<li v-if="user.role !== 'user' && !creating">
|
||||
<a
|
||||
href="/admin/revisions/actors"
|
||||
class="link"
|
||||
@@ -49,7 +49,10 @@
|
||||
v-else
|
||||
@submit.prevent
|
||||
>
|
||||
<div class="editor-header">
|
||||
<div
|
||||
v-if="actor"
|
||||
class="editor-header"
|
||||
>
|
||||
<h2 class="heading ellipsis">Edit actor #{{ actor.id }} - {{ actor.name }}</h2>
|
||||
|
||||
<a
|
||||
@@ -59,6 +62,13 @@
|
||||
>Go to actor</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="editor-header"
|
||||
>
|
||||
<h2 class="heading ellipsis">Add actor</h2>
|
||||
</div>
|
||||
|
||||
<ul class="nolist">
|
||||
<li
|
||||
v-for="item in fields"
|
||||
@@ -281,6 +291,7 @@
|
||||
</select>
|
||||
|
||||
<input
|
||||
v-if="item.hasDescription !== false"
|
||||
v-model="edits[item.key].description"
|
||||
class="description input"
|
||||
placeholder="Description"
|
||||
@@ -317,7 +328,7 @@
|
||||
|
||||
<div class="editor-actions">
|
||||
<Checkbox
|
||||
v-if="user.role !== 'user'"
|
||||
v-if="user.role !== 'user' && actor"
|
||||
label="Approve and apply immediately"
|
||||
:checked="apply"
|
||||
:disabled="editing.size === 0"
|
||||
@@ -346,6 +357,7 @@
|
||||
<script setup>
|
||||
import { ref, computed, inject } from 'vue';
|
||||
import { format } from 'date-fns';
|
||||
import omit from 'object.omit';
|
||||
|
||||
import EditSocials from '#/components/edit/socials.vue';
|
||||
import EditPlace from '#/components/edit/place.vue';
|
||||
@@ -361,13 +373,15 @@ import {
|
||||
post,
|
||||
} from '#/src/api.js';
|
||||
|
||||
import events from '#/src/events.js';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
|
||||
const user = pageContext.user;
|
||||
const actor = ref(pageContext.pageProps.actor);
|
||||
const actor = ref(pageContext.pageProps.actor || null);
|
||||
|
||||
const fields = computed(() => [
|
||||
...(actor.value.photos.length > 0 ? [{
|
||||
...(actor.value?.photos.length > 0 ? [{
|
||||
key: 'avatar',
|
||||
type: 'avatar',
|
||||
value: actor.value.avatar?.id,
|
||||
@@ -377,13 +391,13 @@ const fields = computed(() => [
|
||||
? [{
|
||||
key: 'name',
|
||||
type: 'string',
|
||||
value: actor.value.name,
|
||||
value: actor.value?.name,
|
||||
}]
|
||||
: []),
|
||||
{
|
||||
key: 'gender',
|
||||
type: 'select',
|
||||
value: actor.value.gender,
|
||||
value: actor.value?.gender || null,
|
||||
options: [null, 'female', 'male', 'transsexual', 'other'],
|
||||
inline: true,
|
||||
},
|
||||
@@ -391,7 +405,7 @@ const fields = computed(() => [
|
||||
key: 'dateOfBirth',
|
||||
label: 'date of birth',
|
||||
type: 'date',
|
||||
value: actor.value.dateOfBirth
|
||||
value: actor.value?.dateOfBirth
|
||||
? format(actor.value.dateOfBirth, 'yyyy-MM-dd')
|
||||
: null,
|
||||
inline: true,
|
||||
@@ -399,28 +413,28 @@ const fields = computed(() => [
|
||||
{
|
||||
key: 'socials',
|
||||
type: 'socials',
|
||||
value: actor.value.socials,
|
||||
value: actor.value?.socials || [],
|
||||
},
|
||||
{
|
||||
key: 'origin',
|
||||
type: 'place',
|
||||
value: {
|
||||
country: actor.value.origin?.country?.alpha2 || null,
|
||||
place: [actor.value.origin?.city, actor.value.origin?.state].filter(Boolean).join(', '),
|
||||
country: actor.value?.origin?.country?.alpha2 || null,
|
||||
place: [actor.value?.origin?.city, actor.value?.origin?.state].filter(Boolean).join(', '),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'residence',
|
||||
type: 'place',
|
||||
value: {
|
||||
country: actor.value.residence?.country?.alpha2 || null,
|
||||
place: [actor.value.residence?.city, actor.value.residence?.state].filter(Boolean).join(', '),
|
||||
country: actor.value?.residence?.country?.alpha2 || null,
|
||||
place: [actor.value?.residence?.city, actor.value?.residence?.state].filter(Boolean).join(', '),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'ethnicity',
|
||||
type: 'string',
|
||||
value: actor.value.ethnicity,
|
||||
value: actor.value?.ethnicity || null,
|
||||
suggestions: [
|
||||
'Asian',
|
||||
'Black',
|
||||
@@ -433,20 +447,20 @@ const fields = computed(() => [
|
||||
key: 'size',
|
||||
type: 'size',
|
||||
value: {
|
||||
metricHeight: actor.value.height?.metric,
|
||||
metricWeight: actor.value.weight?.metric,
|
||||
imperialHeight: actor.value.height?.imperial || [],
|
||||
imperialWeight: actor.value.weight?.imperial,
|
||||
metricHeight: actor.value?.height?.metric || null,
|
||||
metricWeight: actor.value?.weight?.metric || null,
|
||||
imperialHeight: actor.value?.height?.imperial || [],
|
||||
imperialWeight: actor.value?.weight?.imperial || null,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'figure',
|
||||
type: 'figure',
|
||||
value: {
|
||||
bust: actor.value.bust,
|
||||
cup: actor.value.cup,
|
||||
waist: actor.value.waist,
|
||||
hip: actor.value.hip,
|
||||
bust: actor.value?.bust || null,
|
||||
cup: actor.value?.cup || null,
|
||||
waist: actor.value?.waist || null,
|
||||
hip: actor.value?.hip || null,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -454,25 +468,25 @@ const fields = computed(() => [
|
||||
type: 'augmentation',
|
||||
note: 'Provide explicit evidence, such as social media posts, visible scarring, or before/after. Avoid "it\'s obvious".',
|
||||
value: {
|
||||
naturalBoobs: actor.value.naturalBoobs,
|
||||
boobsVolume: actor.value.boobsVolume,
|
||||
boobsImplant: actor.value.boobsImplant,
|
||||
boobsPlacement: actor.value.boobsPlacement,
|
||||
boobsIncision: actor.value.boobsIncision,
|
||||
boobsSurgeon: actor.value.boobsSurgeon,
|
||||
naturalButt: actor.value.naturalButt,
|
||||
buttVolume: actor.value.buttVolume,
|
||||
buttImplant: actor.value.buttImplant,
|
||||
naturalLips: actor.value.naturalLips,
|
||||
lipsVolume: actor.value.lipsVolume,
|
||||
naturalLabia: actor.value.naturalLabia,
|
||||
naturalBoobs: actor.value?.naturalBoobs || null,
|
||||
boobsVolume: actor.value?.boobsVolume || null,
|
||||
boobsImplant: actor.value?.boobsImplant || null,
|
||||
boobsPlacement: actor.value?.boobsPlacement || null,
|
||||
boobsIncision: actor.value?.boobsIncision || null,
|
||||
boobsSurgeon: actor.value?.boobsSurgeon || null,
|
||||
naturalButt: actor.value?.naturalButt || null,
|
||||
buttVolume: actor.value?.buttVolume || null,
|
||||
buttImplant: actor.value?.buttImplant || null,
|
||||
naturalLips: actor.value?.naturalLips || null,
|
||||
lipsVolume: actor.value?.lipsVolume || null,
|
||||
naturalLabia: actor.value?.naturalLabia || null,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'hairColor',
|
||||
label: 'hair color',
|
||||
type: 'select',
|
||||
value: actor.value.hairColor,
|
||||
value: actor.value?.hairColor || null,
|
||||
options: [
|
||||
null,
|
||||
'black',
|
||||
@@ -491,7 +505,7 @@ const fields = computed(() => [
|
||||
key: 'eyes',
|
||||
label: 'eye color',
|
||||
type: 'select',
|
||||
value: actor.value.eyes,
|
||||
value: actor.value?.eyes || null,
|
||||
options: [
|
||||
null,
|
||||
'blue',
|
||||
@@ -506,8 +520,8 @@ const fields = computed(() => [
|
||||
key: 'tattoos',
|
||||
type: 'has',
|
||||
value: {
|
||||
has: actor.value.hasTattoos,
|
||||
description: actor.value.tattoos,
|
||||
has: actor.value?.hasTattoos || null,
|
||||
description: actor.value?.tattoos || null,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -515,14 +529,14 @@ const fields = computed(() => [
|
||||
type: 'has',
|
||||
note: 'Excludes earrings',
|
||||
value: {
|
||||
has: actor.value.hasPiercings,
|
||||
description: actor.value.piercings,
|
||||
has: actor.value?.hasPiercings || null,
|
||||
description: actor.value?.piercings || null,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'agency',
|
||||
type: 'string',
|
||||
value: actor.value.agency,
|
||||
value: actor.value?.agency || null,
|
||||
suggestions: [
|
||||
'101 Modeling',
|
||||
'Adult Talent Managers (ATMLA)',
|
||||
@@ -540,25 +554,41 @@ const fields = computed(() => [
|
||||
key: 'penis',
|
||||
type: 'penis',
|
||||
value: {
|
||||
metricLength: actor.value.penisLength?.metric,
|
||||
metricGirth: actor.value.penisGirth?.metric,
|
||||
imperialLength: actor.value.penisLength?.imperial,
|
||||
imperialGirth: actor.value.penisGirth?.imperial,
|
||||
isCircumcised: actor.value.isCircumcised,
|
||||
metricLength: actor.value?.penisLength?.metric || null,
|
||||
metricGirth: actor.value?.penisGirth?.metric || null,
|
||||
imperialLength: actor.value?.penisLength?.imperial || null,
|
||||
imperialGirth: actor.value?.penisGirth?.imperial || null,
|
||||
isCircumcised: actor.value?.isCircumcised || null,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'dateOfDeath',
|
||||
label: 'date of death',
|
||||
type: 'date',
|
||||
value: actor.value.dateOfDeath
|
||||
value: actor.value?.dateOfDeath
|
||||
? format(actor.value.dateOfDeath, 'yyyy-MM-dd')
|
||||
: null,
|
||||
},
|
||||
]);
|
||||
{
|
||||
key: 'allowGlobalMatch',
|
||||
label: 'global match',
|
||||
type: 'has',
|
||||
note: 'Allow this actor to be assigned to scenes automatically, overriding single-name protections.',
|
||||
hasDescription: false,
|
||||
value: {
|
||||
has: actor.value?.isGlobal || null,
|
||||
},
|
||||
},
|
||||
]
|
||||
.filter((field) => (actor.value ? true : ['name', 'gender'].includes(field.key)))
|
||||
.map((field) => ({
|
||||
...field,
|
||||
forced: actor.value ? field.forced : true,
|
||||
})));
|
||||
|
||||
const editing = ref(new Set());
|
||||
const editing = ref(new Set(actor.value ? [] : ['name', 'gender']));
|
||||
const edits = ref(Object.fromEntries(fields.value.map((field) => [field.key, field.value])));
|
||||
const creating = !actor.value;
|
||||
const comment = ref(null);
|
||||
const apply = ref(user.role !== 'user');
|
||||
const submitting = ref(false);
|
||||
@@ -609,13 +639,54 @@ const groupMap = {
|
||||
weight: 'size',
|
||||
};
|
||||
|
||||
async function submitCreate() {
|
||||
submitting.value = true;
|
||||
|
||||
try {
|
||||
const newActor = {
|
||||
actor: Object.fromEntries(Array.from(['name', 'gender']).flatMap((key) => {
|
||||
if (!edits.value[key]) {
|
||||
throw new Error(`Missing ${key}`);
|
||||
}
|
||||
|
||||
if (edits.value[key] && typeof edits.value[key] === 'object' && !Array.isArray(edits.value[key])) {
|
||||
return Object.entries(edits.value[key]).map(([valueKey, value]) => [keyMap[key]?.[valueKey] || valueKey, value]);
|
||||
}
|
||||
|
||||
return [[key, edits.value[key]]];
|
||||
})),
|
||||
comment: comment.value,
|
||||
};
|
||||
|
||||
const { actor: createdActor } = await post('/actors', newActor, {
|
||||
successFeedback: 'Actor has been added.',
|
||||
appendErrorMessage: true,
|
||||
});
|
||||
|
||||
actor.value = createdActor;
|
||||
} catch (error) {
|
||||
events.emit('feedback', {
|
||||
type: 'error',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
submitting.value = false;
|
||||
submitted.value = true;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!actor.value) {
|
||||
submitCreate();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
submitting.value = true;
|
||||
|
||||
await post('/revisions/actors', {
|
||||
actorId: actor.value.id,
|
||||
edits: {
|
||||
edits: omit({
|
||||
...Object.fromEntries(Array.from(editing.value).flatMap((key) => {
|
||||
if (edits.value[key] && typeof edits.value[key] === 'object' && !Array.isArray(edits.value[key])) {
|
||||
return Object.entries(edits.value[key]).map(([valueKey, value]) => [keyMap[key]?.[valueKey] || valueKey, value]);
|
||||
@@ -629,15 +700,16 @@ async function submit() {
|
||||
penisLength: penisUnits.value === 'imperial' ? edits.value.penis.imperialLength : edits.value.penis.metricLength,
|
||||
penisGirth: penisUnits.value === 'imperial' ? edits.value.penis.imperialGirth : edits.value.penis.metricGirth,
|
||||
}).filter(([key]) => editing.value.has(groupMap[key] || key))),
|
||||
metricHeight: undefined,
|
||||
metricWeight: undefined,
|
||||
imperialHeight: undefined,
|
||||
imperialWeight: undefined,
|
||||
metricLength: undefined,
|
||||
metricGirth: undefined,
|
||||
imperialLength: undefined,
|
||||
imperialGirth: undefined,
|
||||
},
|
||||
}, [
|
||||
'metricHeight',
|
||||
'metricWeight',
|
||||
'imperialHeight',
|
||||
'imperialWeight',
|
||||
'metricLength',
|
||||
'metricGirth',
|
||||
'imperialLength',
|
||||
'imperialGirth',
|
||||
]),
|
||||
sizeUnits: sizeUnits.value,
|
||||
figureUnits: figureUnits.value,
|
||||
penisUnits: penisUnits.value,
|
||||
34
pages/actors/edit/+onBeforeRender.js
Normal file
34
pages/actors/edit/+onBeforeRender.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { redirect } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
||||
|
||||
import { fetchActorsById } from '#/src/actors.js';
|
||||
import { fetchCountries } from '#/src/countries.js';
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
if (!pageContext.user) {
|
||||
throw redirect(`/login?r=${encodeURIComponent(pageContext.urlOriginal)}`);
|
||||
}
|
||||
|
||||
const [actor] = pageContext.routeParams.actorId
|
||||
? await fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user)
|
||||
: [];
|
||||
|
||||
const countries = await fetchCountries();
|
||||
|
||||
/*
|
||||
if (!actor) {
|
||||
throw render(404, `Cannot find actor '${pageContext.routeParams.actorId}'.`);
|
||||
}
|
||||
*/
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
title: actor
|
||||
? `Editing '${actor.name}'`
|
||||
: 'Adding actor',
|
||||
pageProps: {
|
||||
actor,
|
||||
countries,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
20
pages/actors/edit/+route.js
Normal file
20
pages/actors/edit/+route.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// export default '/actor/edit/@actorId/*';
|
||||
// import { redirect } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
||||
import { match } from 'path-to-regexp';
|
||||
|
||||
const path = '/actor/(edit|new)/:actorId?/:actorSlug?';
|
||||
const urlMatch = match(path, { decode: decodeURIComponent });
|
||||
|
||||
export default (pageContext) => {
|
||||
const matched = urlMatch(pageContext.urlPathname);
|
||||
|
||||
if (matched) {
|
||||
return {
|
||||
routeParams: matched.params.actorId ? {
|
||||
actorId: matched.params.actorId,
|
||||
} : {},
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -44,7 +44,7 @@
|
||||
</a>
|
||||
|
||||
<a
|
||||
v-if="user?.abilities?.some((ability) => ability.plainUrls)"
|
||||
v-if="user?.abilities?.some((ability) => ability.subject === 'plainUrls')"
|
||||
:href="entity.url"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
|
||||
@@ -57,19 +57,23 @@ export async function onBeforeRender(pageContext) {
|
||||
fetchReleases(pageContext, entityId),
|
||||
]);
|
||||
|
||||
const entityIds = entity.isIndependent || !entity.parent
|
||||
? [entity.id]
|
||||
: [entity.id, entity.parent.id];
|
||||
|
||||
const campaigns = await getRandomCampaigns([
|
||||
{
|
||||
entityIds: [entity.id, entity.parent?.id].filter(Boolean),
|
||||
entityIds,
|
||||
minRatio: 3,
|
||||
allowRandomFallback: false,
|
||||
},
|
||||
{
|
||||
entityIds: [entity.id, entity.parent?.id].filter(Boolean),
|
||||
entityIds,
|
||||
minRatio: 3,
|
||||
allowRandomFallback: false,
|
||||
},
|
||||
pageContext.routeParams.domain === 'scenes' ? {
|
||||
entityIds: [entity.id, entity.parent?.id].filter(Boolean),
|
||||
entityIds,
|
||||
minRatio: 0.75,
|
||||
maxRatio: 1.25,
|
||||
allowRandomFallback: false,
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
|
||||
<Link
|
||||
:href="user?.abilities?.some((ability) => ability.plainUrls) ? scene.url : scene.watchUrl"
|
||||
:href="user?.abilities?.some((ability) => ability.subject === 'plainUrls') ? scene.url : scene.watchUrl"
|
||||
:title="scene.date ? formatDate(scene.date.toISOString(), 'y-MM-dd hh:mm') : `Release date unknown, added ${formatDate(scene.createdAt, 'y-MM-dd')}`"
|
||||
target="_blank"
|
||||
class="date nolink"
|
||||
|
||||
BIN
public/img/rta.gif
Normal file
BIN
public/img/rta.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -70,6 +70,9 @@ async function onRenderHtml(pageContext) {
|
||||
|
||||
<meta name="description" content="Keep track of new porn releases and re-discover classics from your favorite porn stars and sites" />
|
||||
|
||||
<!-- RTA restricted to adults label -->
|
||||
<meta name="RATING" content="RTA-5042-1996-1400-1577-RTA" />
|
||||
|
||||
${config.analytics.enabled ? dangerouslySkipEscape(`<script src="${config.analytics.address}" data-website-id="${config.analytics.siteId}" data-exclude-hash="true" async></script>`) : ''}
|
||||
|
||||
<title>${title}</title>
|
||||
|
||||
109
src/actors.js
109
src/actors.js
@@ -3,6 +3,7 @@ import { differenceInYears } from 'date-fns';
|
||||
import { unit } from 'mathjs';
|
||||
import { MerkleJson } from 'merkle-json';
|
||||
import moment from 'moment';
|
||||
import { nanoid } from 'nanoid';
|
||||
import omit from 'object.omit';
|
||||
import convert from 'convert';
|
||||
import unprint from 'unprint';
|
||||
@@ -21,6 +22,8 @@ import slugify from '../utils/slugify.js';
|
||||
import { curateRevision } from './revisions.js';
|
||||
import { interpolateProfiles, platformsByHostname } from '../common/actors.mjs'; // eslint-disable-line import/namespace
|
||||
import { resolvePlace } from '../common/geo.mjs'; // eslint-disable-line import/namespace
|
||||
import { syncScenes, syncActors } from './sync.js';
|
||||
import verifyAbility from '../utils/verify-ability.js';
|
||||
|
||||
const logger = initLogger();
|
||||
const mj = new MerkleJson();
|
||||
@@ -55,7 +58,7 @@ const keyMap = {
|
||||
isCircumcised: 'circumcised',
|
||||
};
|
||||
|
||||
const socialsOrder = ['onlyfans', 'twitter', 'fansly', 'loyalfans', 'manyvids', 'pornhub', 'linktree', null];
|
||||
const socialsOrder = ['onlyfans', 'fansly', 'twitter', 'instagram', 'loyalfans', 'manyvids', 'pornhub', 'linktree', null];
|
||||
|
||||
export function curateActor(actor, context = {}) {
|
||||
return {
|
||||
@@ -80,6 +83,8 @@ export function curateActor(actor, context = {}) {
|
||||
ageThen: context.sceneDate && actor.date_of_birth && actor.date_of_birth.getFullYear() > 1
|
||||
? differenceInYears(context.sceneDate, actor.date_of_birth)
|
||||
: null,
|
||||
dateOfBirth: actor.date_of_birth,
|
||||
dateOfDeath: actor.date_of_death,
|
||||
bust: actor.bust,
|
||||
cup: actor.cup,
|
||||
waist: actor.waist,
|
||||
@@ -370,6 +375,10 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.isGlobal) {
|
||||
builder.where('entity_id', 0);
|
||||
}
|
||||
|
||||
// attribute filters
|
||||
['country'].forEach((attribute) => {
|
||||
if (filters[attribute]) {
|
||||
@@ -519,6 +528,102 @@ export async function fetchActors(filters, rawOptions, reqUser) {
|
||||
};
|
||||
}
|
||||
|
||||
function curateActorEntry(actor, context) {
|
||||
return {
|
||||
name: actor.name,
|
||||
slug: slugify(actor.name),
|
||||
entry_id: nanoid(), // allows for manual creation of multiple actors with the same name
|
||||
gender: actor.gender,
|
||||
comment: context?.comment,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createActor(newActor, context, reqUser) {
|
||||
if (!reqUser || reqUser.role === 'user') {
|
||||
throw new HttpError('You are not permitted to create actors', 403);
|
||||
}
|
||||
|
||||
const curatedActorEntry = curateActorEntry(newActor, context);
|
||||
const [actorEntry] = await knex('actors').insert(curatedActorEntry).returning('*');
|
||||
|
||||
await syncActors([actorEntry.id]);
|
||||
|
||||
return curateActor(actorEntry);
|
||||
}
|
||||
|
||||
export async function mergeActors(targetActorId, sourceActorId, reqUser) {
|
||||
if (!verifyAbility(reqUser, 'actor', 'merge')) {
|
||||
throw new HttpError('You are not permitted to merge actors', 403);
|
||||
}
|
||||
|
||||
const [targetActor, sourceActor] = await Promise.all([
|
||||
knex('actors')
|
||||
.where('id', targetActorId)
|
||||
.first(),
|
||||
knex('actors')
|
||||
.where('id', sourceActorId)
|
||||
.first(),
|
||||
]);
|
||||
|
||||
if (!targetActor) {
|
||||
throw new HttpError('Target actor not found', 404);
|
||||
}
|
||||
|
||||
if (!sourceActor) {
|
||||
throw new HttpError('Source actor not found', 404);
|
||||
}
|
||||
|
||||
if (targetActor.entity_id) {
|
||||
throw new HttpError('Target actor is not global', 400);
|
||||
}
|
||||
|
||||
if (targetActor.alias_for) {
|
||||
throw new HttpError('Target actor is aliased', 400);
|
||||
}
|
||||
|
||||
const trx = await knex.transaction();
|
||||
|
||||
await trx('actors')
|
||||
.update('alias_for', targetActorId)
|
||||
.where('id', sourceActorId);
|
||||
|
||||
const mergedProfiles = await trx('actors_profiles')
|
||||
.update('actor_id', targetActorId)
|
||||
.where('actor_id', sourceActorId)
|
||||
.returning('id');
|
||||
|
||||
const mergedScenes = await trx('releases_actors')
|
||||
.update({
|
||||
actor_id: targetActorId,
|
||||
alias_id: sourceActorId,
|
||||
})
|
||||
.where('actor_id', sourceActorId)
|
||||
.returning('release_id');
|
||||
|
||||
try {
|
||||
await trx.commit();
|
||||
} catch (error) {
|
||||
await trx.rollback();
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
await interpolateProfiles([targetActorId, sourceActorId], {
|
||||
knex,
|
||||
logger,
|
||||
moment,
|
||||
slugify,
|
||||
omit,
|
||||
}, { refreshView: false });
|
||||
|
||||
await syncScenes(mergedScenes.map((scene) => scene.release_id));
|
||||
|
||||
return {
|
||||
scenes: mergedScenes.length,
|
||||
profiles: mergedProfiles.length,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchActorRevisions(revisionId, filters = {}, reqUser) {
|
||||
const limit = filters.limit || 50;
|
||||
const page = filters.page || 1;
|
||||
@@ -723,6 +828,8 @@ async function applyActorRevision(revisionIds, reqUser) {
|
||||
slugify,
|
||||
omit,
|
||||
}, { refreshView: false });
|
||||
|
||||
await syncActors(actorIds);
|
||||
}
|
||||
|
||||
export async function reviewActorRevision(revisionId, isApproved, { feedback }, reqUser) {
|
||||
|
||||
@@ -203,6 +203,8 @@ export async function verifyKey(userId, key, req) {
|
||||
.then(() => {
|
||||
// no need to wait for this
|
||||
});
|
||||
|
||||
return fetchUser(storedKey.user_id);
|
||||
}
|
||||
|
||||
export async function createKey(reqUser) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import initLogger from './logger.js';
|
||||
import { curateRevision } from './revisions.js';
|
||||
import { getAffiliateSceneUrl } from './affiliates.js';
|
||||
import { censor } from './censor.js';
|
||||
import { syncScenes } from './sync.js';
|
||||
|
||||
const logger = initLogger();
|
||||
const mj = new MerkleJson();
|
||||
@@ -888,6 +889,10 @@ async function applySceneRevision(revisionIds, reqUser) {
|
||||
throw error;
|
||||
});
|
||||
}, Promise.resolve());
|
||||
|
||||
const sceneIds = Array.from(new Set(revisions.map((revision) => revision.scene_id)));
|
||||
|
||||
await syncScenes(sceneIds);
|
||||
}
|
||||
|
||||
export async function reviewSceneRevision(revisionId, isApproved, { feedback }, reqUser) {
|
||||
@@ -932,6 +937,10 @@ export async function reviewSceneRevision(revisionId, isApproved, { feedback },
|
||||
}
|
||||
|
||||
export async function createSceneRevision(sceneId, { edits, comment, apply }, reqUser) {
|
||||
if (!reqUser) {
|
||||
throw new HttpError('Must be authenticated to create scene revision', 401);
|
||||
}
|
||||
|
||||
const [
|
||||
[scene],
|
||||
openRevisions,
|
||||
|
||||
@@ -200,6 +200,10 @@ export async function createStash(newStash, sessionUser) {
|
||||
throw new HttpError('You are not authenthicated', 401);
|
||||
}
|
||||
|
||||
if (!newStash) {
|
||||
throw new HttpError('Missing new stash', 400);
|
||||
}
|
||||
|
||||
verifyStashName(newStash);
|
||||
|
||||
try {
|
||||
@@ -224,6 +228,14 @@ export async function updateStash(stashIdOrSlug, updatedStash, sessionUser) {
|
||||
throw new HttpError('You are not authenthicated', 401);
|
||||
}
|
||||
|
||||
if (!stashIdOrSlug) {
|
||||
throw new HttpError('Missing stash ID or slug', 400);
|
||||
}
|
||||
|
||||
if (!updatedStash) {
|
||||
throw new HttpError('Missing updated stash', 400);
|
||||
}
|
||||
|
||||
if (updatedStash.name) {
|
||||
verifyStashName(updatedStash);
|
||||
}
|
||||
@@ -260,6 +272,10 @@ export async function removeStash(stashId, sessionUser) {
|
||||
throw new HttpError('You are not authenthicated', 401);
|
||||
}
|
||||
|
||||
if (!stashId) {
|
||||
throw new HttpError('Missing stash ID', 400);
|
||||
}
|
||||
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
if (!stash) {
|
||||
@@ -283,6 +299,14 @@ export async function removeStash(stashId, sessionUser) {
|
||||
}
|
||||
|
||||
export async function stashActor(actorId, stashId, sessionUser) {
|
||||
if (!actorId) {
|
||||
throw new HttpError('Missing actor ID', 400);
|
||||
}
|
||||
|
||||
if (!stashId) {
|
||||
throw new HttpError('Missing stash ID', 400);
|
||||
}
|
||||
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
if (!stash) {
|
||||
@@ -324,6 +348,14 @@ export async function stashActor(actorId, stashId, sessionUser) {
|
||||
}
|
||||
|
||||
export async function unstashActor(actorId, stashId, sessionUser) {
|
||||
if (!actorId) {
|
||||
throw new HttpError('Missing actor ID', 400);
|
||||
}
|
||||
|
||||
if (!stashId) {
|
||||
throw new HttpError('Missing stash ID', 400);
|
||||
}
|
||||
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
if (!stash) {
|
||||
@@ -367,6 +399,14 @@ export async function unstashActor(actorId, stashId, sessionUser) {
|
||||
}
|
||||
|
||||
export async function stashScene(sceneId, stashId, sessionUser) {
|
||||
if (!sceneId) {
|
||||
throw new HttpError('Missing scene ID', 400);
|
||||
}
|
||||
|
||||
if (!stashId) {
|
||||
throw new HttpError('Missing stash ID', 400);
|
||||
}
|
||||
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
if (!stash) {
|
||||
@@ -409,6 +449,14 @@ export async function stashScene(sceneId, stashId, sessionUser) {
|
||||
}
|
||||
|
||||
export async function unstashScene(sceneId, stashId, sessionUser) {
|
||||
if (!sceneId) {
|
||||
throw new HttpError('Missing scene ID', 400);
|
||||
}
|
||||
|
||||
if (!stashId) {
|
||||
throw new HttpError('Missing stash ID', 400);
|
||||
}
|
||||
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
if (!stash) {
|
||||
@@ -448,6 +496,14 @@ export async function unstashScene(sceneId, stashId, sessionUser) {
|
||||
}
|
||||
|
||||
export async function stashMovie(movieId, stashId, sessionUser) {
|
||||
if (!movieId) {
|
||||
throw new HttpError('Missing movie ID', 400);
|
||||
}
|
||||
|
||||
if (!stashId) {
|
||||
throw new HttpError('Missing stash ID', 400);
|
||||
}
|
||||
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
if (!stash) {
|
||||
@@ -489,6 +545,14 @@ export async function stashMovie(movieId, stashId, sessionUser) {
|
||||
}
|
||||
|
||||
export async function unstashMovie(movieId, stashId, sessionUser) {
|
||||
if (!movieId) {
|
||||
throw new HttpError('Missing movie ID', 400);
|
||||
}
|
||||
|
||||
if (!stashId) {
|
||||
throw new HttpError('Missing stash ID', 400);
|
||||
}
|
||||
|
||||
const stash = await fetchStashById(stashId, sessionUser);
|
||||
|
||||
if (!stash) {
|
||||
|
||||
440
src/sync.js
Normal file
440
src/sync.js
Normal file
@@ -0,0 +1,440 @@
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import initLogger from './logger.js';
|
||||
import { knexOwner as knex } from './knex.js';
|
||||
import { searchApi, indexApi } from './manticore.js';
|
||||
import chunk from '../utils/chunk.js';
|
||||
import filterTitle from '../utils/filter-title.js';
|
||||
|
||||
const logger = initLogger();
|
||||
|
||||
export async function syncStashes(domain = 'scene', ids) {
|
||||
const stashes = await knex(`stashes_${domain}s`)
|
||||
.select(
|
||||
`stashes_${domain}s.id as stashed_id`,
|
||||
`stashes_${domain}s.${domain}_id`,
|
||||
'stashes.id as stash_id',
|
||||
'stashes.user_id as user_id',
|
||||
`stashes_${domain}s.created_at as created_at`,
|
||||
)
|
||||
.modify((builder) => {
|
||||
if (ids) {
|
||||
builder.whereRaw(`stashes_${domain}s.${domain}_id = ANY(?)`, [ids]);
|
||||
}
|
||||
})
|
||||
.leftJoin('stashes', 'stashes.id', `stashes_${domain}s.stash_id`);
|
||||
|
||||
await chunk(stashes, 1000).reduce(async (chain, stashChunk, index) => {
|
||||
await chain;
|
||||
|
||||
const stashDocs = stashChunk.map((stash) => ({
|
||||
replace: {
|
||||
index: `${domain}s_stashed`,
|
||||
id: stash.stashed_id,
|
||||
doc: {
|
||||
[`${domain}_id`]: stash[`${domain}_id`],
|
||||
stash_id: stash.stash_id,
|
||||
user_id: stash.user_id,
|
||||
created_at: Math.round(stash.created_at.getTime() / 1000),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
await indexApi.bulk(stashDocs.map((doc) => JSON.stringify(doc)).join('\n'));
|
||||
|
||||
logger.verbose(`Seeded ${index * 1000 + stashChunk.length}/${stashes.length} ${domain} stashes`);
|
||||
}, Promise.resolve());
|
||||
|
||||
// purge orphaned docs
|
||||
const itemIds = ids ?? [...new Set(stashes.map((s) => s[`${domain}_id`]))];
|
||||
|
||||
if (itemIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validStashedIds = new Set(stashes.map((stash) => stash.stashed_id));
|
||||
|
||||
await chunk(itemIds, 1000).reduce(async (chain, itemIdChunk) => {
|
||||
await chain;
|
||||
|
||||
const searchResponse = await searchApi.search({
|
||||
index: `${domain}s_stashed`,
|
||||
query: {
|
||||
in: {
|
||||
[`${domain}_id`]: itemIdChunk,
|
||||
},
|
||||
},
|
||||
limit: 1000,
|
||||
});
|
||||
|
||||
const docs = searchResponse?.hits?.hits ?? [];
|
||||
|
||||
const orphanedIds = docs
|
||||
.map((hit) => hit._id)
|
||||
.filter((manticoreId) => !validStashedIds.has(manticoreId));
|
||||
|
||||
if (orphanedIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteDocs = orphanedIds.map((orphanId) => ({
|
||||
delete: {
|
||||
index: `${domain}s_stashed`,
|
||||
id: orphanId,
|
||||
},
|
||||
}));
|
||||
|
||||
await indexApi.bulk(deleteDocs.map((doc) => JSON.stringify(doc)).join('\n'));
|
||||
|
||||
logger.verbose(`Purged ${orphanedIds.length} orphaned ${domain} stash documents`);
|
||||
}, Promise.resolve());
|
||||
}
|
||||
|
||||
export async function syncManticoreScenes(sceneIds) {
|
||||
logger.info(`Updating Manticore search documents for ${sceneIds ? sceneIds.length : 'all' } scenes`);
|
||||
|
||||
const scenes = await knex.raw(`
|
||||
SELECT
|
||||
releases.id AS id,
|
||||
releases.title,
|
||||
releases.created_at,
|
||||
releases.date,
|
||||
releases.shoot_id,
|
||||
scenes_meta.stashed,
|
||||
entities.id as channel_id,
|
||||
entities.slug as channel_slug,
|
||||
entities.name as channel_name,
|
||||
parents.id as network_id,
|
||||
parents.slug as network_slug,
|
||||
parents.name as network_name,
|
||||
studios.id as studio_id,
|
||||
studios.slug as studio_slug,
|
||||
studios.name as studio_name,
|
||||
grandparents.id as parent_network_id,
|
||||
COALESCE(JSON_AGG(DISTINCT (actors.id, actors.name)) FILTER (WHERE actors.id IS NOT NULL), '[]') as actors,
|
||||
COALESCE(JSON_AGG(DISTINCT (tags.id, tags.name, tags.priority, tags_aliases.name)) FILTER (WHERE tags.id IS NOT NULL), '[]') as tags,
|
||||
COALESCE(JSON_AGG(DISTINCT (movies.id, movies.title)) FILTER (WHERE movies.id IS NOT NULL), '[]') as movies,
|
||||
COALESCE(JSON_AGG(DISTINCT (series.id, series.title)) FILTER (WHERE series.id IS NOT NULL), '[]') as series,
|
||||
studios.showcased IS NOT false
|
||||
AND (entities.showcased IS NOT false OR COALESCE(studios.showcased, false) = true)
|
||||
AND (parents.showcased IS NOT false OR COALESCE(entities.showcased, false) = true OR COALESCE(studios.showcased, false) = true)
|
||||
AND (releases_summaries.batch_showcased IS NOT false)
|
||||
AS showcased,
|
||||
row_number() OVER (PARTITION BY releases.entry_id, parents.id ORDER BY releases.effective_date DESC) as dupe_index
|
||||
FROM releases
|
||||
LEFT JOIN releases_summaries ON releases_summaries.release_id = releases.id
|
||||
LEFT JOIN scenes_meta ON scenes_meta.scene_id = releases.id
|
||||
LEFT JOIN entities ON releases.entity_id = entities.id
|
||||
LEFT JOIN entities AS parents ON parents.id = entities.parent_id
|
||||
LEFT JOIN entities AS grandparents ON grandparents.id = parents.parent_id
|
||||
LEFT JOIN entities AS studios ON studios.id = releases.studio_id
|
||||
LEFT JOIN releases_actors AS local_actors ON local_actors.release_id = releases.id
|
||||
LEFT JOIN releases_directors AS local_directors ON local_directors.release_id = releases.id
|
||||
LEFT JOIN releases_tags AS local_tags ON local_tags.release_id = releases.id
|
||||
LEFT JOIN actors ON local_actors.actor_id = actors.id
|
||||
LEFT JOIN actors AS directors ON local_directors.director_id = directors.id
|
||||
LEFT JOIN tags ON local_tags.tag_id = tags.id
|
||||
LEFT JOIN tags as tags_aliases ON local_tags.tag_id = tags_aliases.alias_for AND tags_aliases.secondary = true
|
||||
LEFT JOIN movies_scenes ON movies_scenes.scene_id = releases.id
|
||||
LEFT JOIN movies ON movies.id = movies_scenes.movie_id
|
||||
LEFT JOIN series_scenes ON series_scenes.scene_id = releases.id
|
||||
LEFT JOIN series ON series.id = series_scenes.serie_id
|
||||
${sceneIds ? 'WHERE releases.id = ANY(?)' : ''}
|
||||
GROUP BY
|
||||
releases.id,
|
||||
releases.title,
|
||||
releases.created_at,
|
||||
releases.date,
|
||||
releases.shoot_id,
|
||||
scenes_meta.stashed,
|
||||
releases_summaries.batch_showcased,
|
||||
entities.id,
|
||||
entities.name,
|
||||
entities.slug,
|
||||
entities.alias,
|
||||
entities.showcased,
|
||||
parents.id,
|
||||
parents.name,
|
||||
parents.slug,
|
||||
parents.alias,
|
||||
grandparents.id,
|
||||
studios.id,
|
||||
studios.name,
|
||||
studios.slug,
|
||||
parents.showcased,
|
||||
studios.showcased
|
||||
`, sceneIds && [sceneIds]);
|
||||
|
||||
const scenesById = Object.fromEntries(scenes.rows.map((scene) => [scene.id, scene]));
|
||||
|
||||
const docs = (sceneIds || Object.keys(scenesById)).map((sceneId) => {
|
||||
const scene = scenesById[sceneId];
|
||||
|
||||
if (!scene) {
|
||||
return {
|
||||
delete: {
|
||||
index: 'scenes',
|
||||
id: sceneId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const flatActors = scene.actors.flatMap((actor) => actor.f2.split(' '));
|
||||
const flatTags = scene.tags.filter((tag) => tag.f3 > 6).flatMap((tag) => [tag.f2].concat(tag.f4)).filter(Boolean); // only make top tags searchable to minimize cluttered results
|
||||
const filteredTitle = filterTitle(scene.title, [...flatActors, ...flatTags]);
|
||||
|
||||
return {
|
||||
replace: {
|
||||
index: 'scenes',
|
||||
id: scene.id,
|
||||
doc: {
|
||||
title: scene.title || undefined,
|
||||
title_filtered: filteredTitle || undefined,
|
||||
date: scene.date ? Math.round(scene.date.getTime() / 1000) : undefined,
|
||||
created_at: Math.round(scene.created_at.getTime() / 1000),
|
||||
effective_date: Math.round((scene.date || scene.created_at).getTime() / 1000),
|
||||
is_showcased: scene.showcased,
|
||||
shoot_id: scene.shoot_id || undefined,
|
||||
channel_id: scene.channel_id,
|
||||
channel_slug: scene.channel_slug,
|
||||
channel_name: scene.channel_name,
|
||||
network_id: scene.network_id || undefined,
|
||||
network_slug: scene.network_slug || undefined,
|
||||
network_name: scene.network_name || undefined,
|
||||
studio_id: scene.studio_id || undefined,
|
||||
studio_slug: scene.studio_slug || undefined,
|
||||
studio_name: scene.studio_name || undefined,
|
||||
entity_ids: [scene.channel_id, scene.network_id, scene.parent_network_id, scene.studio_id].filter(Boolean), // manticore does not support OR, this allows IN
|
||||
actor_ids: scene.actors.map((actor) => actor.f1),
|
||||
actors: scene.actors.map((actor) => actor.f2).join(),
|
||||
tag_ids: scene.tags.map((tag) => tag.f1),
|
||||
tags: flatTags.join(' '), // only make top tags searchable to minimize cluttered results
|
||||
movie_ids: scene.movies.map((movie) => movie.f1),
|
||||
movies: scene.movies.map((movie) => movie.f2).join(' '),
|
||||
serie_ids: scene.series.map((serie) => serie.f1),
|
||||
series: scene.series.map((serie) => serie.f2).join(' '),
|
||||
meta: scene.date ? format(scene.date, 'y yy M MMM MMMM d') : undefined,
|
||||
stashed: scene.stashed || 0,
|
||||
dupe_index: scene.dupe_index || 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (docs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [manticoreResult] = await Promise.all([
|
||||
chunk(docs, 10000).reduce(async (chain, docsChunk, index, array) => {
|
||||
const acc = await chain;
|
||||
const data = await indexApi.bulk(docsChunk.map((doc) => JSON.stringify(doc)).join('\n'));
|
||||
|
||||
logger.verbose(`Seeded ${index + 1}/${array.length}, errors: ${data.errors} ${data.error}`);
|
||||
|
||||
return acc.concat(data.items);
|
||||
}, Promise.resolve([])),
|
||||
syncStashes('scene', sceneIds),
|
||||
]);
|
||||
|
||||
return manticoreResult;
|
||||
}
|
||||
|
||||
export async function syncScenes(releaseIds) {
|
||||
await knex.raw('REFRESH MATERIALIZED VIEW scenes_meta;');
|
||||
|
||||
await syncManticoreScenes(releaseIds);
|
||||
}
|
||||
|
||||
export async function syncManticoreMovies(movieIds) {
|
||||
logger.info(`Updating Manticore search documents for ${movieIds ? movieIds.length : 'all' } movies`);
|
||||
|
||||
const movies = await knex.raw(`
|
||||
SELECT
|
||||
movies.id AS id,
|
||||
movies.title,
|
||||
movies.created_at,
|
||||
movies.date,
|
||||
movies_meta.stashed,
|
||||
entities.id as channel_id,
|
||||
entities.slug as channel_slug,
|
||||
entities.name as channel_name,
|
||||
parents.id as network_id,
|
||||
parents.slug as network_slug,
|
||||
parents.name as network_name,
|
||||
movies_covers IS NOT NULL as has_cover,
|
||||
COALESCE(JSON_AGG(DISTINCT (actors.id, actors.name)) FILTER (WHERE actors.id IS NOT NULL), '[]') as actors,
|
||||
COALESCE(JSON_AGG(DISTINCT (tags.id, tags.name, tags.priority, tags_aliases.name)) FILTER (WHERE tags.id IS NOT NULL), '[]') as tags,
|
||||
COALESCE(JSON_AGG(DISTINCT (movie_tags.id, movie_tags.name, movie_tags.priority, movie_tags_aliases.name)) FILTER (WHERE movie_tags.id IS NOT NULL), '[]') as movie_tags,
|
||||
row_number() OVER (PARTITION BY movies.entry_id, parents.id ORDER BY movies.effective_date DESC) as dupe_index
|
||||
FROM movies
|
||||
LEFT JOIN movies_meta ON movies_meta.movie_id = movies.id
|
||||
LEFT JOIN movies_scenes ON movies_scenes.movie_id = movies.id
|
||||
LEFT JOIN movies_tags ON movies_tags.movie_id = movies.id
|
||||
LEFT JOIN entities ON movies.entity_id = entities.id
|
||||
LEFT JOIN entities AS parents ON parents.id = entities.parent_id
|
||||
LEFT JOIN releases_actors AS local_actors ON local_actors.release_id = movies_scenes.scene_id
|
||||
LEFT JOIN releases_directors AS local_directors ON local_directors.release_id = movies_scenes.scene_id
|
||||
LEFT JOIN releases_tags AS local_tags ON local_tags.release_id = movies_scenes.scene_id
|
||||
LEFT JOIN actors ON local_actors.actor_id = actors.id
|
||||
LEFT JOIN actors AS directors ON local_directors.director_id = directors.id
|
||||
LEFT JOIN tags ON local_tags.tag_id = tags.id
|
||||
LEFT JOIN tags as tags_aliases ON local_tags.tag_id = tags_aliases.alias_for AND tags_aliases.secondary = true
|
||||
LEFT JOIN tags as movie_tags ON movies_tags.tag_id = movie_tags.id
|
||||
LEFT JOIN tags as movie_tags_aliases ON movies_tags.tag_id = movie_tags_aliases.alias_for AND movie_tags_aliases.secondary = true
|
||||
LEFT JOIN movies_covers ON movies_covers.movie_id = movies.id
|
||||
${movieIds ? 'WHERE movies.id = ANY(?)' : ''}
|
||||
GROUP BY
|
||||
movies.id,
|
||||
movies.title,
|
||||
movies.created_at,
|
||||
movies.date,
|
||||
movies_meta.stashed,
|
||||
movies_meta.stashed_scenes,
|
||||
movies_meta.stashed_total,
|
||||
entities.id,
|
||||
entities.name,
|
||||
entities.slug,
|
||||
entities.alias,
|
||||
parents.id,
|
||||
parents.name,
|
||||
parents.slug,
|
||||
parents.alias,
|
||||
movies_covers.*
|
||||
`, movieIds && [movieIds]);
|
||||
|
||||
const moviesById = Object.fromEntries(movies.rows.map((movie) => [movie.id, movie]));
|
||||
|
||||
const docs = (movieIds || Object.keys(moviesById)).map((movieId) => {
|
||||
const movie = moviesById[movieId];
|
||||
|
||||
if (!movie) {
|
||||
return {
|
||||
delete: {
|
||||
index: 'movies',
|
||||
id: movieId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const combinedTags = Object.values(Object.fromEntries(movie.tags.concat(movie.movie_tags).map((tag) => [tag.f1, {
|
||||
id: tag.f1,
|
||||
name: tag.f2,
|
||||
priority: tag.f3,
|
||||
alias: tag.f4,
|
||||
}])));
|
||||
|
||||
const flatActors = movie.actors.flatMap((actor) => actor.f2.match(/[\w']+/g)); // match word characters to filter out brackets etc.
|
||||
const flatTags = combinedTags.filter((tag) => tag.priority > 6).flatMap((tag) => (tag.alias ? `${tag.name} ${tag.alias}` : tag.name).match(/[\w']+/g)); // only make top tags searchable to minimize cluttered results
|
||||
const filteredTitle = movie.title && [...flatActors, ...flatTags].reduce((accTitle, tag) => accTitle.replace(new RegExp(tag.replace(/[^\w\s]+/g, ''), 'gi'), ''), movie.title).trim().replace(/\s{2,}/g, ' ');
|
||||
|
||||
return {
|
||||
replace: {
|
||||
index: 'movies',
|
||||
id: movie.id,
|
||||
doc: {
|
||||
title: movie.title || undefined,
|
||||
title_filtered: filteredTitle || undefined,
|
||||
date: movie.date ? Math.round(movie.date.getTime() / 1000) : undefined,
|
||||
created_at: Math.round(movie.created_at.getTime() / 1000),
|
||||
effective_date: Math.round((movie.date || movie.created_at).getTime() / 1000),
|
||||
channel_id: movie.channel_id,
|
||||
channel_slug: movie.channel_slug,
|
||||
channel_name: movie.channel_name,
|
||||
network_id: movie.network_id || undefined,
|
||||
network_slug: movie.network_slug || undefined,
|
||||
network_name: movie.network_name || undefined,
|
||||
entity_ids: [movie.channel_id, movie.network_id].filter(Boolean), // manticore does not support OR, this allows IN
|
||||
actor_ids: movie.actors.map((actor) => actor.f1),
|
||||
actors: movie.actors.map((actor) => actor.f2).join(),
|
||||
tag_ids: combinedTags.map((tag) => tag.id),
|
||||
tags: flatTags.join(' '),
|
||||
has_cover: movie.has_cover,
|
||||
meta: movie.date ? format(movie.date, 'y yy M MMM MMMM d') : undefined,
|
||||
stashed: movie.stashed || 0,
|
||||
stashed_scenes: movie.stashed_scenes || 0,
|
||||
stashed_total: movie.stashed_total || 0,
|
||||
dupe_index: movie.dupe_index || 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (docs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return indexApi.bulk(docs.map((doc) => JSON.stringify(doc)).join('\n'));
|
||||
}
|
||||
|
||||
export async function syncMovies(releaseIds) {
|
||||
await knex.raw('REFRESH MATERIALIZED VIEW movies_meta;');
|
||||
|
||||
await syncManticoreMovies(releaseIds);
|
||||
}
|
||||
|
||||
export async function syncManticoreActors(actorIds) {
|
||||
logger.info(`Updating Manticore search documents for ${actorIds ? actorIds.length : 'all' } actors`);
|
||||
|
||||
// manually select date of birth, otherwise it is retrieved in local timezone but interpreted as UTC...
|
||||
const actors = await knex.raw(`
|
||||
SELECT
|
||||
actors.*,
|
||||
actors_meta.*,
|
||||
date_of_birth AT TIME ZONE 'Europe/Amsterdam' AT TIME ZONE 'UTC' as dob
|
||||
FROM actors
|
||||
LEFT JOIN actors_meta ON actors_meta.actor_id = actors.id
|
||||
${actorIds ? 'WHERE actors.id = ANY(?)' : ''}
|
||||
`, actorIds && [actorIds]);
|
||||
|
||||
const actorsById = Object.fromEntries(actors.rows.map((actor) => [actor.id, actor]));
|
||||
|
||||
const docs = (actorIds || Object.keys(actorsById)).map((actorId) => {
|
||||
const actor = actorsById[actorId];
|
||||
|
||||
if (!actor) {
|
||||
return {
|
||||
delete: {
|
||||
index: 'actors',
|
||||
id: actorId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
replace: {
|
||||
index: 'actors',
|
||||
id: actor.id,
|
||||
doc: {
|
||||
entity_id: actor.entity_id,
|
||||
name: actor.name,
|
||||
slug: actor.slug,
|
||||
gender: actor.gender || undefined,
|
||||
date_of_birth: actor.dob ? Math.round(actor.dob.getTime() / 1000) : undefined,
|
||||
has_avatar: !!actor.avatar_media_id,
|
||||
country: actor.birth_country_alpha2 || undefined,
|
||||
height: actor.height || undefined,
|
||||
mass: actor.weight || undefined, // weight is a reserved keyword in manticore
|
||||
cup: actor.cup || undefined,
|
||||
natural_boobs: actor.natural_boobs === null ? 0 : Number(actor.natural_boobs) + 1, // manticore bool does not seem to support null, and we need three states for natural_boobs: yes, no and unknown
|
||||
penis_length: actor.penis_length || undefined,
|
||||
penis_girth: actor.penis_girth || undefined,
|
||||
stashed: actor.stashed || 0,
|
||||
scenes: actor.scenes || 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (docs.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return indexApi.bulk(docs.map((doc) => JSON.stringify(doc)).join('\n'));
|
||||
}
|
||||
|
||||
export async function syncActors(actorIds) {
|
||||
await knex.raw('REFRESH MATERIALIZED VIEW actors_meta;');
|
||||
|
||||
await syncManticoreActors(actorIds);
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import Router from 'express-promise-router';
|
||||
import omit from 'object.omit';
|
||||
|
||||
import {
|
||||
fetchActors,
|
||||
fetchActorsById,
|
||||
createActor,
|
||||
mergeActors,
|
||||
fetchActorRevisions,
|
||||
createActorRevision,
|
||||
reviewActorRevision,
|
||||
@@ -24,6 +27,7 @@ export function curateActorsQuery(query) {
|
||||
weight: query.weight?.split(',').map((weight) => Number(weight)),
|
||||
requireAvatar: query.avatar,
|
||||
stashId: Number(query.stashId) || null,
|
||||
isGlobal: !!query.global,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -169,6 +173,18 @@ export async function fetchActorsByIdGraphql(query, _req, _info) {
|
||||
return curatedActors[0];
|
||||
}
|
||||
|
||||
export async function createActorApi(req, res) {
|
||||
const actor = await createActor(req.body.actor, omit(req.body, ['actor']), req.user);
|
||||
|
||||
res.send({ actor });
|
||||
}
|
||||
|
||||
export async function mergeActorsApi(req, res) {
|
||||
const result = await mergeActors(Number(req.params.targetActorId), Number(req.params.sourceActorId), req.user);
|
||||
|
||||
res.send(result);
|
||||
}
|
||||
|
||||
async function fetchActorRevisionsApi(req, res) {
|
||||
const revisions = await fetchActorRevisions(Number(req.params.revisionId) || null, req.query, req.user);
|
||||
|
||||
@@ -190,6 +206,9 @@ async function reviewActorRevisionApi(req, res) {
|
||||
export const actorsRouter = Router();
|
||||
|
||||
actorsRouter.get('/api/actors', fetchActorsApi);
|
||||
actorsRouter.post('/api/actors', createActorApi);
|
||||
|
||||
actorsRouter.post('/api/actors/:targetActorId/merge/:sourceActorId', mergeActorsApi);
|
||||
|
||||
actorsRouter.get('/api/revisions/actors', fetchActorRevisionsApi);
|
||||
actorsRouter.get('/api/revisions/actors/:revisionId', fetchActorRevisionsApi);
|
||||
|
||||
@@ -2,14 +2,14 @@ export default function consentHandler(req, res, next) {
|
||||
const redirect = req.headers.referer && new URL(req.headers.referer).searchParams.get('redirect');
|
||||
|
||||
if (Object.hasOwn(req.query, 'lgbt')) {
|
||||
const lgbtFilters = (req.tagFilter || []).filter((tag) => !['gay', 'bisexual', 'transsexual'].includes(tag));
|
||||
const lgbtFilters = Array.from(new Set([...(req.tagFilter || []).filter((tag) => !['gay', 'bisexual', 'transsexual'].includes(tag)), 'extreme-insertion']));
|
||||
|
||||
req.tagFilter = lgbtFilters; // eslint-disable-line no-param-reassign
|
||||
res.cookie('tags', JSON.stringify(lgbtFilters));
|
||||
}
|
||||
|
||||
if (Object.hasOwn(req.query, 'straight')) {
|
||||
const straightFilters = Array.from(new Set([...(req.tagFilter || []), 'gay', 'bisexual', 'transsexual']));
|
||||
const straightFilters = Array.from(new Set([...(req.tagFilter || []), 'gay', 'bisexual', 'transsexual', 'extreme-insertion']));
|
||||
|
||||
req.tagFilter = straightFilters; // eslint-disable-line no-param-reassign
|
||||
res.cookie('tags', JSON.stringify(straightFilters));
|
||||
|
||||
@@ -16,6 +16,7 @@ import initRestrictionHandler from './restrictions.js';
|
||||
|
||||
import { scenesRouter } from './scenes.js';
|
||||
import { actorsRouter } from './actors.js';
|
||||
import { syncRouter } from './sync.js';
|
||||
|
||||
import { fetchMoviesApi } from './movies.js';
|
||||
import { fetchEntitiesApi } from './entities.js';
|
||||
@@ -122,11 +123,7 @@ export default async function initServer() {
|
||||
|
||||
router.use('/api/*', async (req, _res, next) => {
|
||||
if (req.headers['api-user']) {
|
||||
await verifyKey(req.headers['api-user'], req.headers['api-key'], req);
|
||||
|
||||
req.user = { // eslint-disable-line no-param-reassign
|
||||
id: Number(req.headers['api-user']),
|
||||
};
|
||||
req.user = await verifyKey(req.headers['api-user'], req.headers['api-key'], req);
|
||||
}
|
||||
|
||||
next();
|
||||
@@ -150,6 +147,7 @@ export default async function initServer() {
|
||||
router.use(alertsRouter);
|
||||
router.use(scenesRouter);
|
||||
router.use(actorsRouter);
|
||||
router.use(syncRouter);
|
||||
|
||||
// MOVIES
|
||||
router.get('/api/movies', fetchMoviesApi);
|
||||
|
||||
49
src/web/sync.js
Normal file
49
src/web/sync.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import Router from 'express-promise-router';
|
||||
|
||||
import {
|
||||
syncScenes,
|
||||
syncMovies,
|
||||
syncActors,
|
||||
syncStashes,
|
||||
} from '../sync.js';
|
||||
|
||||
import verifyAbility from '../../utils/verify-ability.js';
|
||||
|
||||
export const syncRouter = Router();
|
||||
|
||||
async function syncScenesApi(req, res) {
|
||||
verifyAbility(req.user, 'sync', null, { throwError: true });
|
||||
|
||||
await syncScenes(req.body.sceneIds);
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
async function syncMoviesApi(req, res) {
|
||||
verifyAbility(req.user, 'sync', null, { throwError: true });
|
||||
|
||||
await syncMovies(req.body.movieIds);
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
async function syncActorsApi(req, res) {
|
||||
verifyAbility(req.user, 'sync', null, { throwError: true });
|
||||
|
||||
await syncActors(req.body.actorIds);
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
async function syncStashesApi(req, res) {
|
||||
verifyAbility(req.user, 'sync', null, { throwError: true });
|
||||
|
||||
await syncStashes(req.body.stashIds);
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
syncRouter.post('/api/sync/scenes', syncScenesApi);
|
||||
syncRouter.post('/api/sync/movies', syncMoviesApi);
|
||||
syncRouter.post('/api/sync/actors', syncActorsApi);
|
||||
syncRouter.post('/api/sync/stashes', syncStashesApi);
|
||||
2
static
2
static
Submodule static updated: 258250e8c0...d77e9faeb9
43
tools/manticore-actors.js
Normal file
43
tools/manticore-actors.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import argv from '../src/argv.js';
|
||||
|
||||
import { knexOwner as knex } from '../src/knex.js';
|
||||
import { utilsApi } from '../src/manticore.js';
|
||||
import { syncManticoreActors } from '../src/sync.js';
|
||||
|
||||
async function init() {
|
||||
if (argv.update) {
|
||||
await utilsApi.sql('drop table if exists actors');
|
||||
await utilsApi.sql(`create table actors(
|
||||
id int,
|
||||
name text,
|
||||
slug string,
|
||||
entity_id int,
|
||||
gender string,
|
||||
date_of_birth timestamp,
|
||||
country string,
|
||||
has_avatar bool,
|
||||
mass int,
|
||||
height int,
|
||||
cup string,
|
||||
natural_boobs int,
|
||||
penis_length int,
|
||||
penis_girth int,
|
||||
stashed int,
|
||||
scenes int
|
||||
) min_prefix_len = '3'`);
|
||||
|
||||
console.log('Recreated actors table, syncing actors...');
|
||||
|
||||
const data = await syncManticoreActors();
|
||||
|
||||
console.log('data', data);
|
||||
|
||||
knex.destroy();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
knex.destroy();
|
||||
}
|
||||
|
||||
init();
|
||||
45
tools/manticore-movies.js
Normal file
45
tools/manticore-movies.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import argv from '../src/argv.js';
|
||||
import { knexOwner as knex } from '../src/knex.js';
|
||||
import { utilsApi } from '../src/manticore.js';
|
||||
import { syncManticoreMovies } from '../src/sync.js';
|
||||
|
||||
async function init() {
|
||||
if (argv.update) {
|
||||
await utilsApi.sql('drop table if exists movies');
|
||||
await utilsApi.sql(`create table movies (
|
||||
id int,
|
||||
title text,
|
||||
title_filtered text,
|
||||
channel_id int,
|
||||
channel_name text,
|
||||
channel_slug text,
|
||||
network_id int,
|
||||
network_name text,
|
||||
network_slug text,
|
||||
entity_ids multi,
|
||||
actor_ids multi,
|
||||
actors text,
|
||||
tag_ids multi,
|
||||
tags text,
|
||||
meta text,
|
||||
date timestamp,
|
||||
has_cover bool,
|
||||
created_at timestamp,
|
||||
effective_date timestamp,
|
||||
stashed int,
|
||||
stashed_scenes int,
|
||||
stashed_total int,
|
||||
dupe_index int
|
||||
)`);
|
||||
|
||||
console.log('Recreated movies tables, syncing movies...');
|
||||
|
||||
const data = await syncManticoreMovies();
|
||||
|
||||
console.log('data', data);
|
||||
}
|
||||
|
||||
knex.destroy();
|
||||
}
|
||||
|
||||
init();
|
||||
59
tools/manticore-scenes.js
Normal file
59
tools/manticore-scenes.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import argv from '../src/argv.js';
|
||||
import { knexOwner as knex } from '../src/knex.js';
|
||||
import { utilsApi } from '../src/manticore.js';
|
||||
import { syncManticoreScenes } from '../src/sync.js';
|
||||
|
||||
async function init() {
|
||||
if (argv.update) {
|
||||
await utilsApi.sql('drop table if exists scenes');
|
||||
await utilsApi.sql(`create table scenes (
|
||||
id int,
|
||||
title text,
|
||||
title_filtered text,
|
||||
entry_id text,
|
||||
shoot_id text,
|
||||
channel_id int,
|
||||
channel_name text,
|
||||
channel_slug text,
|
||||
network_id int,
|
||||
network_name text,
|
||||
network_slug text,
|
||||
studio_id int,
|
||||
studio_name text,
|
||||
studio_slug text,
|
||||
entity_ids multi,
|
||||
actor_ids multi,
|
||||
actors text,
|
||||
tag_ids multi,
|
||||
tags text,
|
||||
movie_ids multi,
|
||||
movies text,
|
||||
serie_ids multi,
|
||||
series text,
|
||||
meta text,
|
||||
date timestamp,
|
||||
fingerprints text,
|
||||
is_showcased bool,
|
||||
created_at timestamp,
|
||||
effective_date timestamp,
|
||||
stashed int,
|
||||
dupe_index int
|
||||
)`);
|
||||
|
||||
await utilsApi.sql('drop table if exists scenes_tags');
|
||||
await utilsApi.sql(`create table scenes_tags (
|
||||
id int,
|
||||
scene_id int,
|
||||
tag_id int,
|
||||
actor_id int
|
||||
)`);
|
||||
|
||||
console.log('Recreated scenes tables, syncing scenes...');
|
||||
|
||||
await syncManticoreScenes();
|
||||
}
|
||||
|
||||
knex.destroy();
|
||||
}
|
||||
|
||||
init();
|
||||
42
tools/manticore-stashes.js
Normal file
42
tools/manticore-stashes.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { knexOwner as knex } from '../src/knex.js';
|
||||
import { utilsApi } from '../src/manticore.js';
|
||||
import { syncStashes } from '../src/sync.js';
|
||||
|
||||
async function init() {
|
||||
await utilsApi.sql('drop table if exists scenes_stashed');
|
||||
|
||||
await utilsApi.sql(`create table if not exists scenes_stashed (
|
||||
scene_id int,
|
||||
stash_id int,
|
||||
user_id int,
|
||||
created_at timestamp
|
||||
)`);
|
||||
|
||||
await utilsApi.sql('drop table if exists movies_stashed');
|
||||
|
||||
await utilsApi.sql(`create table if not exists movies_stashed (
|
||||
movie_id int,
|
||||
stash_id int,
|
||||
user_id int,
|
||||
created_at timestamp
|
||||
)`);
|
||||
|
||||
await utilsApi.sql('drop table if exists actors_stashed');
|
||||
|
||||
await utilsApi.sql(`create table if not exists actors_stashed (
|
||||
actor_id int,
|
||||
stash_id int,
|
||||
user_id int,
|
||||
created_at timestamp
|
||||
)`);
|
||||
|
||||
console.log('Recreated stash tables, syncing stashes...');
|
||||
|
||||
await syncStashes('scene');
|
||||
await syncStashes('actor');
|
||||
await syncStashes('movie');
|
||||
|
||||
knex.destroy();
|
||||
}
|
||||
|
||||
init();
|
||||
7
utils/filter-title.js
Normal file
7
utils/filter-title.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function filterTitle(title, keys) {
|
||||
if (!title) {
|
||||
return title;
|
||||
}
|
||||
|
||||
return keys.reduce((accTitle, tag) => accTitle.replace(new RegExp(`\\b${tag.replace(/[^\w\s]+/g, '')}\\b`, 'gi'), ''), title).trim().replace(/\s{2,}/, ' ');
|
||||
}
|
||||
27
utils/verify-ability.js
Normal file
27
utils/verify-ability.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { HttpError } from '../src/errors.js';
|
||||
|
||||
function checkAbility(user, subject, action) {
|
||||
if (!user?.abilities) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (subject && action) {
|
||||
return user.abilities.some((ability) => ability.subject === subject && ability.action === action);
|
||||
}
|
||||
|
||||
if (subject) {
|
||||
return user.abilities.some((ability) => ability[subject] === true || (ability.subject === subject && !ability.action));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export default function verifyAbility(user, subject, action, options = {}) {
|
||||
const isAble = checkAbility(user, subject, action);
|
||||
|
||||
if (!isAble && options.throwError) {
|
||||
throw new HttpError(`Insufficient privileges for ${[subject, action].filter(Boolean).join()}`, 403);
|
||||
}
|
||||
|
||||
return isAble;
|
||||
}
|
||||
Reference in New Issue
Block a user