Compare commits
31 Commits
62dcaba875
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 885fe7c9e9 | |||
| 930cc52373 | |||
| 4636a213b3 | |||
| 469954f613 | |||
| 5bdcd65d42 | |||
| cb91cd4cc7 | |||
| e04ddaed9b | |||
| 605da5e46c | |||
| 360e8ece85 | |||
| 1543bf9d03 | |||
| 287932d9d7 | |||
| 7c4de31c12 | |||
| 497c6150f7 | |||
| 514f51f111 | |||
| 244dc4fff6 | |||
| 4b39f787c9 | |||
| fb92b9c973 | |||
| 181358db7d | |||
| 3790567d44 | |||
| b3af993236 | |||
| 7ae2bb7635 | |||
| a75f0662ad | |||
| ffd68d5037 | |||
| adf9e2334c | |||
| 4125811017 | |||
| 4e8356b072 | |||
| bad116cdc0 | |||
| 9eac6871a4 | |||
| 3b694689f3 | |||
| 1604ddaa78 | |||
| 16181923b6 |
@@ -74,7 +74,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="label">
|
<span class="label">
|
||||||
<span class="name ellipsis">{{ actor.name }}</span>
|
<span
|
||||||
|
class="name ellipsis"
|
||||||
|
:style="{ 'font-size': `${Math.max(0.9 + Math.min((17 - actor.name.length), 0) * 0.06, 0.65)}rem` }"
|
||||||
|
>{{ actor.name }}</span>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-if="actor.entity"
|
v-if="actor.entity"
|
||||||
@@ -82,6 +85,13 @@
|
|||||||
:src="`/logos/${actor.entity.slug}/favicon_dark.png`"
|
:src="`/logos/${actor.entity.slug}/favicon_dark.png`"
|
||||||
class="favicon"
|
class="favicon"
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
v-if="actor.alias && actor.alias.name !== actor.name"
|
||||||
|
v-tooltip="`Credited as '${actor.alias.name}'`"
|
||||||
|
icon="at-sign"
|
||||||
|
class="alias"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -148,6 +158,7 @@ const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id ===
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
|
height: 1.75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -161,7 +172,7 @@ const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id ===
|
|||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
padding: .35rem .25rem .35rem .5rem;
|
padding: 0 .25rem 0 .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.favicon {
|
.favicon {
|
||||||
@@ -258,4 +269,11 @@ const favorited = ref(props.actor.stashes.some((actorStash) => actorStash.id ===
|
|||||||
height: .75rem;
|
height: .75rem;
|
||||||
margin-left: .25rem;
|
margin-left: .25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.alias {
|
||||||
|
height: 100%;
|
||||||
|
fill: var(--glass-weak-20);
|
||||||
|
padding: 0 .25rem;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -19,10 +19,16 @@
|
|||||||
>{{ avatar.sharpness.toFixed(2) }}</span>
|
>{{ avatar.sharpness.toFixed(2) }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
:title="`Added ${format(avatar.createdAt, 'yyyy-MM-dd')}, may not reflect photo age`"
|
||||||
|
class="avatar-date"
|
||||||
|
>{{ format(avatar.createdAt, '\'\'yy-MM') }}</span>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
:href="getPath(avatar)"
|
:href="getPath(avatar)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="avatar-zoom"
|
class="avatar-zoom"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
icon="search"
|
icon="search"
|
||||||
@@ -32,6 +38,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { format } from 'date-fns';
|
||||||
import getPath from '#/src/get-path.js';
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
@@ -89,7 +96,8 @@ defineProps({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.avatar-meta,
|
.avatar-meta,
|
||||||
.avatar-credit {
|
.avatar-credit,
|
||||||
|
.avatar-date {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -112,4 +120,10 @@ defineProps({
|
|||||||
bottom: .75rem;
|
bottom: .75rem;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar-date {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -146,7 +146,6 @@ const { pageProps } = inject('pageContext');
|
|||||||
const {
|
const {
|
||||||
tag: pageTag,
|
tag: pageTag,
|
||||||
actor: pageActor,
|
actor: pageActor,
|
||||||
stash: pageStash,
|
|
||||||
} = pageProps;
|
} = pageProps;
|
||||||
|
|
||||||
const search = ref('');
|
const search = ref('');
|
||||||
@@ -177,7 +176,7 @@ const priorityTags = [
|
|||||||
'lesbian',
|
'lesbian',
|
||||||
];
|
];
|
||||||
|
|
||||||
const isActorTagsAvailable = computed(() => props.actorTags && (props.filters.actors.length > 0 || pageActor) && !pageStash);
|
const isActorTagsAvailable = computed(() => props.actorTags && (props.filters.actors.length > 0 || pageActor));
|
||||||
|
|
||||||
const groupedTags = computed(() => {
|
const groupedTags = computed(() => {
|
||||||
// can't show actor tags inside stash, because both require a join, and manticore currently only supports one
|
// can't show actor tags inside stash, because both require a join, and manticore currently only supports one
|
||||||
@@ -225,6 +224,8 @@ const groupedTags = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function toggleTag(tag, combine) {
|
function toggleTag(tag, combine) {
|
||||||
|
emit('update', 'onlyActorTags', showActorTags.value, false);
|
||||||
|
|
||||||
if (props.filters.tags.includes(tag.slug)) {
|
if (props.filters.tags.includes(tag.slug)) {
|
||||||
emit('update', 'tags', props.filters.tags.filter((tagId) => tagId !== tag.slug));
|
emit('update', 'tags', props.filters.tags.filter((tagId) => tagId !== tag.slug));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ const filters = ref({
|
|||||||
search: urlParsed.search.q,
|
search: urlParsed.search.q,
|
||||||
years: urlParsed.search.years?.split(',').filter(Boolean).map(Number) || [],
|
years: urlParsed.search.years?.split(',').filter(Boolean).map(Number) || [],
|
||||||
tags: urlParsed.search.tags?.split(',').filter(Boolean) || [],
|
tags: urlParsed.search.tags?.split(',').filter(Boolean) || [],
|
||||||
|
onlyActorTags: Object.hasOwn(urlParsed.search, 'at'),
|
||||||
entity: queryEntity,
|
entity: queryEntity,
|
||||||
actors: queryActors,
|
actors: queryActors,
|
||||||
});
|
});
|
||||||
@@ -346,6 +347,7 @@ async function search(options = {}) {
|
|||||||
years: filters.value.years.join(',') || undefined,
|
years: filters.value.years.join(',') || undefined,
|
||||||
actors: filters.value.actors.map((filterActor) => getActorIdentifier(filterActor)).join(',') || undefined, // don't include page actor ID in query, already a parameter
|
actors: filters.value.actors.map((filterActor) => getActorIdentifier(filterActor)).join(',') || undefined, // don't include page actor ID in query, already a parameter
|
||||||
tags: filters.value.tags.join(',') || undefined,
|
tags: filters.value.tags.join(',') || undefined,
|
||||||
|
at: (filters.value.tags.length > 0 && filters.value.onlyActorTags) || undefined,
|
||||||
// e: filters.value.entity?.type === 'network' ? `_${filters.value.entity.slug}` : (filters.value.entity?.slug || undefined),
|
// e: filters.value.entity?.type === 'network' ? `_${filters.value.entity.slug}` : (filters.value.entity?.slug || undefined),
|
||||||
e: filters.value.entity ? `${entityPrefixes[filters.value.entity.type]}${filters.value.entity.slug}` : undefined,
|
e: filters.value.entity ? `${entityPrefixes[filters.value.entity.type]}${filters.value.entity.slug}` : undefined,
|
||||||
}, { redirect: false });
|
}, { redirect: false });
|
||||||
@@ -355,6 +357,7 @@ async function search(options = {}) {
|
|||||||
years: filters.value.years.filter(Boolean).join(','), // if we're on an actor page, that actor ID needs to be included
|
years: filters.value.years.filter(Boolean).join(','), // if we're on an actor page, that actor ID needs to be included
|
||||||
actors: [pageActor, ...filters.value.actors].filter(Boolean).map((filterActor) => getActorIdentifier(filterActor)).join(','), // if we're on an actor page, that actor ID needs to be included
|
actors: [pageActor, ...filters.value.actors].filter(Boolean).map((filterActor) => getActorIdentifier(filterActor)).join(','), // if we're on an actor page, that actor ID needs to be included
|
||||||
tags: [pageTag?.slug, ...filters.value.tags].filter(Boolean).join(','),
|
tags: [pageTag?.slug, ...filters.value.tags].filter(Boolean).join(','),
|
||||||
|
at: !!filters.value.onlyActorTags,
|
||||||
stashId: pageStash?.id,
|
stashId: pageStash?.id,
|
||||||
e: entitySlug,
|
e: entitySlug,
|
||||||
scope: scope.value,
|
scope: scope.value,
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "traxxx-web",
|
"name": "traxxx-web",
|
||||||
"version": "0.50.13",
|
"version": "0.51.4",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"version": "0.50.13",
|
"version": "0.51.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@brillout/json-serializer": "^0.5.8",
|
"@brillout/json-serializer": "^0.5.8",
|
||||||
"@dicebear/collection": "^7.0.5",
|
"@dicebear/collection": "^7.0.5",
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
"overrides": {
|
"overrides": {
|
||||||
"vite": "$vite"
|
"vite": "$vite"
|
||||||
},
|
},
|
||||||
"version": "0.50.13",
|
"version": "0.51.4",
|
||||||
"imports": {
|
"imports": {
|
||||||
"#/*": "./*.js"
|
"#/*": "./*.js"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -466,18 +466,18 @@ const fields = computed(() => [
|
|||||||
type: 'augmentation',
|
type: 'augmentation',
|
||||||
note: 'Provide explicit evidence, such as social media posts, visible scarring, or before/after. Avoid "it\'s obvious".',
|
note: 'Provide explicit evidence, such as social media posts, visible scarring, or before/after. Avoid "it\'s obvious".',
|
||||||
value: {
|
value: {
|
||||||
naturalBoobs: actor.value?.naturalBoobs || null,
|
naturalBoobs: actor.value?.naturalBoobs ?? null,
|
||||||
boobsVolume: actor.value?.boobsVolume || null,
|
boobsVolume: actor.value?.boobsVolume || null,
|
||||||
boobsImplant: actor.value?.boobsImplant || null,
|
boobsImplant: actor.value?.boobsImplant || null,
|
||||||
boobsPlacement: actor.value?.boobsPlacement || null,
|
boobsPlacement: actor.value?.boobsPlacement || null,
|
||||||
boobsIncision: actor.value?.boobsIncision || null,
|
boobsIncision: actor.value?.boobsIncision || null,
|
||||||
boobsSurgeon: actor.value?.boobsSurgeon || null,
|
boobsSurgeon: actor.value?.boobsSurgeon || null,
|
||||||
naturalButt: actor.value?.naturalButt || null,
|
naturalButt: actor.value?.naturalButt ?? null,
|
||||||
buttVolume: actor.value?.buttVolume || null,
|
buttVolume: actor.value?.buttVolume || null,
|
||||||
buttImplant: actor.value?.buttImplant || null,
|
buttImplant: actor.value?.buttImplant || null,
|
||||||
naturalLips: actor.value?.naturalLips || null,
|
naturalLips: actor.value?.naturalLips ?? null,
|
||||||
lipsVolume: actor.value?.lipsVolume || null,
|
lipsVolume: actor.value?.lipsVolume || null,
|
||||||
naturalLabia: actor.value?.naturalLabia || null,
|
naturalLabia: actor.value?.naturalLabia ?? null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,7 +45,13 @@
|
|||||||
:key="`actor-${actor.id}`"
|
:key="`actor-${actor.id}`"
|
||||||
class="actor"
|
class="actor"
|
||||||
>
|
>
|
||||||
<td class="actor-id ellipsis">{{ actor.id }}</td>
|
<td class="actor-id ellipsis">
|
||||||
|
<a
|
||||||
|
:href="`/actor/${actor.id}/${actor.slug}`"
|
||||||
|
target="_blank"
|
||||||
|
class="nolink"
|
||||||
|
>{{ actor.id }}</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td
|
<td
|
||||||
v-tooltip="actor.entity?.name || 'Global'"
|
v-tooltip="actor.entity?.name || 'Global'"
|
||||||
|
|||||||
@@ -140,30 +140,41 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tags">
|
<ul class="tags nolist">
|
||||||
<div
|
<li
|
||||||
v-for="actorTags in tags"
|
v-for="tag in tags"
|
||||||
:key="`tags-${actorTags.actor?.slug || 'scene'}`"
|
:key="`tag-${tag.id}`"
|
||||||
class="tags-section"
|
class="tag"
|
||||||
|
:class="{ 'has-actors': tag.actors.length > 0 }"
|
||||||
>
|
>
|
||||||
<ul class="nolist">
|
<Link
|
||||||
<li
|
:href="`/tag/${tag.slug}`"
|
||||||
v-if="actorTags.actor"
|
class="tag-name nolink"
|
||||||
class="tags-actor"
|
>{{ tag.name }}</Link>
|
||||||
>{{ actorTags.actor.name }}:</li>
|
|
||||||
|
|
||||||
<li
|
<span
|
||||||
v-for="tag in actorTags.tags"
|
v-for="tagActor in tag.actors"
|
||||||
:key="`tag-${tag.id}`"
|
:key="`tagactor-${tagActor.id}`"
|
||||||
|
v-tooltip="{
|
||||||
|
content: `Performed by ${tagActor.name}`,
|
||||||
|
triggers: ['hover', 'click'],
|
||||||
|
}"
|
||||||
|
class="tag-frame"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="tagActor.avatar"
|
||||||
|
class="tag-avatar"
|
||||||
|
:src="getPath(tagActor.avatar, 'thumbnail')"
|
||||||
>
|
>
|
||||||
<Link
|
|
||||||
:href="`/tag/${tag.slug}`"
|
<Icon
|
||||||
class="tag nolink"
|
v-else
|
||||||
>{{ tag.name }}</Link>
|
icon="star-full"
|
||||||
</li>
|
class="tag-star"
|
||||||
</ul>
|
/>
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="scene.movies.length > 0 || scene.series.length > 0"
|
v-if="scene.movies.length > 0 || scene.series.length > 0"
|
||||||
@@ -424,6 +435,7 @@ import Cookies from 'js-cookie';
|
|||||||
import { formatDate, formatDuration } from '#/utils/format.js';
|
import { formatDate, formatDuration } from '#/utils/format.js';
|
||||||
import events from '#/src/events.js';
|
import events from '#/src/events.js';
|
||||||
import processSummaryTemplate from '#/utils/process-summary-template.js';
|
import processSummaryTemplate from '#/utils/process-summary-template.js';
|
||||||
|
import getPath from '#/src/get-path.js';
|
||||||
|
|
||||||
import Banner from '#/components/media/banner.vue';
|
import Banner from '#/components/media/banner.vue';
|
||||||
import ActorTile from '#/components/actors/tile.vue';
|
import ActorTile from '#/components/actors/tile.vue';
|
||||||
@@ -450,16 +462,44 @@ const {
|
|||||||
|
|
||||||
const { scene } = pageProps;
|
const { scene } = pageProps;
|
||||||
|
|
||||||
const tags = [
|
/*
|
||||||
{
|
const tags = scene.tags.map((tag) => ({
|
||||||
tags: scene.tags.filter((tag) => tag.actorId === null),
|
...tag,
|
||||||
actor: null,
|
actor: scene.actors.find((actor) => actor.id === tag.actorId) || null,
|
||||||
},
|
}));
|
||||||
...scene.actors.map((actor) => ({
|
*/
|
||||||
actor,
|
|
||||||
tags: scene.tags.filter((tag) => tag.actorId === actor.id),
|
const actorsById = Object.fromEntries(scene.actors.map((actor) => [actor.id, actor]));
|
||||||
})),
|
|
||||||
].filter((actorTags) => actorTags.tags.length > 0);
|
const tags = Array.from(scene.tags
|
||||||
|
.reduce((acc, tag) => {
|
||||||
|
const accTag = acc.get(tag.id);
|
||||||
|
|
||||||
|
if (accTag && tag.actorId) {
|
||||||
|
return acc.set(tag.id, {
|
||||||
|
...tag,
|
||||||
|
actors: [...accTag.actors, actorsById[tag.actorId]].toSorted((actorA, actorB) => actorA.name.localeCompare(actorB.name)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accTag) {
|
||||||
|
// shouldn't happen, but account for it
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag.actorId) {
|
||||||
|
return acc.set(tag.id, {
|
||||||
|
...tag,
|
||||||
|
actors: [actorsById[tag.actorId]],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc.set(tag.id, {
|
||||||
|
...tag,
|
||||||
|
actors: [],
|
||||||
|
});
|
||||||
|
}, new Map())
|
||||||
|
.values());
|
||||||
|
|
||||||
const showSummaryDialog = ref(false);
|
const showSummaryDialog = ref(false);
|
||||||
|
|
||||||
@@ -664,7 +704,7 @@ function copySummary() {
|
|||||||
.tags {
|
.tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: .25rem 1rem;
|
gap: .35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-section {
|
.tags-section {
|
||||||
@@ -684,25 +724,60 @@ function copySummary() {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
|
||||||
.actor {
|
.actor {
|
||||||
width: 10rem;
|
width: 9rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
padding: .5rem;
|
display: flex;
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
margin: 0 .25rem .25rem 0;
|
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
box-shadow: 0 0 3px var(--shadow-weak-30);
|
box-shadow: 0 0 3px var(--shadow-weak-30);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 0 3px var(--shadow-weak-20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-name {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: .5rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
box-shadow: 0 0 3px var(--shadow-weak-20);
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-frame {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
box-shadow: inset 0 0 3px var(--shadow-weak-20);
|
||||||
|
pointer-events: none; /* so it doesn't block hover/click on the image */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-avatar {
|
||||||
|
height: 350%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-star {
|
||||||
|
height: 100%;
|
||||||
|
fill: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
.movies,
|
.movies,
|
||||||
.series {
|
.series {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h2 class="title">{{ tag.name }}</h2>
|
<h2
|
||||||
|
:title="`${tag.name} (#${tag.id})`"
|
||||||
|
class="title"
|
||||||
|
>{{ tag.name }}</h2>
|
||||||
|
|
||||||
<Heart
|
<Heart
|
||||||
domain="tags"
|
domain="tags"
|
||||||
|
|||||||
110
src/actors.js
110
src/actors.js
@@ -62,11 +62,22 @@ const keyMap = {
|
|||||||
const socialsOrder = ['onlyfans', 'fansly', 'twitter', 'instagram', 'loyalfans', 'manyvids', 'pornhub', 'linktree', null];
|
const socialsOrder = ['onlyfans', 'fansly', 'twitter', 'instagram', 'loyalfans', 'manyvids', 'pornhub', 'linktree', null];
|
||||||
|
|
||||||
export function curateActor(actor, context = {}) {
|
export function curateActor(actor, context = {}) {
|
||||||
|
if (!actor) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: actor.id,
|
id: actor.id,
|
||||||
slug: actor.slug,
|
slug: actor.slug,
|
||||||
name: actor.name,
|
name: actor.name,
|
||||||
aliases: actor.aliases || [],
|
aliases: actor.aliases || [], // used for profile pages
|
||||||
|
alias: actor.alias
|
||||||
|
? {
|
||||||
|
id: actor.alias.id,
|
||||||
|
slug: actor.alias.slug,
|
||||||
|
name: actor.alias.name,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
gender: actor.gender,
|
gender: actor.gender,
|
||||||
age: actor.age,
|
age: actor.age,
|
||||||
ethnicity: actor.ethnicity,
|
ethnicity: actor.ethnicity,
|
||||||
@@ -234,7 +245,7 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
|||||||
knex.raw('COALESCE(residence_countries.alias, residence_countries.name) as residence_country_name'),
|
knex.raw('COALESCE(residence_countries.alias, residence_countries.name) as residence_country_name'),
|
||||||
knex.raw('row_to_json(entities) as entity'),
|
knex.raw('row_to_json(entities) as entity'),
|
||||||
knex.raw('row_to_json(sfw_media) as sfw_avatar'),
|
knex.raw('row_to_json(sfw_media) as sfw_avatar'),
|
||||||
knex.raw('json_agg(aliases) as aliases'),
|
knex.raw('json_agg(aliases) filter (where aliases.id is not null) as aliases'),
|
||||||
)
|
)
|
||||||
.leftJoin('actors_meta', 'actors_meta.actor_id', 'actors.id')
|
.leftJoin('actors_meta', 'actors_meta.actor_id', 'actors.id')
|
||||||
.leftJoin('actors as aliases', 'aliases.alias_for', 'actors.id')
|
.leftJoin('actors as aliases', 'aliases.alias_for', 'actors.id')
|
||||||
@@ -598,13 +609,18 @@ export async function mergeActors(targetActorId, sourceActorIds, reqUser) {
|
|||||||
|
|
||||||
const trx = await knex.transaction();
|
const trx = await knex.transaction();
|
||||||
|
|
||||||
let mergedProfiles;
|
let mergedProfiles = [];
|
||||||
let mergedScenes;
|
let mergedSceneActors = [];
|
||||||
|
let existingSceneActors = [];
|
||||||
|
let duplicateSourceActors = [];
|
||||||
|
let mergedActorStashes = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [existingProfiles] = await Promise.all([
|
const [existingProfiles, sourceProfiles] = await Promise.all([
|
||||||
trx('actors_profiles')
|
trx('actors_profiles')
|
||||||
.where('actor_id', targetActorId),
|
.where('actor_id', targetActorId),
|
||||||
|
trx('actors_profiles')
|
||||||
|
.whereIn('actor_id', sourceActorIds),
|
||||||
trx('actors')
|
trx('actors')
|
||||||
.update('alias_for', targetActorId)
|
.update('alias_for', targetActorId)
|
||||||
.whereIn('id', sourceActorIds)
|
.whereIn('id', sourceActorIds)
|
||||||
@@ -613,19 +629,50 @@ export async function mergeActors(targetActorId, sourceActorIds, reqUser) {
|
|||||||
trx('actors_avatars')
|
trx('actors_avatars')
|
||||||
.update('actor_id', targetActorId)
|
.update('actor_id', targetActorId)
|
||||||
.whereIn('actor_id', sourceActorIds),
|
.whereIn('actor_id', sourceActorIds),
|
||||||
trx('stashes_actors')
|
|
||||||
.update('actor_id', targetActorId)
|
|
||||||
.whereIn('actor_id', sourceActorIds)
|
|
||||||
.returning('id'),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// multiple source actors may provide profiles for the same entity, but we can only assign one to the target actor; prefer the newest
|
||||||
|
const newestSourceProfileMap = Object.fromEntries(sourceProfiles
|
||||||
|
.toSorted((profileA, profileB) => profileA.updated_at - profileB.updated_at)
|
||||||
|
.map((profile) => [profile.entity_id, profile.id]));
|
||||||
|
|
||||||
|
const duplicateSourceProfiles = sourceProfiles.filter((profile) => newestSourceProfileMap[profile.entity_id] && newestSourceProfileMap[profile.entity_id] !== profile.id);
|
||||||
|
|
||||||
|
// assign source actor profiles to target actor, unless a profile for that entity is already present
|
||||||
mergedProfiles = await trx('actors_profiles')
|
mergedProfiles = await trx('actors_profiles')
|
||||||
.update('actor_id', targetActorId)
|
.update('actor_id', targetActorId)
|
||||||
.whereIn('actor_id', sourceActorIds)
|
.whereIn('actor_id', sourceActorIds)
|
||||||
.whereNotIn('entity_id', existingProfiles.map((profile) => profile.entity_id))
|
.whereNotIn('entity_id', existingProfiles.map((profile) => profile.entity_id))
|
||||||
|
.whereNotIn('id', duplicateSourceProfiles.map((profile) => profile.id))
|
||||||
.returning('id');
|
.returning('id');
|
||||||
|
|
||||||
mergedScenes = await trx('releases_actors')
|
// find releases that have more than one source actor assigned
|
||||||
|
duplicateSourceActors = await trx('releases_actors')
|
||||||
|
.select('release_id', knex.raw('array_agg(actor_id) as actor_ids'))
|
||||||
|
.whereIn('actor_id', sourceActorIds)
|
||||||
|
.groupBy('release_id')
|
||||||
|
.having(trx.raw('COUNT(DISTINCT actor_id) > 1'));
|
||||||
|
|
||||||
|
if (duplicateSourceActors.length > 0) {
|
||||||
|
// some scenes have multiple source actors assigned, which will cause a conflict after merging; we will need to remove all but one
|
||||||
|
await trx('releases_actors')
|
||||||
|
.whereIn('release_id', duplicateSourceActors.map((sceneActor) => sceneActor.release_id))
|
||||||
|
.whereIn('actor_id', duplicateSourceActors.flatMap((sceneActor) => sceneActor.actor_ids.slice(1)))
|
||||||
|
.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// find scenes that already have target actor assigned
|
||||||
|
existingSceneActors = await trx('releases_actors')
|
||||||
|
.where('actor_id', targetActorId);
|
||||||
|
|
||||||
|
// delete release source actors for scenes that already have the target actor assigned
|
||||||
|
await trx('releases_actors')
|
||||||
|
.whereIn('release_id', existingSceneActors.map((sceneActor) => sceneActor.release_id))
|
||||||
|
.whereIn('actor_id', sourceActorIds)
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
// alias release source actors to target actors
|
||||||
|
mergedSceneActors = await trx('releases_actors')
|
||||||
.update({
|
.update({
|
||||||
actor_id: targetActorId,
|
actor_id: targetActorId,
|
||||||
alias_id: knex.raw('actor_id'),
|
alias_id: knex.raw('actor_id'),
|
||||||
@@ -633,6 +680,40 @@ export async function mergeActors(targetActorId, sourceActorIds, reqUser) {
|
|||||||
.whereIn('actor_id', sourceActorIds)
|
.whereIn('actor_id', sourceActorIds)
|
||||||
.returning('release_id');
|
.returning('release_id');
|
||||||
|
|
||||||
|
const [targetActorStashes, sourceActorStashes] = await Promise.all([
|
||||||
|
trx('stashes_actors')
|
||||||
|
.where('actor_id', targetActorId),
|
||||||
|
trx('stashes_actors')
|
||||||
|
.whereIn('actor_id', sourceActorIds),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// remove source actors from stashes that already contain target actor
|
||||||
|
await trx('stashes_actors')
|
||||||
|
.whereIn('stash_id', targetActorStashes.map((stash) => stash.stash_id))
|
||||||
|
.whereIn('actor_id', sourceActorIds)
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
// find stashes that have more than one source actor assigned
|
||||||
|
const duplicateStashActors = await trx('stashes_actors')
|
||||||
|
.select('stash_id', knex.raw('array_agg(actor_id order by created_at) as actor_ids'))
|
||||||
|
.whereIn('actor_id', sourceActorStashes.map((actorStash) => actorStash.actor_id))
|
||||||
|
.groupBy('stash_id')
|
||||||
|
.having(trx.raw('COUNT(DISTINCT actor_id) > 1'));
|
||||||
|
|
||||||
|
if (duplicateStashActors.length > 0) {
|
||||||
|
// some stashes have multiple source actors assigned, which will cause a conflict after merging; we will need to remove all but one
|
||||||
|
await trx('stashes_actors')
|
||||||
|
.whereIn('stash_id', duplicateStashActors.map((actorStash) => actorStash.stash_id))
|
||||||
|
.whereIn('actor_id', duplicateStashActors.flatMap((actorStash) => actorStash.actor_ids.slice(1)))
|
||||||
|
.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// we update an existing entry instead of creating a new one, so the original stash date is preserved
|
||||||
|
mergedActorStashes = await trx('stashes_actors')
|
||||||
|
.update('actor_id', targetActorId)
|
||||||
|
.whereIn('actor_id', sourceActorIds)
|
||||||
|
.returning('stash_id');
|
||||||
|
|
||||||
await trx.commit();
|
await trx.commit();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await trx.rollback();
|
await trx.rollback();
|
||||||
@@ -649,14 +730,19 @@ export async function mergeActors(targetActorId, sourceActorIds, reqUser) {
|
|||||||
}, { refreshView: false });
|
}, { refreshView: false });
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
syncScenes(mergedScenes.map((scene) => scene.release_id)),
|
syncScenes([
|
||||||
|
...mergedSceneActors.map((sceneActor) => sceneActor.release_id),
|
||||||
|
...existingSceneActors.map((sceneActor) => sceneActor.release_id),
|
||||||
|
...duplicateSourceActors.map((sceneActor) => sceneActor.release_id),
|
||||||
|
]),
|
||||||
syncActors([targetActorId, ...sourceActorIds]),
|
syncActors([targetActorId, ...sourceActorIds]),
|
||||||
syncStashes('actor', [targetActorId, ...sourceActorIds]),
|
syncStashes('actor', [targetActorId, ...sourceActorIds]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scenes: mergedScenes.length,
|
scenes: mergedSceneActors.length,
|
||||||
profiles: mergedProfiles.length,
|
profiles: mergedProfiles.length,
|
||||||
|
stashes: mergedActorStashes.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import initServer from './web/server.js';
|
import initServer from './web/server.js';
|
||||||
import { initCaches } from './cache.js';
|
import { initCaches } from './cache.js';
|
||||||
|
import { initSyncCron } from './sync.js';
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await initCaches();
|
await initCaches();
|
||||||
|
|
||||||
initServer();
|
initServer();
|
||||||
|
initSyncCron();
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
@@ -32,5 +32,6 @@ export function curateMedia(media, context = {}) {
|
|||||||
type: context.type || null,
|
type: context.type || null,
|
||||||
sfw: curateMedia(media.sfw_media),
|
sfw: curateMedia(media.sfw_media),
|
||||||
isRestricted: context.isRestricted,
|
isRestricted: context.isRestricted,
|
||||||
|
createdAt: media.created_at,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,16 +209,19 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
|||||||
'actors.*',
|
'actors.*',
|
||||||
knex.raw('row_to_json(avatars) as avatar'),
|
knex.raw('row_to_json(avatars) as avatar'),
|
||||||
knex.raw('row_to_json(sfw_media) as sfw_avatar'),
|
knex.raw('row_to_json(sfw_media) as sfw_avatar'),
|
||||||
|
knex.raw('row_to_json(aliases) as alias'),
|
||||||
|
knex.raw('case when aliases.id is not null then json_build_object(\'id\', aliases.id, \'name\', aliases.name, \'slug\', aliases.slug) end as alias'),
|
||||||
'countries.name as birth_country_name',
|
'countries.name as birth_country_name',
|
||||||
'countries.alias as birth_country_alias',
|
'countries.alias as birth_country_alias',
|
||||||
'releases_actors.release_id',
|
'releases_actors.release_id',
|
||||||
)
|
)
|
||||||
.leftJoin('actors', 'actors.id', 'releases_actors.actor_id')
|
.leftJoin('actors', 'actors.id', 'releases_actors.actor_id')
|
||||||
|
.leftJoin('actors as aliases', 'aliases.id', 'releases_actors.alias_id')
|
||||||
.leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id')
|
.leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id')
|
||||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'avatars.sfw_media_id')
|
.leftJoin('media as sfw_media', 'sfw_media.id', 'avatars.sfw_media_id')
|
||||||
.leftJoin('countries', 'countries.alpha2', 'actors.birth_country_alpha2')
|
.leftJoin('countries', 'countries.alpha2', 'actors.birth_country_alpha2')
|
||||||
.whereIn('release_id', sceneIds)
|
.whereIn('release_id', sceneIds)
|
||||||
.groupBy('actors.id', 'releases_actors.release_id', 'avatars.id', 'countries.name', 'countries.alias', 'sfw_media.id'),
|
.groupBy('actors.id', 'aliases.id', 'releases_actors.release_id', 'avatars.id', 'countries.name', 'countries.alias', 'sfw_media.id'),
|
||||||
directors: knex('releases_directors')
|
directors: knex('releases_directors')
|
||||||
.whereIn('release_id', sceneIds)
|
.whereIn('release_id', sceneIds)
|
||||||
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
|
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
|
||||||
@@ -415,6 +418,20 @@ function curateFacet(results, field) {
|
|||||||
|| [];
|
|| [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const packN = 100_000;
|
||||||
|
|
||||||
|
function mergePackedTags(tags) {
|
||||||
|
const mergedCounts = tags.reduce((merged, tag) => {
|
||||||
|
const tagId = tag.key % packN;
|
||||||
|
|
||||||
|
merged.set(tagId, (merged.get(tagId) ?? 0) + tag.doc_count);
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}, new Map());
|
||||||
|
|
||||||
|
return Array.from(mergedCounts.entries(), ([key, count]) => ({ key, doc_count: count }));
|
||||||
|
}
|
||||||
|
|
||||||
async function queryManticoreSql(filters, options, _reqUser) {
|
async function queryManticoreSql(filters, options, _reqUser) {
|
||||||
const aggSize = config.database.manticore.maxAggregateSize;
|
const aggSize = config.database.manticore.maxAggregateSize;
|
||||||
|
|
||||||
@@ -437,7 +454,6 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||||||
:yearsFacet:
|
:yearsFacet:
|
||||||
:actorsFacet:
|
:actorsFacet:
|
||||||
:tagsFacet:
|
:tagsFacet:
|
||||||
:actorTagsFacet:
|
|
||||||
:channelsFacet:
|
:channelsFacet:
|
||||||
:studiosFacet:;
|
:studiosFacet:;
|
||||||
show meta;
|
show meta;
|
||||||
@@ -470,11 +486,6 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||||||
year(scenes.effective_date) as effective_year,
|
year(scenes.effective_date) as effective_year,
|
||||||
weight() as _score
|
weight() as _score
|
||||||
`));
|
`));
|
||||||
|
|
||||||
// manticore only supports one joined table, so we can't use it inside stashes
|
|
||||||
builder
|
|
||||||
.leftJoin('scenes_tags', 'scenes_tags.scene_id', 'scenes_.id')
|
|
||||||
.groupBy('scenes.id');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.query) {
|
if (filters.query) {
|
||||||
@@ -487,7 +498,17 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
filters.tagIds?.forEach((tagId) => {
|
filters.tagIds?.forEach((tagId) => {
|
||||||
builder.where('any(tag_ids)', tagId);
|
if (filters.onlyActorTags) {
|
||||||
|
builder.where((whereBuilder) => {
|
||||||
|
whereBuilder.where('any(assigned_tag_ids)', tagId);
|
||||||
|
|
||||||
|
filters.actorIds?.forEach((actorId) => {
|
||||||
|
whereBuilder.orWhere('any(assigned_tag_ids)', actorId * 1_000_00 + tagId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
builder.where('any(tag_ids)', tagId);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (filters.notTagIds) {
|
if (filters.notTagIds) {
|
||||||
@@ -530,12 +551,6 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||||||
builder.where('scenes.is_showcased', filters.isShowcased);
|
builder.where('scenes.is_showcased', filters.isShowcased);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
if (filters.isShowcased) {
|
|
||||||
builder.where('scenes.date', '>', 0);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (options.dedupe) {
|
if (options.dedupe) {
|
||||||
builder.where('scenes.dupe_index', '<', 2);
|
builder.where('scenes.dupe_index', '<', 2);
|
||||||
}
|
}
|
||||||
@@ -580,11 +595,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||||||
// option threads=1 fixes actors, but drastically slows down performance, wait for fix
|
// option threads=1 fixes actors, but drastically slows down performance, wait for fix
|
||||||
yearsFacet: options.aggregateYears ? knex.raw('facet effective_year as years_facet order by effective_year desc limit ?', [aggSize]) : null,
|
yearsFacet: options.aggregateYears ? knex.raw('facet effective_year as years_facet order by effective_year desc limit ?', [aggSize]) : null,
|
||||||
actorsFacet: options.aggregateActors ? knex.raw('facet scenes.actor_ids as actors_facet distinct id order by count(*) desc limit ?', [aggSize]) : null,
|
actorsFacet: options.aggregateActors ? knex.raw('facet scenes.actor_ids as actors_facet distinct id order by count(*) desc limit ?', [aggSize]) : null,
|
||||||
// don't facet tags associated to other actors, actor ID 0 means global
|
tagsFacet: options.aggregateTags ? knex.raw('facet scenes.assigned_tag_ids as tags_facet distinct id order by count(*) desc limit ?', [aggSize]) : null,
|
||||||
tagsFacet: options.aggregateTags ? knex.raw('facet scenes.tag_ids as tags_facet distinct id order by count(*) desc limit ?', [aggSize]) : null,
|
|
||||||
actorTagsFacet: options.aggregateTags && !filters.stashId // eslint-disable-line no-nested-ternary
|
|
||||||
? knex.raw(`facet IF(IN(scenes_tags.actor_id, ${[0, ...filters?.actorIds || []]}), scenes_tags.tag_id, 0) as actor_tags_facet distinct id order by count(*) desc limit ?`, [aggSize])
|
|
||||||
: null,
|
|
||||||
channelsFacet: options.aggregateChannels ? knex.raw('facet scenes.channel_id as channels_facet distinct id order by count(*) desc limit ?', [aggSize]) : null,
|
channelsFacet: options.aggregateChannels ? knex.raw('facet scenes.channel_id as channels_facet distinct id order by count(*) desc limit ?', [aggSize]) : null,
|
||||||
studiosFacet: options.aggregateChannels ? knex.raw('facet scenes.studio_id as studios_facet distinct id order by count(*) desc limit ?', [aggSize]) : null,
|
studiosFacet: options.aggregateChannels ? knex.raw('facet scenes.studio_id as studios_facet distinct id order by count(*) desc limit ?', [aggSize]) : null,
|
||||||
maxMatches: config.database.manticore.maxMatches,
|
maxMatches: config.database.manticore.maxMatches,
|
||||||
@@ -607,10 +618,26 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||||||
const years = curateFacet(results, 'years_facet');
|
const years = curateFacet(results, 'years_facet');
|
||||||
const actorIds = curateFacet(results, 'actors_facet');
|
const actorIds = curateFacet(results, 'actors_facet');
|
||||||
const tagIds = curateFacet(results, 'tags_facet');
|
const tagIds = curateFacet(results, 'tags_facet');
|
||||||
const actorTagIds = curateFacet(results, 'actor_tags_facet');
|
|
||||||
const channelIds = curateFacet(results, 'channels_facet');
|
const channelIds = curateFacet(results, 'channels_facet');
|
||||||
const studioIds = curateFacet(results, 'studios_facet');
|
const studioIds = curateFacet(results, 'studios_facet');
|
||||||
|
|
||||||
|
const allTagIds = mergePackedTags(tagIds);
|
||||||
|
|
||||||
|
const actorTagIds = mergePackedTags(tagIds.filter((tag) => {
|
||||||
|
if (tag.key < packN || !filters?.actorIds.length) {
|
||||||
|
// global
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagActorId = Math.floor(tag.key / packN);
|
||||||
|
|
||||||
|
if (filters.actorIds.includes(tagActorId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}));
|
||||||
|
|
||||||
const total = Number(results.at(-1).data.find((entry) => entry.Variable_name === 'total_found')?.Value) || 0;
|
const total = Number(results.at(-1).data.find((entry) => entry.Variable_name === 'total_found')?.Value) || 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -619,7 +646,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
|||||||
aggregations: {
|
aggregations: {
|
||||||
years,
|
years,
|
||||||
actorIds,
|
actorIds,
|
||||||
tagIds,
|
tagIds: allTagIds,
|
||||||
actorTagIds,
|
actorTagIds,
|
||||||
channelIds,
|
channelIds,
|
||||||
studioIds,
|
studioIds,
|
||||||
|
|||||||
41
src/sync.js
41
src/sync.js
@@ -114,7 +114,8 @@ export async function syncManticoreScenes(sceneIds) {
|
|||||||
studios.name as studio_name,
|
studios.name as studio_name,
|
||||||
grandparents.id as parent_network_id,
|
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 (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 (actors_aliases.id, actors_aliases.name)) FILTER (WHERE actors_aliases.id IS NOT NULL), '[]') as actors_aliases,
|
||||||
|
COALESCE(JSON_AGG(DISTINCT (tags.id, tags.name, tags.priority, tags_aliases.name, local_tags.actor_id)) 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 (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,
|
COALESCE(JSON_AGG(DISTINCT (series.id, series.title)) FILTER (WHERE series.id IS NOT NULL), '[]') as series,
|
||||||
studios.showcased IS NOT false
|
studios.showcased IS NOT false
|
||||||
@@ -135,6 +136,7 @@ export async function syncManticoreScenes(sceneIds) {
|
|||||||
LEFT JOIN releases_tags AS local_tags ON local_tags.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 ON local_actors.actor_id = actors.id
|
||||||
LEFT JOIN actors AS directors ON local_directors.director_id = directors.id
|
LEFT JOIN actors AS directors ON local_directors.director_id = directors.id
|
||||||
|
LEFT JOIN actors AS actors_aliases ON actors_aliases.alias_for = actors.id
|
||||||
LEFT JOIN tags ON local_tags.tag_id = tags.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 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_scenes ON movies_scenes.scene_id = releases.id
|
||||||
@@ -185,6 +187,18 @@ export async function syncManticoreScenes(sceneIds) {
|
|||||||
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 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]);
|
const filteredTitle = filterTitle(scene.title, [...flatActors, ...flatTags]);
|
||||||
|
|
||||||
|
// use decimal packing with 5-decimal pad to allow for actor-specific tags, i.e. actor 135 tag 5 = 13500005
|
||||||
|
// all global tags are necessarily < 10,000, all tags for actor 135 are >= 13500000 and <= 13599999
|
||||||
|
// f1 = tag ID, f5 = actor ID
|
||||||
|
const assignedTagIds = scene.tags.map((tag) => (tag.f5 === null ? tag.f1 : tag.f5 * 1_000_00 + tag.f1));
|
||||||
|
|
||||||
|
/*
|
||||||
|
if (sceneId === '187734') {
|
||||||
|
console.log(scene, assignedTagIds);
|
||||||
|
throw new Error('ABORT');
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return {
|
return {
|
||||||
replace: {
|
replace: {
|
||||||
index: 'scenes',
|
index: 'scenes',
|
||||||
@@ -207,9 +221,10 @@ export async function syncManticoreScenes(sceneIds) {
|
|||||||
studio_slug: scene.studio_slug || undefined,
|
studio_slug: scene.studio_slug || undefined,
|
||||||
studio_name: scene.studio_name || 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
|
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),
|
actor_ids: scene.actors.map((actor) => actor.f1), // don't include aliases in ID or they would show up in filters
|
||||||
actors: scene.actors.map((actor) => actor.f2).join(),
|
actors: Array.from(new Set([...scene.actors.map((actor) => actor.f2), ...scene.actors_aliases.map((actor) => actor.f2)])).join(),
|
||||||
tag_ids: scene.tags.map((tag) => tag.f1),
|
tag_ids: Array.from(new Set(scene.tags.map((tag) => tag.f1))),
|
||||||
|
assigned_tag_ids: assignedTagIds,
|
||||||
tags: flatTags.join(' '), // only make top tags searchable to minimize cluttered results
|
tags: flatTags.join(' '), // only make top tags searchable to minimize cluttered results
|
||||||
movie_ids: scene.movies.map((movie) => movie.f1),
|
movie_ids: scene.movies.map((movie) => movie.f1),
|
||||||
movies: scene.movies.map((movie) => movie.f2).join(' '),
|
movies: scene.movies.map((movie) => movie.f2).join(' '),
|
||||||
@@ -470,11 +485,13 @@ export async function syncQueue() {
|
|||||||
logger[process.tasks > 0 ? 'info' : 'verbose'](`Processed ${tasks.length} sync items`);
|
logger[process.tasks > 0 ? 'info' : 'verbose'](`Processed ${tasks.length} sync items`);
|
||||||
}
|
}
|
||||||
|
|
||||||
CronJob.from({
|
export function initSyncCron() {
|
||||||
cronTime: config.sync.crontab,
|
CronJob.from({
|
||||||
async onTick() {
|
cronTime: config.sync.crontab,
|
||||||
syncQueue();
|
async onTick() {
|
||||||
},
|
syncQueue();
|
||||||
start: config.sync.enabled,
|
},
|
||||||
runOnInit: true,
|
start: config.sync.enabled,
|
||||||
});
|
runOnInit: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export async function curateScenesQuery(query) {
|
|||||||
notActorIds: splitActors.filter((actor) => actor.charAt(0) === '!').map((identifier) => parseActorIdentifier(identifier.slice(1))?.id).filter(Boolean),
|
notActorIds: splitActors.filter((actor) => actor.charAt(0) === '!').map((identifier) => parseActorIdentifier(identifier.slice(1))?.id).filter(Boolean),
|
||||||
tagIds,
|
tagIds,
|
||||||
notTagIds: notTagIds.filter((tagId) => !tagIds.includes(tagId)), // included tags get priority over excluded tags
|
notTagIds: notTagIds.filter((tagId) => !tagIds.includes(tagId)), // included tags get priority over excluded tags
|
||||||
|
onlyActorTags: !!query.at,
|
||||||
entityId,
|
entityId,
|
||||||
notEntityIds,
|
notEntityIds,
|
||||||
movieId: Number(query.movieId) || null,
|
movieId: Number(query.movieId) || null,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ async function init() {
|
|||||||
actor_ids multi,
|
actor_ids multi,
|
||||||
actors text,
|
actors text,
|
||||||
tag_ids multi,
|
tag_ids multi,
|
||||||
|
assigned_tag_ids multi64,
|
||||||
tags text,
|
tags text,
|
||||||
movie_ids multi,
|
movie_ids multi,
|
||||||
movies text,
|
movies text,
|
||||||
@@ -41,12 +42,15 @@ async function init() {
|
|||||||
)`);
|
)`);
|
||||||
|
|
||||||
await utilsApi.sql('drop table if exists scenes_tags');
|
await utilsApi.sql('drop table if exists scenes_tags');
|
||||||
|
|
||||||
|
/* legacy, using packed decimal keys now
|
||||||
await utilsApi.sql(`create table scenes_tags (
|
await utilsApi.sql(`create table scenes_tags (
|
||||||
id int,
|
id int,
|
||||||
scene_id int,
|
scene_id int,
|
||||||
tag_id int,
|
tag_id int,
|
||||||
actor_id int
|
actor_id int
|
||||||
)`);
|
)`);
|
||||||
|
*/
|
||||||
|
|
||||||
console.log('Recreated scenes tables, syncing scenes...');
|
console.log('Recreated scenes tables, syncing scenes...');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user