Added actor profile revisions.
This commit is contained in:
parent
b5bef49f73
commit
3967745fb3
|
@ -2,3 +2,6 @@
|
|||
path = static
|
||||
ignore = all
|
||||
url = git@unknown.name:DebaucheryLibrarian/traxxx-assets.git
|
||||
[submodule "common"]
|
||||
path = common
|
||||
url = git@unknown.name:DebaucheryLibrarian/traxxx-common.git
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 40011a62dae9da8deda71e9f8daf39665a8b7958
|
|
@ -136,13 +136,32 @@
|
|||
class="bio-item figure"
|
||||
>
|
||||
<dfn class="bio-label"><Icon icon="ruler" />Figure</dfn>
|
||||
<span class="bio-value">{{ actor.bust || '??' }}{{ actor.cup || '?' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}</span>
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-if="!actor.naturalBoobs || !actor.naturalButt"
|
||||
class="bio-item augmentations"
|
||||
>
|
||||
<dfn class="bio-label"><Icon icon="magic-wand2" />Augmentations</dfn>
|
||||
|
||||
<span class="bio-value">
|
||||
<Icon
|
||||
v-if="actor.naturalBoobs === false"
|
||||
:title="'Enhanced boobs'"
|
||||
icon="magic-wand2"
|
||||
class="enhanced"
|
||||
/>{{ actor.bust || '??' }}{{ actor.cup || '?' }}-{{ actor.waist || '??' }}-{{ actor.hip || '??' }}
|
||||
<div
|
||||
v-if="!actor.naturalBoobs"
|
||||
:title="[actor.boobsVolume, augmentationMap[actor.boobsPlacement] || actor.boobsPlacement, augmentationMap[actor.boobsImplant] || actor.boobsImplant].filter(Boolean).join(' ')"
|
||||
class="augmentations-section"
|
||||
>Boobs<template v-if="actor.boobsVolume || actor.boobsImplant">: </template>
|
||||
<template v-if="actor.boobsVolume">{{ actor.boobsVolume }}cc</template>
|
||||
<template v-if="actor.boobsImplant"> {{ augmentationMap[actor.boobsImplant] || actor.boobsImplant }}</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!actor.naturalButt"
|
||||
class="augmentations-section"
|
||||
>Butt<template v-if="actor.buttVolume || actor.buttImplant">: </template>
|
||||
<template v-if="actor.buttVolume">{{ actor.buttVolume }}cc</template>
|
||||
<template v-if="actor.buttImplant"> {{ augmentationMap[actor.buttImplant] || actor.buttImplant }}</template>
|
||||
</div>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
|
@ -240,6 +259,21 @@
|
|||
</li>
|
||||
|
||||
<li class="bio-item updated hideable">Updated {{ formatDate(actor.updatedAt, 'yyyy-MM-dd hh:mm') }}, ID: {{ actor.id }}</li>
|
||||
|
||||
<li class="bio-item actor-actions">
|
||||
<a
|
||||
v-if="user && user.role !== 'user'"
|
||||
:href="`/actor/edit/${actor.id}/${actor.slug}`"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>Edit bio</a>
|
||||
|
||||
<a
|
||||
:href="`/actor/revisions/${actor.id}/${actor.slug}`"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>Revisions</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="descriptions-container">
|
||||
|
@ -294,13 +328,16 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, inject } from 'vue';
|
||||
|
||||
import getPath from '#/src/get-path.js';
|
||||
import { formatDate } from '#/utils/format.js';
|
||||
|
||||
const expanded = ref(false);
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const user = pageContext.user;
|
||||
|
||||
const props = defineProps({
|
||||
actor: {
|
||||
type: Object,
|
||||
|
@ -326,6 +363,17 @@ const showExpand = [
|
|||
'weight',
|
||||
].some((attribute) => !!props.actor[attribute]);
|
||||
|
||||
const augmentationMap = {
|
||||
bbl: 'BBL',
|
||||
fat: 'fat transfer',
|
||||
lift: 'direct lift',
|
||||
lipo: 'lipo without BBL',
|
||||
filler: 'filler',
|
||||
mms: 'MMS',
|
||||
over: 'over-muscle',
|
||||
under: 'under-muscle',
|
||||
};
|
||||
|
||||
const descriptions = Object.values(Object.fromEntries(props.actor.profiles
|
||||
.filter((profile) => !!profile.description)
|
||||
.map((profile) => [profile.descriptionHash, {
|
||||
|
@ -519,6 +567,11 @@ const descriptions = Object.values(Object.fromEntries(props.actor.profiles
|
|||
content: ',\00a0';
|
||||
}
|
||||
|
||||
.augmentations .bio-value {
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.updated {
|
||||
color: var(--highlight-weak-20);
|
||||
font-size: .8rem;
|
||||
|
@ -638,6 +691,16 @@ const descriptions = Object.values(Object.fromEntries(props.actor.profiles
|
|||
}
|
||||
}
|
||||
|
||||
.actor-actions {
|
||||
justify-content: flex-start;
|
||||
gap: 1rem;
|
||||
font-weight: normal;
|
||||
|
||||
.link {
|
||||
color: var(--highlight-strong-20);
|
||||
}
|
||||
}
|
||||
|
||||
@media(--big) {
|
||||
.descriptions-container {
|
||||
display: none;
|
||||
|
|
|
@ -4,10 +4,18 @@
|
|||
<ul class="nav-items nolist">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
href="/admin/revisions"
|
||||
href="/admin/revisions/scenes"
|
||||
class="nav-link nolink"
|
||||
:class="{ active: pageContext.routeParams.section === 'revisions' }"
|
||||
>Revisions</a>
|
||||
:class="{ active: pageContext.routeParams.section === 'revisions' && pageContext.routeParams.domain === 'scenes' }"
|
||||
>Scene Revisions</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a
|
||||
href="/admin/revisions/actors"
|
||||
class="nav-link nolink"
|
||||
:class="{ active: pageContext.routeParams.section === 'revisions' && pageContext.routeParams.domain === 'actors' }"
|
||||
>Actor Revisions</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
@ -22,8 +30,6 @@
|
|||
import { inject } from 'vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
|
||||
// console.log(pageContext);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -36,7 +42,14 @@ const pageContext = inject('pageContext');
|
|||
|
||||
.nav {
|
||||
display: flex;
|
||||
padding: 1rem 1rem 0 1rem;
|
||||
padding: 1rem 1rem .75rem 1rem;
|
||||
border-bottom: solid 1px var(--shadow-weak-30);
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
|
|
|
@ -27,16 +27,16 @@
|
|||
<template v-if="context === 'admin' || expanded.has(rev.id)">
|
||||
<div class="rev-header">
|
||||
<a
|
||||
:href="`/scene/${rev.sceneId}`"
|
||||
:href="`/${domain.slice(0, -1)}/${rev.sceneId || rev.actorId}/${rev.base.slug}`"
|
||||
target="_blank"
|
||||
class="rev-link rev-scene nolink noshrink"
|
||||
>{{ rev.sceneId }}@{{ rev.hash.slice(0, 6) }}</a>
|
||||
>{{ rev.sceneId || rev.actorId }}@{{ rev.hash.slice(0, 6) }}</a>
|
||||
|
||||
<a
|
||||
:href="`/scene/${rev.sceneId}`"
|
||||
:href="`/${domain.slice(0, -1)}/${rev.sceneId || rev.actorId}/${rev.base.slug}`"
|
||||
target="_blank"
|
||||
class="rev-link rev-title nolink ellipsis"
|
||||
>{{ rev.base.title }}</a>
|
||||
>{{ rev.base.title || rev.base.name }}</a>
|
||||
|
||||
<div class="rev-details noshrink">
|
||||
<a
|
||||
|
@ -219,6 +219,7 @@ defineProps({
|
|||
|
||||
const pageContext = inject('pageContext');
|
||||
const revisions = ref(pageContext.pageProps.revisions);
|
||||
const domain = pageContext.routeParams.domain;
|
||||
|
||||
const actors = ref(pageContext.pageProps.actors);
|
||||
const tags = ref(pageContext.pageProps.tags);
|
||||
|
@ -241,6 +242,8 @@ const mappedKeys = {
|
|||
|
||||
const dateKeys = [
|
||||
'date',
|
||||
'dateOfBirth',
|
||||
'dateOfDeath',
|
||||
'productionDate',
|
||||
'createdAt',
|
||||
];
|
||||
|
@ -307,7 +310,7 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
|
|||
}));
|
||||
|
||||
async function reloadRevisions() {
|
||||
const updatedRevisions = await get('/revisions', {
|
||||
const updatedRevisions = await get(`/revisions/${domain}`, {
|
||||
isFinalized: showReviewed.value ? undefined : false,
|
||||
limit: 50,
|
||||
});
|
||||
|
@ -322,12 +325,12 @@ async function reviewRevision(revision, isApproved) {
|
|||
reviewedRevisions.value.add(revision.id);
|
||||
|
||||
try {
|
||||
await post(`/revisions/${revision.id}/reviews`, {
|
||||
await post(`/revisions/${domain}/${revision.id}/reviews`, {
|
||||
isApproved,
|
||||
feedback: feedbacks.value[revision.id],
|
||||
});
|
||||
|
||||
const updatedRevision = await get(`/revisions/${revision.id}`, {
|
||||
const updatedRevision = await get(`/revisions/${domain}/${revision.id}`, {
|
||||
revisionId: revision.id,
|
||||
});
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"mathjs": "^12.2.1",
|
||||
"merkle-json": "^2.6.0",
|
||||
"mitt": "^3.0.1",
|
||||
"moment": "^2.30.1",
|
||||
"mysql": "^2.18.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"object.omit": "^3.0.0",
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"mathjs": "^12.2.1",
|
||||
"merkle-json": "^2.6.0",
|
||||
"mitt": "^3.0.1",
|
||||
"moment": "^2.30.1",
|
||||
"mysql": "^2.18.1",
|
||||
"nanoid": "^5.0.4",
|
||||
"object.omit": "^3.0.0",
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
||||
import { redirect, render } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
||||
|
||||
import { fetchActorsById } from '#/src/actors.js';
|
||||
import { fetchScenes } from '#/src/scenes.js';
|
||||
import { fetchMovies } from '#/src/movies.js';
|
||||
import { curateScenesQuery } from '#/src/web/scenes.js';
|
||||
import { curateMoviesQuery } from '#/src/web/movies.js';
|
||||
import { fetchCountries } from '#/src/countries.js';
|
||||
|
||||
async function fetchReleases(pageContext) {
|
||||
if (pageContext.routeParams.domain === 'movies') {
|
||||
|
@ -33,9 +34,16 @@ async function fetchReleases(pageContext) {
|
|||
}
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const [[actor], actorReleases] = await Promise.all([
|
||||
const isEditing = pageContext._pageId === '/pages/actors/@actorId/edit';
|
||||
|
||||
if (isEditing && !pageContext.user) {
|
||||
throw redirect(`/login?r=${encodeURIComponent(pageContext.urlOriginal)}`);
|
||||
}
|
||||
|
||||
const [[actor], actorReleases, countries] = await Promise.all([
|
||||
fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user),
|
||||
fetchReleases(pageContext),
|
||||
isEditing && fetchCountries(),
|
||||
]);
|
||||
|
||||
if (!actor) {
|
||||
|
@ -44,9 +52,12 @@ export async function onBeforeRender(pageContext) {
|
|||
|
||||
return {
|
||||
pageContext: {
|
||||
title: actor.name,
|
||||
title: isEditing
|
||||
? `Editing '${actor.name}'`
|
||||
: actor.name,
|
||||
pageProps: {
|
||||
actor,
|
||||
countries,
|
||||
...actorReleases,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { match } from 'path-to-regexp';
|
||||
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
|
||||
|
||||
const path = '/actor/:actorId/:actorSlug?/:domain(scenes|movies)?/:scope?/:page?';
|
||||
const path = '/actor/:actorId(\\d+)/:actorSlug?/:domain(scenes|movies)?/:scope?/:page?';
|
||||
const urlMatch = match(path, { decode: decodeURIComponent });
|
||||
|
||||
export default (pageContext) => {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
export default '/actor/edit/@actorId/*';
|
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<div class="content">
|
||||
<div class="revs-header">
|
||||
<h2 class="heading">Revisions for "{{ scene.title }}"</h2>
|
||||
|
||||
<div class="revs-actions">
|
||||
<a
|
||||
:href="`/scene/edit/${scene.id}/${scene.slug}`"
|
||||
class="link"
|
||||
>Edit scene</a>
|
||||
|
||||
<a
|
||||
:href="`/scene/${scene.id}/${scene.slug}`"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>Go to scene</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Revisions context="scene" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
|
||||
import Revisions from '#/components/edit/revisions.vue';
|
||||
|
||||
const pageContext = inject('pageContext');
|
||||
const scene = pageContext.pageProps.scene;
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content {
|
||||
padding: 1rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.revs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.heading {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.revs-actions {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media(--compact) {
|
||||
.revs-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,24 @@
|
|||
import { fetchActorsById } from '#/src/actors.js';
|
||||
import { fetchSceneRevisions } from '#/src/scenes.js';
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
const [actor] = await fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user);
|
||||
|
||||
const {
|
||||
revisions,
|
||||
} = await fetchSceneRevisions(null, {
|
||||
sceneId: actor.id,
|
||||
isFinalized: true,
|
||||
limit: 100,
|
||||
}, pageContext.user);
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
title: `Revisions for '${actor.name}'`,
|
||||
pageProps: {
|
||||
actor,
|
||||
revisions,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export default '/actor/revisions/@actorId/*';
|
|
@ -1 +0,0 @@
|
|||
export default '/admin/@section/*';
|
|
@ -0,0 +1,24 @@
|
|||
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
|
||||
import { fetchActorRevisions } from '#/src/actors.js';
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
if (!pageContext.user || pageContext.user.role === 'user') {
|
||||
throw render(404);
|
||||
}
|
||||
|
||||
const {
|
||||
revisions,
|
||||
} = await fetchActorRevisions(null, {
|
||||
isFinalized: false,
|
||||
limit: 50,
|
||||
}, pageContext.user);
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
title: pageContext.routeParams.section,
|
||||
pageProps: {
|
||||
revisions,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import { match } from 'path-to-regexp';
|
||||
|
||||
const path = '/admin/:section/:domain(actors)';
|
||||
const urlMatch = match(path, { decode: decodeURIComponent });
|
||||
|
||||
export default (pageContext) => {
|
||||
const matched = urlMatch(pageContext.urlPathname);
|
||||
|
||||
if (matched) {
|
||||
return {
|
||||
routeParams: {
|
||||
section: matched.params.section,
|
||||
domain: matched.params.domain,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
import { match } from 'path-to-regexp';
|
||||
|
||||
const path = '/admin/:section/:domain(scenes)';
|
||||
const urlMatch = match(path, { decode: decodeURIComponent });
|
||||
|
||||
export default (pageContext) => {
|
||||
const matched = urlMatch(pageContext.urlPathname);
|
||||
|
||||
if (matched) {
|
||||
return {
|
||||
routeParams: {
|
||||
section: matched.params.section,
|
||||
domain: matched.params.domain,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
|
@ -36,8 +36,11 @@
|
|||
>
|
||||
<img
|
||||
v-if="network.hasLogo"
|
||||
:src="network.type === 'network' || network.isIndependent || !network.parent ? `/logos/${network.slug}/thumbs/network.png` : `/logos/${network.parent.slug}/thumbs/${network.slug}.png`"
|
||||
:src="network.type === 'network' || network.isIndependent || !network.parent
|
||||
? `/logos/${network.slug}/thumbs/network.png`
|
||||
: `/logos/${network.parent.slug}/thumbs/${network.slug}.png`"
|
||||
:alt="network.name"
|
||||
loading="lazy"
|
||||
class="logo"
|
||||
>
|
||||
|
||||
|
@ -58,7 +61,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue';
|
||||
import { ref, inject, onMounted } from 'vue';
|
||||
|
||||
import navigate from '#/src/navigate.js';
|
||||
|
||||
|
@ -120,6 +123,12 @@ const sections = [
|
|||
async function search() {
|
||||
navigate('/channels', { q: query.value || undefined }, { redirect: true });
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('load', (event) => {
|
||||
console.log(event);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -194,6 +203,9 @@ async function search() {
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
@media(--small-30) {
|
||||
|
|
|
@ -676,7 +676,9 @@ function copySummary() {
|
|||
|
||||
.templates {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: .25rem 0;
|
||||
margin-top: .5rem;
|
||||
|
||||
.icon {
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
|
||||
<li v-if="user.role !== 'user'">
|
||||
<a
|
||||
href="/admin/revisions"
|
||||
href="/admin/revisions/scenes"
|
||||
class="link"
|
||||
>Go to revisions admin</a>
|
||||
</li>
|
||||
|
@ -50,7 +50,7 @@
|
|||
@submit.prevent
|
||||
>
|
||||
<div class="editor-header">
|
||||
<h2 class="heading ellipsis">Edit scene #{{ scene.id }}</h2>
|
||||
<h2 class="heading ellipsis">Edit scene #{{ scene.id }} - {{ scene.title }}</h2>
|
||||
|
||||
<a
|
||||
:href="`/scene/${scene.id}/${scene.slug}`"
|
||||
|
|
382
src/actors.js
382
src/actors.js
|
@ -1,7 +1,11 @@
|
|||
import config from 'config';
|
||||
import { differenceInYears } from 'date-fns';
|
||||
import { unit } from 'mathjs';
|
||||
import { MerkleJson } from 'merkle-json';
|
||||
import moment from 'moment';
|
||||
import omit from 'object.omit';
|
||||
|
||||
import initLogger from './logger.js';
|
||||
import { knexOwner as knex, knexManticore } from './knex.js';
|
||||
import { utilsApi } from './manticore.js';
|
||||
import { HttpError } from './errors.js';
|
||||
|
@ -11,6 +15,11 @@ import { curateMedia } from './media.js';
|
|||
import { curateStash } from './stashes.js';
|
||||
import escape from '../utils/escape-manticore.js';
|
||||
import slugify from '../utils/slugify.js';
|
||||
import { curateRevision } from './revisions.js';
|
||||
import { interpolateProfiles } from '../common/actors.mjs'; // eslint-disable-line import/namespace
|
||||
|
||||
const logger = initLogger();
|
||||
const mj = new MerkleJson();
|
||||
|
||||
export function curateActor(actor, context = {}) {
|
||||
return {
|
||||
|
@ -22,11 +31,22 @@ export function curateActor(actor, context = {}) {
|
|||
dateOfBirth: actor.date_of_birth,
|
||||
ageFromBirth: actor.date_of_birth && differenceInYears(Date.now(), actor.date_of_birth),
|
||||
ageThen: context.sceneDate && actor.date_of_birth && differenceInYears(context.sceneDate, actor.date_of_birth),
|
||||
dateOfDeath: actor.date_of_death,
|
||||
bust: actor.bust,
|
||||
cup: actor.cup,
|
||||
waist: actor.waist,
|
||||
hip: actor.hip,
|
||||
naturalBoobs: actor.natural_boobs,
|
||||
boobsVolume: actor.boobs_volume,
|
||||
boobsImplant: actor.boobs_implant,
|
||||
boobsPlacement: actor.boobs_placement,
|
||||
boobsSurgeon: actor.boobs_surgeon,
|
||||
naturalButt: actor.natural_butt,
|
||||
buttVolume: actor.butt_volume,
|
||||
buttImplant: actor.butt_implant,
|
||||
penisLength: actor.penis_length,
|
||||
penisGirth: actor.penis_girth,
|
||||
isCircumcised: actor.is_circumcised,
|
||||
height: actor.height && {
|
||||
metric: actor.height,
|
||||
imperial: unit(actor.height, 'cm').splitUnit(['ft', 'in']).map((value) => Math.round(value.toNumber())),
|
||||
|
@ -139,6 +159,8 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
|||
: [],
|
||||
]);
|
||||
|
||||
console.log(actors);
|
||||
|
||||
if (options.order) {
|
||||
return actors.map((actorEntry) => curateActor(actorEntry, {
|
||||
stashes: stashes.filter((stash) => stash.actor_id === actorEntry.id),
|
||||
|
@ -161,6 +183,8 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
|||
});
|
||||
}).filter(Boolean);
|
||||
|
||||
console.log(curatedActors);
|
||||
|
||||
return curatedActors;
|
||||
}
|
||||
|
||||
|
@ -377,3 +401,361 @@ export async function fetchActors(filters, rawOptions, reqUser) {
|
|||
limit: options.limit,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchActorRevisions(revisionId, filters = {}, reqUser) {
|
||||
const limit = filters.limit || 50;
|
||||
const page = filters.page || 1;
|
||||
|
||||
const revisions = await knex('actors_revisions')
|
||||
.select(
|
||||
'actors_revisions.*',
|
||||
'users.username as username',
|
||||
'reviewers.username as reviewer_username',
|
||||
)
|
||||
.leftJoin('users', 'users.id', 'actors_revisions.user_id')
|
||||
.leftJoin('users as reviewers', 'reviewers.id', 'actors_revisions.reviewed_by')
|
||||
.modify((builder) => {
|
||||
if (!['admin', 'editor'].includes(reqUser?.role) && !filters.userId && !filters.actorId) {
|
||||
builder.where('user_id', reqUser.id);
|
||||
}
|
||||
|
||||
if (filters.userId) {
|
||||
if (!['admin', 'editor'].includes(reqUser?.role) && filters.userId !== reqUser.id) {
|
||||
throw new HttpError('You are not permitted to view revisions from other users.', 403);
|
||||
}
|
||||
|
||||
builder.where('actors_revisions.user_id', filters.userId);
|
||||
}
|
||||
|
||||
if (revisionId) {
|
||||
builder.where('actors_revisions.id', revisionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (filters.actorId) {
|
||||
builder.where('actors_revisions.actor_id', filters.actorId);
|
||||
}
|
||||
|
||||
if (filters.isFinalized === false) {
|
||||
builder.whereNull('approved');
|
||||
}
|
||||
|
||||
if (filters.isFinalized === true) {
|
||||
builder.whereNotNull('approved');
|
||||
}
|
||||
})
|
||||
.orderBy('created_at', 'desc')
|
||||
.limit(limit)
|
||||
.offset((page - 1) * limit);
|
||||
|
||||
const curatedRevisions = revisions.map((revision) => curateRevision(revision));
|
||||
|
||||
return {
|
||||
revisions: curatedRevisions,
|
||||
revision: revisionId && curatedRevisions.find((revision) => revision.id === revisionId),
|
||||
};
|
||||
}
|
||||
|
||||
const keyMap = {
|
||||
avatar: 'avatar_media_id',
|
||||
dateOfBirth: 'date_of_birth',
|
||||
dateOfDeath: 'date_of_death',
|
||||
originCountry: 'birth_country_alpha2',
|
||||
originState: 'birth_state',
|
||||
originCity: 'birth_city',
|
||||
residenceCountry: 'residence_country_alpha2',
|
||||
residenceState: 'residence_state',
|
||||
residenceCity: 'residence_city',
|
||||
hairColor: 'hair_color',
|
||||
naturalBoobs: 'natural_boobs',
|
||||
boobsVolume: 'boobs_volume',
|
||||
boobsImplant: 'boobs_implant',
|
||||
boobsPlacement: 'boobs_placement',
|
||||
boobsSurgeon: 'boobs_surgeon',
|
||||
naturalButt: 'natural_butt',
|
||||
buttVolume: 'butt_volume',
|
||||
buttImplant: 'butt_implant',
|
||||
penisLength: 'penis_length',
|
||||
penisGirth: 'penis_girth',
|
||||
isCircumcised: 'circumcised',
|
||||
hasTattoos: 'has_tattoos',
|
||||
hasPiercings: 'has_piercings',
|
||||
};
|
||||
|
||||
async function applyActorValueDelta(profileId, delta, trx) {
|
||||
console.log('value delta', profileId, delta, keyMap[delta.key], delta.value);
|
||||
|
||||
return knex('actors_profiles')
|
||||
.where('id', profileId)
|
||||
.update(keyMap[delta.key] || delta.key, delta.value)
|
||||
.transacting(trx);
|
||||
}
|
||||
|
||||
async function applyActorDirectDelta(actorId, delta, trx) {
|
||||
console.log('value delta', delta);
|
||||
|
||||
return knex('actors')
|
||||
.where('id', actorId)
|
||||
.update(keyMap[delta.key] || delta.key, delta.value)
|
||||
.modify((builder) => {
|
||||
if (delta.key === 'name') {
|
||||
builder.update('slug', slugify(delta.value));
|
||||
}
|
||||
})
|
||||
.transacting(trx);
|
||||
}
|
||||
|
||||
async function fetchMainProfile(actorId, wasCreated = false) {
|
||||
const profileEntry = await knex('actors_profiles')
|
||||
.where('actor_id', actorId)
|
||||
.where('entity_id', null)
|
||||
.first();
|
||||
|
||||
if (profileEntry) {
|
||||
return profileEntry;
|
||||
}
|
||||
|
||||
if (wasCreated) {
|
||||
throw new HttpError('Failed to find or create main profile', 404);
|
||||
}
|
||||
|
||||
await knex('actors_profiles').insert({
|
||||
actor_id: actorId,
|
||||
entity_id: null,
|
||||
});
|
||||
|
||||
return fetchMainProfile(actorId, true);
|
||||
}
|
||||
|
||||
/*
|
||||
async function applyMainProfile(actorId) {
|
||||
const [actorEntry, mainProfile] = await Promise.all([
|
||||
knex('actors')
|
||||
.where('id', actorId)
|
||||
.first(),
|
||||
fetchMainProfile(actorId),
|
||||
]);
|
||||
|
||||
if (!actorEntry) {
|
||||
throw new HttpError('No actor profile found to apply main profile to', 404);
|
||||
}
|
||||
|
||||
const preservedKeys = ['id'];
|
||||
|
||||
// we start iterating from the actor entry so we don't include keys that are not yet supported by the actors table
|
||||
const mergedProfile = Object.fromEntries(Object.entries(actorEntry)
|
||||
.filter(([key]) => Object.hasOwn(mainProfile, key))
|
||||
.map(([key, value]) => [key, mainProfile[key] === null || preservedKeys.includes(key)
|
||||
? value
|
||||
: mainProfile[key]]));
|
||||
|
||||
await knex('actors')
|
||||
.where('id', actorId)
|
||||
.update(mergedProfile);
|
||||
}
|
||||
*/
|
||||
|
||||
async function applyActorRevision(revisionIds, reqUser) {
|
||||
const revisions = await knex('actors_revisions')
|
||||
.whereIn('id', revisionIds)
|
||||
.whereNull('applied_at'); // should not re-apply revision that was already applied
|
||||
|
||||
await revisions.reduce(async (chain, revision) => {
|
||||
await chain;
|
||||
|
||||
const mainProfile = await fetchMainProfile(revision.actor_id);
|
||||
|
||||
await knex.transaction(async (trx) => {
|
||||
await Promise.all(revision.deltas.map(async (delta) => {
|
||||
if ([
|
||||
'gender',
|
||||
'avatar',
|
||||
'dateOfBirth',
|
||||
'dateOfDeath',
|
||||
'originCountry',
|
||||
'originState',
|
||||
'originCity',
|
||||
'residenceCountry',
|
||||
'residenceState',
|
||||
'residenceCity',
|
||||
'height',
|
||||
'weight',
|
||||
'bust',
|
||||
'cup',
|
||||
'waist',
|
||||
'hip',
|
||||
'naturalBoobs',
|
||||
'boobsVolume',
|
||||
'boobsImplant',
|
||||
'boobsPlacement',
|
||||
'boobsSurgeon',
|
||||
'naturalButt',
|
||||
'buttVolume',
|
||||
'buttImplant',
|
||||
'penisLength',
|
||||
'penisGirth',
|
||||
'isCircumcised',
|
||||
'hairColor',
|
||||
'eyes',
|
||||
'hasTattoos',
|
||||
'tattoos',
|
||||
'hasPiercings',
|
||||
'piercings',
|
||||
].includes(delta.key)) {
|
||||
return applyActorValueDelta(mainProfile.id, delta, trx);
|
||||
}
|
||||
|
||||
if (delta.key === 'name' && reqUser.role === 'admin') {
|
||||
return applyActorDirectDelta(revision.actor_id, delta, trx);
|
||||
}
|
||||
|
||||
return null;
|
||||
}));
|
||||
|
||||
await knex('actors_revisions')
|
||||
.transacting(trx)
|
||||
.where('id', revision.id)
|
||||
.update('applied_at', knex.fn.now());
|
||||
|
||||
// await trx.commit();
|
||||
}).catch(async (error) => {
|
||||
logger.error(`Failed to apply revision ${revision.id} on actor ${revision.actor_id}: ${error.message}`);
|
||||
});
|
||||
}, Promise.resolve());
|
||||
|
||||
const actorIds = Array.from(new Set(revisions.map((revision) => revision.actor_id)));
|
||||
|
||||
await interpolateProfiles(actorIds, {
|
||||
knex,
|
||||
logger,
|
||||
moment,
|
||||
slugify,
|
||||
omit,
|
||||
});
|
||||
}
|
||||
|
||||
export async function reviewActorRevision(revisionId, isApproved, { feedback }, reqUser) {
|
||||
if (!reqUser || reqUser.role === 'user') {
|
||||
throw new HttpError('You are not permitted to approve revisions', 403);
|
||||
}
|
||||
|
||||
if (typeof isApproved !== 'boolean') {
|
||||
throw new HttpError('You must either approve or reject the revision', 400);
|
||||
}
|
||||
|
||||
const updated = await knex('actors_revisions')
|
||||
.where('id', revisionId)
|
||||
.whereNull('approved') // don't rerun reviewed revision, must be forked into new revision instead
|
||||
.whereNull('applied_at')
|
||||
.update({
|
||||
approved: isApproved,
|
||||
reviewed_at: knex.fn.now(),
|
||||
reviewed_by: reqUser.id,
|
||||
feedback,
|
||||
});
|
||||
|
||||
if (updated === 0) {
|
||||
throw new HttpError('This revision was already reviewed', 409);
|
||||
}
|
||||
|
||||
if (isApproved) {
|
||||
await applyActorRevision([revisionId], reqUser);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createActorRevision(actorId, { edits, comment, apply }, reqUser) {
|
||||
const [
|
||||
[actor],
|
||||
openRevisions,
|
||||
] = await Promise.all([
|
||||
fetchActorsById([actorId], {
|
||||
reqUser,
|
||||
includeAssets: true,
|
||||
includePartOf: true,
|
||||
}),
|
||||
knex('actors_revisions')
|
||||
.where('user_id', reqUser.id)
|
||||
.whereNull('approved'),
|
||||
]);
|
||||
|
||||
if (!actor) {
|
||||
throw new HttpError(`No actor with ID ${actorId} found to update`, 404);
|
||||
}
|
||||
|
||||
if (openRevisions.length >= config.revisions.unapprovedLimit) {
|
||||
throw new HttpError(`You have ${config.revisions.unapprovedLimit} unapproved revisions, please wait for approval before submitting another revision.`, 429);
|
||||
}
|
||||
|
||||
const baseActor = Object.fromEntries(Object.entries(actor).map(([key, values]) => {
|
||||
if ([
|
||||
'scenes',
|
||||
'likes',
|
||||
'stashes',
|
||||
'profiles',
|
||||
].includes(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* avatar should return id
|
||||
if (values?.hash) {
|
||||
return [key, values.hash];
|
||||
}
|
||||
*/
|
||||
|
||||
if (values?.id) {
|
||||
return [key, values.id];
|
||||
}
|
||||
|
||||
if (values?.metric) {
|
||||
return [key, values.metric];
|
||||
}
|
||||
|
||||
if (Array.isArray(values)) {
|
||||
return [key, values.map((value) => value?.hash || value?.id || value)];
|
||||
}
|
||||
|
||||
return [key, values];
|
||||
}).filter(Boolean));
|
||||
|
||||
const deltas = Object.entries(edits).map(([key, value]) => {
|
||||
if (baseActor[key] === value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const valueSet = new Set(value);
|
||||
const baseSet = new Set(baseActor[key]);
|
||||
|
||||
if (valueSet.size === baseSet.size && baseActor[key].every((id) => valueSet.has(id))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { key, value: Array.from(valueSet) };
|
||||
}
|
||||
|
||||
return { key, value: value || null };
|
||||
}).filter(Boolean);
|
||||
|
||||
if (deltas.length === 0) {
|
||||
throw new HttpError('No effective changes provided', 400);
|
||||
}
|
||||
|
||||
const [revisionEntry] = await knex('actors_revisions')
|
||||
.insert({
|
||||
user_id: reqUser.id,
|
||||
actor_id: actor.id,
|
||||
base: JSON.stringify(baseActor),
|
||||
deltas: JSON.stringify(deltas),
|
||||
hash: mj.hash({
|
||||
base: baseActor,
|
||||
deltas,
|
||||
}),
|
||||
comment,
|
||||
})
|
||||
.returning('id');
|
||||
|
||||
if (['admin', 'editor'].includes(reqUser.role) && apply) {
|
||||
// don't keep the editor waiting for the revision to apply
|
||||
reviewActorRevision(revisionEntry.id, true, {}, reqUser);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,3 +28,10 @@ export async function fetchCountriesByAlpha2(alpha2s, options = {}) {
|
|||
|
||||
return entries.map((countryEntry) => curateCountry(countryEntry));
|
||||
}
|
||||
|
||||
export async function fetchCountries() {
|
||||
const entries = await knex('countries')
|
||||
.orderBy(knex.raw('coalesce(alias, name)'));
|
||||
|
||||
return entries.map((countryEntry) => curateCountry(countryEntry));
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export function curateMedia(media, context = {}) {
|
|||
width: media.width,
|
||||
height: media.height,
|
||||
index: media.index,
|
||||
sharpness: media.sharpness,
|
||||
credit: media.credit,
|
||||
type: context.type || null,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
export function curateRevision(revision) {
|
||||
return {
|
||||
id: revision.id,
|
||||
sceneId: revision.scene_id,
|
||||
actorId: revision.actor_id,
|
||||
base: revision.base,
|
||||
deltas: revision.deltas,
|
||||
hash: revision.hash,
|
||||
comment: revision.comment,
|
||||
user: revision.user_id && {
|
||||
id: revision.user_id,
|
||||
username: revision.username,
|
||||
},
|
||||
review: typeof revision.approved === 'boolean' ? {
|
||||
isApproved: revision.approved,
|
||||
userId: revision.reviewed_by,
|
||||
username: revision.reviewer_username,
|
||||
reviewedAt: revision.reviewed_at,
|
||||
} : null,
|
||||
appliedAt: revision.applied_at,
|
||||
failed: revision.failed,
|
||||
createdAt: revision.created_at,
|
||||
};
|
||||
}
|
|
@ -14,6 +14,7 @@ import { curateMedia } from './media.js';
|
|||
import escape from '../utils/escape-manticore.js';
|
||||
import promiseProps from '../utils/promise-props.js';
|
||||
import initLogger from './logger.js';
|
||||
import { curateRevision } from './revisions.js';
|
||||
|
||||
const logger = initLogger();
|
||||
const mj = new MerkleJson();
|
||||
|
@ -58,8 +59,6 @@ function curateScene(rawScene, assets) {
|
|||
return null;
|
||||
}
|
||||
|
||||
console.log(assets.chapters);
|
||||
|
||||
const curatedScene = {
|
||||
id: rawScene.id,
|
||||
title: rawScene.title,
|
||||
|
@ -634,30 +633,6 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
|
|||
};
|
||||
}
|
||||
|
||||
function curateRevision(revision) {
|
||||
return {
|
||||
id: revision.id,
|
||||
sceneId: revision.scene_id,
|
||||
base: revision.base,
|
||||
deltas: revision.deltas,
|
||||
hash: revision.hash,
|
||||
comment: revision.comment,
|
||||
user: revision.user_id && {
|
||||
id: revision.user_id,
|
||||
username: revision.username,
|
||||
},
|
||||
review: typeof revision.approved === 'boolean' ? {
|
||||
isApproved: revision.approved,
|
||||
userId: revision.reviewed_by,
|
||||
username: revision.reviewer_username,
|
||||
reviewedAt: revision.reviewed_at,
|
||||
} : null,
|
||||
appliedAt: revision.applied_at,
|
||||
failed: revision.failed,
|
||||
createdAt: revision.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchSceneRevisions(revisionId, filters = {}, reqUser) {
|
||||
const limit = filters.limit || 50;
|
||||
const page = filters.page || 1;
|
||||
|
@ -727,6 +702,9 @@ export async function fetchSceneRevisions(revisionId, filters = {}, reqUser) {
|
|||
|
||||
const keyMap = {
|
||||
productionDate: 'production_date',
|
||||
productionLocation: 'production_location',
|
||||
productionCity: 'production_city',
|
||||
productionState: 'production_state',
|
||||
};
|
||||
|
||||
async function applySceneValueDelta(sceneId, delta, trx) {
|
||||
|
@ -952,6 +930,7 @@ export async function createSceneRevision(sceneId, { edits, comment, apply }, re
|
|||
.returning('id');
|
||||
|
||||
if (['admin', 'editor'].includes(reqUser.role) && apply) {
|
||||
await reviewSceneRevision(revisionEntry.id, true, {}, reqUser);
|
||||
// don't keep the editor waiting for the revision to apply
|
||||
reviewSceneRevision(revisionEntry.id, true, {}, reqUser);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import Router from 'express-promise-router';
|
||||
|
||||
import {
|
||||
fetchActors,
|
||||
fetchActorsById,
|
||||
fetchActorRevisions,
|
||||
createActorRevision,
|
||||
reviewActorRevision,
|
||||
} from '../actors.js';
|
||||
|
||||
export function curateActorsQuery(query) {
|
||||
|
@ -131,11 +136,36 @@ export async function fetchActorsByIdGraphql(query, _req, _info) {
|
|||
const actors = await fetchActorsById([].concat(query.id, query.ids).filter(Boolean));
|
||||
const curatedActors = actors.map((actor) => curateGraphqlActor(actor));
|
||||
|
||||
console.log(actors);
|
||||
|
||||
if (query.ids) {
|
||||
return curatedActors;
|
||||
}
|
||||
|
||||
return curatedActors[0];
|
||||
}
|
||||
|
||||
async function fetchActorRevisionsApi(req, res) {
|
||||
const revisions = await fetchActorRevisions(Number(req.params.revisionId) || null, req.query, req.user);
|
||||
|
||||
res.send(revisions);
|
||||
}
|
||||
|
||||
async function createActorRevisionApi(req, res) {
|
||||
await createActorRevision(Number(req.body.actorId), req.body, req.user);
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
async function reviewActorRevisionApi(req, res) {
|
||||
await reviewActorRevision(Number(req.params.revisionId), req.body.isApproved, req.body, req.user);
|
||||
|
||||
res.status(204).send();
|
||||
}
|
||||
|
||||
export const actorsRouter = Router();
|
||||
|
||||
actorsRouter.get('/api/actors', fetchActorsApi);
|
||||
|
||||
actorsRouter.get('/api/revisions/actors', fetchActorRevisionsApi);
|
||||
actorsRouter.get('/api/revisions/actors/:revisionId', fetchActorRevisionsApi);
|
||||
actorsRouter.post('/api/revisions/actors', createActorRevisionApi);
|
||||
actorsRouter.post('/api/revisions/actors/:revisionId/reviews', reviewActorRevisionApi);
|
||||
|
|
|
@ -252,7 +252,7 @@ export const scenesRouter = Router();
|
|||
scenesRouter.get('/api/scenes', fetchScenesApi);
|
||||
scenesRouter.get('/api/scenes/:sceneId', fetchSceneApi);
|
||||
|
||||
scenesRouter.get('/api/revisions', fetchSceneRevisionsApi);
|
||||
scenesRouter.get('/api/revisions/:revisionId', fetchSceneRevisionsApi);
|
||||
scenesRouter.post('/api/revisions', createSceneRevisionApi);
|
||||
scenesRouter.post('/api/revisions/:revisionId/reviews', reviewSceneRevisionApi);
|
||||
scenesRouter.get('/api/revisions/scenes', fetchSceneRevisionsApi);
|
||||
scenesRouter.get('/api/revisions/scenes/:revisionId', fetchSceneRevisionsApi);
|
||||
scenesRouter.post('/api/revisions/scenes', createSceneRevisionApi);
|
||||
scenesRouter.post('/api/revisions/scenes/:revisionId/reviews', reviewSceneRevisionApi);
|
||||
|
|
|
@ -14,8 +14,8 @@ import errorHandler from './error.js';
|
|||
import consentHandler from './consent.js';
|
||||
|
||||
import { scenesRouter } from './scenes.js';
|
||||
import { actorsRouter } from './actors.js';
|
||||
|
||||
import { fetchActorsApi } from './actors.js';
|
||||
import { fetchMoviesApi } from './movies.js';
|
||||
import { fetchEntitiesApi } from './entities.js';
|
||||
import { fetchTagsApi } from './tags.js';
|
||||
|
@ -144,9 +144,7 @@ export default async function initServer() {
|
|||
router.use(userRouter);
|
||||
router.use(stashesRouter);
|
||||
router.use(scenesRouter);
|
||||
|
||||
// ACTORS
|
||||
router.get('/api/actors', fetchActorsApi);
|
||||
router.use(actorsRouter);
|
||||
|
||||
// MOVIES
|
||||
router.get('/api/movies', fetchMoviesApi);
|
||||
|
|
2
static
2
static
|
@ -1 +1 @@
|
|||
Subproject commit 0f7fc7c9f4ba7454bb92482eaffd93b345ea9834
|
||||
Subproject commit 083dfa7120cedc41757fe0da08d073faa6898e9f
|
Loading…
Reference in New Issue