Compare commits

..

35 Commits

Author SHA1 Message Date
27ce8b0ceb 0.47.12 2026-03-13 04:52:24 +01:00
0e5724533f Removed Manticore tools to prevent confusion. 2026-03-13 04:52:22 +01:00
cea58d12ff 0.47.11 2026-03-13 04:45:10 +01:00
25034e7a4b Added manticore table recreation to stash sync. 2026-03-13 04:45:07 +01:00
cd4a7ce9c8 0.47.10 2026-03-10 22:12:41 +01:00
2229255ff4 Disabled actor tags for performance evaluation. 2026-03-10 22:12:39 +01:00
299dbe3239 0.47.9 2026-03-07 02:30:53 +01:00
deced84c59 Added function for scene facet composition. 2026-03-07 02:30:51 +01:00
0150ae8d1c 0.47.8 2026-03-07 02:09:06 +01:00
6877ee75ed Added toggle to select actor tags or all tags in filters. 2026-03-07 02:09:04 +01:00
b5726aec84 0.47.7 2026-03-06 06:12:25 +01:00
6c1f1c2a1c Added cache reload button to admin panel so restarts are needed less often. 2026-03-06 06:12:23 +01:00
9a59448933 0.47.6 2026-03-06 05:13:23 +01:00
c55f6d2cf2 Added /tour filter to affiliate link composition. Added note to scene tag edit field. 2026-03-06 05:13:21 +01:00
16bf7b019f 0.47.5 2026-03-06 03:57:56 +01:00
1ff5b6b036 Fixed global tags not showing up if no actor is filtered for. 2026-03-06 03:57:53 +01:00
3c47a1b14e 0.47.4 2026-03-05 17:23:31 +01:00
da6ccccab4 Fixed tile tag deduping order. 2026-03-05 17:23:29 +01:00
f79ef53ebd 0.47.3 2026-03-05 16:34:43 +01:00
185984bf0c Fixed actor and channel aggregation duplicates. 2026-03-05 16:34:41 +01:00
dd522c1fb1 0.47.2 2026-03-05 02:00:12 +01:00
07f290ad85 Excluding actor-specific tags from aggregated tag filter. 2026-03-05 02:00:10 +01:00
eefc213144 0.47.1 2026-03-04 04:03:51 +01:00
849b4f0de7 Improved scene actor tag spacing CSS. 2026-03-04 04:03:48 +01:00
bf3a712de8 0.47.0 2026-03-04 03:57:07 +01:00
46839b48cf Added tag actor editing. 2026-03-04 03:57:04 +01:00
f4447b23de Flipped scene and actor tags. 2026-03-04 02:53:49 +01:00
8a734b9fa9 0.46.24 2026-03-04 02:52:57 +01:00
34ca806e84 Displaying actor-specific scene tags. 2026-03-04 02:52:55 +01:00
b7ac8917e9 0.46.23 2026-03-02 22:35:43 +01:00
39b25209f4 Selecting independent site banners from network directory. 2026-03-02 22:35:41 +01:00
ce287bc006 0.46.22 2026-03-02 04:08:50 +01:00
f0e3e741ff Fixed banner path for independent sites. 2026-03-02 04:08:47 +01:00
856928760d 0.46.21 2026-03-01 17:51:55 +01:00
a4bd5d0d83 Restricting trailers to logged in users (there seems to have been a regression). 2026-03-01 17:51:49 +01:00
29 changed files with 363 additions and 424 deletions

View File

@@ -62,6 +62,7 @@
--text: #222; --text: #222;
--text-light: #fff; --text-light: #fff;
--text-piss: #b92;
/* --link: #48f; */ /* --link: #48f; */
--link: var(--primary); --link: var(--primary);
@@ -101,6 +102,7 @@
--background-dim: var(--shadow-weak-10); --background-dim: var(--shadow-weak-10);
--text: #fcfcfc; --text: #fcfcfc;
--text-piss: #ba0;
--glass-weak-50: rgba(255, 255, 255, .02); --glass-weak-50: rgba(255, 255, 255, .02);
--glass-weak-40: rgba(255, 255, 255, .05); --glass-weak-40: rgba(255, 255, 255, .05);

View File

@@ -0,0 +1,6 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M14.422 7.078c-0.786-0.672-1.807-1.078-2.922-1.078-2.202 0-4.035 1.582-4.424 3.672h1.54c0.36-1.253 1.517-2.172 2.884-2.172 0.7 0 1.344 0.241 1.855 0.645l-1.855 1.855h4.5v-4.5l-1.578 1.578z"></path>
<path d="M11.5 13.5c-0.7 0-1.344-0.241-1.855-0.645l1.855-1.855h-4.5v4.5l1.578-1.578c0.786 0.672 1.807 1.078 2.922 1.078 2.202 0 4.035-1.582 4.424-3.672h-1.54c-0.36 1.253-1.517 2.172-2.884 2.172z"></path>
<path d="M2.158 9.146c-0.762-0.275-1.074-0.562-1.158-0.677v-2.531c0.995 0.643 2.64 1.062 4.5 1.062 0.169 0 0.335-0.004 0.5-0.010v-1.314c-0.165 0.008-0.332 0.012-0.5 0.012-1.152 0-2.252-0.181-3.098-0.51-0.707-0.275-0.996-0.562-1.074-0.677 0.078-0.115 0.367-0.402 1.074-0.677 0.846-0.329 1.946-0.51 3.098-0.51s2.252 0.181 3.098 0.51c0.707 0.275 0.995 0.562 1.074 0.677-0.061 0.090-0.252 0.287-0.674 0.5h1.892c0.072-0.162 0.11-0.329 0.11-0.5 0-1.381-2.462-2.5-5.5-2.5s-5.5 1.119-5.5 2.5v8c0 1.381 2.462 2.5 5.5 2.5 0.169 0 0.335-0.004 0.5-0.010v-1.312c-0.165 0.007-0.332 0.010-0.5 0.010-1.242 0-2.429-0.181-3.342-0.51-0.763-0.275-1.074-0.562-1.158-0.677v-2.594c0.995 0.643 2.64 1.062 4.5 1.062 0.169 0 0.335-0.004 0.5-0.010v-1.312c-0.165 0.007-0.332 0.010-0.5 0.010-1.242 0-2.429-0.181-3.342-0.51z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M5.5 7c3.038 0 5.5-1.119 5.5-2.5s-2.462-2.5-5.5-2.5-5.5 1.119-5.5 2.5v8c0 1.381 2.462 2.5 5.5 2.5 0.503 0 0.99-0.031 1.452-0.088-0.287-0.381-0.526-0.8-0.711-1.246-0.244 0.014-0.491 0.022-0.741 0.022-1.242 0-2.429-0.181-3.342-0.51-0.763-0.275-1.074-0.562-1.158-0.677v-2.594c0.995 0.643 2.64 1.062 4.5 1.062 0.114 0 0.226-0.002 0.338-0.005 0.043-0.459 0.141-0.902 0.287-1.324-0.206 0.010-0.415 0.016-0.625 0.016-1.242 0-2.429-0.181-3.342-0.51-0.762-0.275-1.074-0.562-1.158-0.677v-2.531c0.995 0.643 2.64 1.062 4.5 1.062zM2.402 3.823c0.846-0.329 1.946-0.51 3.098-0.51s2.252 0.181 3.098 0.51c0.707 0.275 0.995 0.562 1.074 0.677-0.078 0.115-0.367 0.402-1.074 0.677-0.846 0.329-1.946 0.51-3.098 0.51s-2.252-0.181-3.098-0.51c-0.707-0.275-0.996-0.562-1.074-0.677 0.078-0.115 0.367-0.402 1.074-0.677z"></path>
<path d="M11.5 7c-2.485 0-4.5 2.015-4.5 4.5s2.015 4.5 4.5 4.5 4.5-2.015 4.5-4.5-2.015-4.5-4.5-4.5zM14 12h-3v-3h1v2h2v1z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,6 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M11.5 7c-2.485 0-4.5 2.015-4.5 4.5s2.015 4.5 4.5 4.5c2.485 0 4.5-2.015 4.5-4.5s-2.015-4.5-4.5-4.5zM11.5 14.688c-1.758 0-3.188-1.43-3.188-3.188s1.43-3.188 3.188-3.188 3.188 1.43 3.188 3.188-1.43 3.188-3.188 3.188z"></path>
<path d="M12 11v-2h-1v3h3v-1z"></path>
<path d="M5.5 7c3.038 0 5.5-1.119 5.5-2.5s-2.462-2.5-5.5-2.5-5.5 1.119-5.5 2.5v8c0 1.381 2.462 2.5 5.5 2.5 0.503 0 0.99-0.031 1.452-0.088-0.287-0.381-0.526-0.8-0.711-1.246-0.244 0.014-0.491 0.022-0.741 0.022-1.242 0-2.429-0.181-3.342-0.51-0.763-0.275-1.074-0.562-1.158-0.677v-2.594c0.995 0.643 2.64 1.062 4.5 1.062 0.114 0 0.226-0.002 0.338-0.005 0.043-0.459 0.141-0.902 0.287-1.324-0.206 0.010-0.415 0.016-0.625 0.016-1.242 0-2.429-0.181-3.342-0.51-0.762-0.275-1.074-0.562-1.158-0.677v-2.531c0.995 0.643 2.64 1.062 4.5 1.062zM2.402 3.823c0.846-0.329 1.946-0.51 3.098-0.51s2.252 0.181 3.098 0.51c0.707 0.275 0.995 0.562 1.074 0.677-0.078 0.115-0.367 0.402-1.074 0.677-0.846 0.329-1.946 0.51-3.098 0.51s-2.252-0.181-3.098-0.51c-0.707-0.275-0.996-0.562-1.074-0.677 0.078-0.115 0.367-0.402 1.074-0.677z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

5
assets/img/icons/loop3.svg Executable file
View File

@@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M13.901 2.599c-1.463-1.597-3.565-2.599-5.901-2.599-4.418 0-8 3.582-8 8h1.5c0-3.59 2.91-6.5 6.5-6.5 1.922 0 3.649 0.835 4.839 2.161l-2.339 2.339h5.5v-5.5l-2.099 2.099z"></path>
<path d="M14.5 8c0 3.59-2.91 6.5-6.5 6.5-1.922 0-3.649-0.835-4.839-2.161l2.339-2.339h-5.5v5.5l2.099-2.099c1.463 1.597 3.565 2.599 5.901 2.599 4.418 0 8-3.582 8-8h-1.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 500 B

4
assets/img/icons/reset.svg Executable file
View File

@@ -0,0 +1,4 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path d="M4.126 9c-0.082 0.32-0.126 0.655-0.126 1 0 2.209 1.791 4 4 4s4-1.791 4-4-1.791-4-4-4v3l-5-4 5-4v3c3.314 0 6 2.686 6 6s-2.686 6-6 6-6-2.686-6-6c0-0.341 0.029-0.675 0.084-1h2.043z"></path>
</svg>

After

Width:  |  Height:  |  Size: 334 B

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generated by IcoMoon.io -->
<svg
version="1.1"
width="16"
height="16"
viewBox="0 0 16 16"
id="svg2"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<path
id="path2"
d="M 7 0 C 4 0 4 2.015 4 4.5 C 4 6.048 4.898 7.5957969 6 8.2167969 L 6 9.0410156 C 2.608 9.3180156 0 10.985 0 13 L 4.9726562 13 C 4.6689986 12.449922 4.7486357 11.731833 5.2109375 11.269531 L 8 8.4804688 L 8 8.2167969 C 9.102 7.5957969 10 6.048 10 4.5 C 10 2.015 10 0 7 0 z M 9 11.509766 L 8.2167969 12.292969 L 8.9238281 13 L 9 13 L 9 11.509766 z " />
<g
id="g2"
transform="matrix(0.51568847,0,0,0.51568847,5.8471254,8.4255678)">
<path
d="m 18.938,-1 h -6 c -0.412,0 -0.989,0.239 -1.28,0.53 L 4.219,6.969 c -0.292,0.292 -0.292,0.769 0,1.061 l 6.439,6.439 c 0.292,0.292 0.769,0.292 1.061,0 L 19.158,7.03 c 0.292,-0.292 0.53,-0.868 0.53,-1.28 v -6 c 0,-0.412 -0.337,-0.75 -0.75,-0.75 z m -3.75,6 c -0.828,0 -1.5,-0.672 -1.5,-1.5 0,-0.828 0.672,-1.5 1.5,-1.5 0.828,0 1.5,0.672 1.5,1.5 0,0.828 -0.672,1.5 -1.5,1.5 z"
id="path1-5" />
<path
d="m 1.688,7.5 8.5,-8.5 h -1.25 c -0.412,0 -0.989,0.239 -1.28,0.53 L 0.219,6.969 c -0.292,0.292 -0.292,0.769 0,1.061 l 6.439,6.439 c 0.292,0.292 0.769,0.292 1.061,0 l 0.47,-0.47 -6.5,-6.5 z"
id="path2-3" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -26,6 +26,11 @@
>Entity Health</a> >Entity Health</a>
</li> </li>
</ul> </ul>
<button
class="button"
@click="initCaches"
><Icon icon="database-refresh" />Reload caches</button>
</nav> </nav>
<div class="content"> <div class="content">
@@ -37,7 +42,17 @@
<script setup> <script setup>
import { inject } from 'vue'; import { inject } from 'vue';
import { post } from '#/src/api.js';
const pageContext = inject('pageContext'); const pageContext = inject('pageContext');
async function initCaches() {
await post('/caches', null, {
successFeedback: 'Reloaded caches',
errorFeedback: 'Failed to reload caches',
appendErrorMessage: true,
});
}
</script> </script>
<style scoped> <style scoped>
@@ -50,6 +65,7 @@ const pageContext = inject('pageContext');
.nav { .nav {
display: flex; display: flex;
justify-content: space-between;
padding: 1rem 1rem .75rem 1rem; padding: 1rem 1rem .75rem 1rem;
border-bottom: solid 1px var(--shadow-weak-30); border-bottom: solid 1px var(--shadow-weak-30);
margin-bottom: .25rem; margin-bottom: .25rem;

View File

@@ -66,6 +66,7 @@ const props = defineProps({
const bannerSrc = (() => { const bannerSrc = (() => {
if (props.campaign.banner) { if (props.campaign.banner) {
// if (props.campaign.banner.entity.type === 'network' || props.campaign.banner.entity.isIndependent || !props.campaign.banner.entity.parent) {
if (props.campaign.banner.entity.type === 'network' || !props.campaign.banner.entity.parent) { if (props.campaign.banner.entity.type === 'network' || !props.campaign.banner.entity.parent) {
return `/banners/${props.campaign.banner.entity.slug}/${props.campaign.banner.id}.${props.campaign.banner.type || 'jpg'}`; return `/banners/${props.campaign.banner.entity.slug}/${props.campaign.banner.id}.${props.campaign.banner.type || 'jpg'}`;
} }

View File

@@ -1,43 +1,56 @@
<template> <template>
<ul <div class="tags-sections">
class="tags nolist" <div
:class="{ disabled: !editing.has(item.key) }" v-for="actorTags in tags"
> :key="`tags-${actorTags.actor?.slug || 'scene'}`"
<li class="tags-section"
v-for="tag in [...item.value, ...newTags]"
:key="`tag-${tag.id}`"
class="tag"
:class="{ deleted: edits.tags && !edits.tags.some((tagId) => tagId === tag.id) }"
> >
<span class="tag-name">{{ tag.name }}</span> <ul
class="tags nolist"
<Icon :class="{ disabled: !editing.has(item.key) }"
v-if="edits.tags && !edits.tags.some((tagId) => tagId === tag.id)"
icon="checkmark"
class="add"
@click="emit('tags', edits.tags.concat(tag.id))"
/>
<Icon
v-else
icon="cross2"
class="remove"
@click="emit('tags', edits.tags.filter((tagId) => tagId !== tag.id))"
/>
</li>
<li class="new">
<TagSearch
:disabled="!editing.has(item.key)"
@tag="addTag"
> >
<Icon <li
icon="plus3" v-if="actorTags.actor"
class="add" class="tags-actor"
/> >{{ actorTags.actor.name }}:</li>
</TagSearch>
</li> <li
</ul> v-for="tag in [...actorTags.tags, ...newTags.filter((newTag) => newTag.actorId === actorTags.actorId)]"
:key="`tag-${tag.id}`"
class="tag"
:class="{ deleted: edits.tags && !edits.tags.some((sceneTag) => sceneTag.id === tag.id && sceneTag.actorId === actorTags.actorId) }"
>
<span class="tag-name">{{ tag.name }}</span>
<Icon
v-if="edits.tags && !edits.tags.some((sceneTag) => sceneTag.id === tag.id && sceneTag.actorId === actorTags.actorId)"
icon="checkmark"
class="add"
@click="emit('tags', edits.tags.concat(tag))"
/>
<Icon
v-else
icon="cross2"
class="remove"
@click="emit('tags', edits.tags.filter((sceneTag) => !(sceneTag.id === tag.id && sceneTag.actorId === actorTags.actorId)))"
/>
</li>
<li class="new">
<TagSearch
:disabled="!editing.has(item.key)"
@tag="(tag) => addTag(tag, actorTags.actor)"
>
<Icon
icon="plus3"
class="add"
/>
</TagSearch>
</li>
</ul>
</div>
</div>
</template> </template>
<script setup> <script setup>
@@ -70,8 +83,23 @@ const props = defineProps({
const emit = defineEmits(['tags']); const emit = defineEmits(['tags']);
function addTag(tag) { const tags = [
if (props.edits.tags.some((tagId) => tagId === tag.id)) { {
tags: props.item.value.filter((tag) => tag.actorId === null),
actor: null,
actorId: null,
},
...props.scene.actors.map((actor) => ({
tags: props.item.value.filter((tag) => tag.actorId === actor.id),
actor,
actorId: actor?.id || null,
})),
];
function addTag(newTag, actor) {
const actorId = actor?.id || null;
if (props.edits.tags.some((sceneTag) => sceneTag.id === newTag.id && sceneTag.actorId === actorId)) {
events.emit('feedback', { events.emit('feedback', {
type: 'error', type: 'error',
message: 'Tag already added', message: 'Tag already added',
@@ -80,9 +108,13 @@ function addTag(tag) {
return; return;
} }
newTags.value = newTags.value.concat(tag); const newTagWithActorId = {
...newTag,
actorId,
};
emit('tags', props.edits.tags.concat(tag.id)); newTags.value = newTags.value.concat(newTagWithActorId);
emit('tags', props.edits.tags.concat(newTagWithActorId));
} }
watch(() => props.scene, () => { newTags.value = []; }); watch(() => props.scene, () => { newTags.value = []; });
@@ -116,7 +148,7 @@ watch(() => props.scene, () => { newTags.value = []; });
align-items: center; align-items: center;
margin-left: .25rem; margin-left: .25rem;
&:hover { &:hover .icon {
box-shadow: 0 0 3px var(--shadow-weak-20); box-shadow: 0 0 3px var(--shadow-weak-20);
} }
@@ -129,6 +161,18 @@ watch(() => props.scene, () => { newTags.value = []; });
} }
} }
.tags-sections {
display: flex;
flex-direction: column;
gap: .75rem;
margin: .5rem 0;
}
.tags-actor {
margin-right: .5rem;
font-weight: bold;
}
.tag { .tag {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
@@ -145,9 +189,11 @@ watch(() => props.scene, () => { newTags.value = []; });
.tag, .tag,
.new { .new {
.remove, .remove,
.add { .add,
.actor {
height: auto; height: auto;
padding: .25rem .3rem; padding: .25rem .3rem;
margin-left: .25rem;
border-radius: .25rem; border-radius: .25rem;
fill: var(--highlight-strong-10); fill: var(--highlight-strong-10);

View File

@@ -12,14 +12,32 @@
<Icon icon="search" /> <Icon icon="search" />
</label> </label>
<!--
<div
v-show="showActorTags"
v-tooltip="'Tags relevant to the selected actors'"
class="filter-sort order noselect"
@click="showActorTags = false"
>
<Icon icon="user-tags" />
</div>
<div
v-show="!showActorTags"
v-tooltip="'All tags'"
class="filter-sort order noselect"
@click="showActorTags = true"
>
<Icon icon="price-tags" />
</div>
-->
<div <div
v-show="order === 'priority'" v-show="order === 'priority'"
class="filter-sort order noselect" class="filter-sort order noselect"
@click="order = 'count'" @click="order = 'count'"
> >
<Icon <Icon icon="star" />
icon="star"
/>
</div> </div>
<div <div
@@ -115,16 +133,22 @@ const props = defineProps({
type: Array, type: Array,
default: () => [], default: () => [],
}, },
actorTags: {
type: Array,
default: () => [],
},
}); });
const emit = defineEmits(['update']); const emit = defineEmits(['update']);
const { pageProps } = inject('pageContext');
// const { tag: pageTag, actor: pageActor } = pageProps;
const { tag: pageTag } = pageProps;
const search = ref(''); const search = ref('');
const searchRegexp = computed(() => new RegExp(search.value, 'i')); const searchRegexp = computed(() => new RegExp(search.value, 'i'));
const order = ref('priority'); const order = ref('priority');
// const showActorTags = ref(true);
const { pageProps } = inject('pageContext');
const { tag: pageTag } = pageProps;
const priorityTags = [ const priorityTags = [
'anal', 'anal',
@@ -148,8 +172,15 @@ const priorityTags = [
]; ];
const groupedTags = computed(() => { const groupedTags = computed(() => {
const selected = props.tags.filter((tag) => props.filters.tags.includes(tag.slug)); /*
const filtered = props.tags.filter((tag) => !props.filters.tags.includes(tag.slug) const tags = showActorTags.value && props.actorTags && (props.filters.actors.length > 0 || pageActor)
? props.actorTags
: props.tags;
*/
const tags = props.tags;
const selected = tags.filter((tag) => props.filters.tags.includes(tag.slug));
const filtered = tags.filter((tag) => !props.filters.tags.includes(tag.slug)
&& tag.id !== pageTag?.id && tag.id !== pageTag?.id
&& searchRegexp.value.test(tag.name)); && searchRegexp.value.test(tag.name));

View File

@@ -34,6 +34,7 @@
<TagsFilter <TagsFilter
:filters="filters" :filters="filters"
:tags="aggTags" :tags="aggTags"
:actor-tags="aggActorTags"
@update="updateFilter" @update="updateFilter"
/> />
@@ -263,6 +264,7 @@ const scenes = ref(pageProps.scenes);
const aggYears = ref(pageProps.aggYears || []); const aggYears = ref(pageProps.aggYears || []);
const aggActors = ref(pageProps.aggActors || []); const aggActors = ref(pageProps.aggActors || []);
const aggTags = ref(pageProps.aggTags || []); const aggTags = ref(pageProps.aggTags || []);
const aggActorTags = ref(pageProps.aggActorTags || []);
const aggChannels = ref(pageProps.aggChannels || []); const aggChannels = ref(pageProps.aggChannels || []);
const currentPage = ref(Number(routeParams.page)); const currentPage = ref(Number(routeParams.page));
@@ -363,6 +365,7 @@ async function search(options = {}) {
aggYears.value = res.aggYears; aggYears.value = res.aggYears;
aggActors.value = res.aggActors; aggActors.value = res.aggActors;
aggTags.value = res.aggTags; aggTags.value = res.aggTags;
aggActorTags.value = res.aggActorTags;
aggChannels.value = res.aggChannels; aggChannels.value = res.aggChannels;
total.value = res.total; total.value = res.total;

View File

@@ -81,7 +81,7 @@
<ul <ul
class="row tags nolist" class="row tags nolist"
:title="scene.tags.map((tag) => tag.name).join(', ')" :title="sceneTags.map((tag) => tag.name).join(', ')"
> >
<li <li
v-if="scene.shootId" v-if="scene.shootId"
@@ -94,9 +94,10 @@
</li> </li>
<li <li
v-for="tag in scene.tags" v-for="tag in sceneTags"
:key="`tag-${scene.id}-${tag.id}`" :key="`tag-${scene.id}-${tag.id}`"
class="tag" class="tag"
:class="{ piss: tag.slug === 'pissing' }"
> >
<Link <Link
:href="`/tag/${tag.slug}`" :href="`/tag/${tag.slug}`"
@@ -134,7 +135,10 @@ const { user } = pageContext;
const pageStash = pageContext.pageProps.stash; const pageStash = pageContext.pageProps.stash;
const currentStash = pageStash || pageContext.assets?.primaryStash; const currentStash = pageStash || pageContext.assets?.primaryStash;
const priorityTags = props.scene.tags.map((tag) => tag.name).slice(0, 2); const tagsById = Object.fromEntries(props.scene.tags.map((tag) => [tag.id, tag]));
const sceneTags = Array.from(new Set(props.scene.tags.map((tag) => tag.id))).map((tagId) => tagsById[tagId]);
const priorityTags = sceneTags.map((tag) => tag.name).slice(0, 2);
const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id === currentStash?.id)); const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id === currentStash?.id));
</script> </script>
@@ -263,6 +267,12 @@ const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.id ===
font-size: .75rem; font-size: .75rem;
} }
/*
.tag.piss {
color: var(--text-piss);
}
*/
.shoot { .shoot {
font-size: .75rem; font-size: .75rem;
font-weight: bold; font-weight: bold;

4
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{ {
"name": "traxxx-web", "name": "traxxx-web",
"version": "0.46.20", "version": "0.47.12",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"version": "0.46.20", "version": "0.47.12",
"dependencies": { "dependencies": {
"@brillout/json-serializer": "^0.5.8", "@brillout/json-serializer": "^0.5.8",
"@dicebear/collection": "^7.0.5", "@dicebear/collection": "^7.0.5",

View File

@@ -92,7 +92,7 @@
"overrides": { "overrides": {
"vite": "$vite" "vite": "$vite"
}, },
"version": "0.46.20", "version": "0.47.12",
"imports": { "imports": {
"#/*": "./*.js" "#/*": "./*.js"
} }

View File

@@ -140,20 +140,30 @@
</li> </li>
</ul> </ul>
<ul <div class="tags">
v-if="scene.tags.length > 0" <div
class="tags nolist" v-for="actorTags in tags"
> :key="`tags-${actorTags.actor?.slug || 'scene'}`"
<li class="tags-section"
v-for="tag in scene.tags"
:key="`tag-${tag.id}`"
> >
<Link <ul class="nolist">
:href="`/tag/${tag.slug}`" <li
class="tag nolink" v-if="actorTags.actor"
>{{ tag.name }}</Link> class="tags-actor"
</li> >{{ actorTags.actor.name }}:</li>
</ul>
<li
v-for="tag in actorTags.tags"
:key="`tag-${tag.id}`"
>
<Link
:href="`/tag/${tag.slug}`"
class="tag nolink"
>{{ tag.name }}</Link>
</li>
</ul>
</div>
</div>
<div <div
v-if="scene.movies.length > 0 || scene.series.length > 0" v-if="scene.movies.length > 0 || scene.series.length > 0"
@@ -440,6 +450,17 @@ const {
const { scene } = pageProps; const { scene } = pageProps;
const tags = [
{
tags: scene.tags.filter((tag) => tag.actorId === null),
actor: null,
},
...scene.actors.map((actor) => ({
actor,
tags: scene.tags.filter((tag) => tag.actorId === actor.id),
})),
].filter((actorTags) => actorTags.tags.length > 0);
const showSummaryDialog = ref(false); const showSummaryDialog = ref(false);
const qualities = { const qualities = {
@@ -640,6 +661,22 @@ function copySummary() {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.tags {
display: flex;
flex-wrap: wrap;
gap: .25rem 1rem;
}
.tags-section {
display: inline-flex;
gap: .25rem;
}
.tags-actor {
margin-right: .5rem;
font-weight: bold;
}
.actors { .actors {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;

View File

@@ -261,6 +261,8 @@ const fields = computed(() => [
key: 'tags', key: 'tags',
type: 'tags', type: 'tags',
value: scene.value.tags.toSorted((tagA, tagB) => tagA.name.localeCompare(tagB.name)), value: scene.value.tags.toSorted((tagA, tagB) => tagA.name.localeCompare(tagB.name)),
simplify: false,
note: 'Actor-specific tags should only be used where confusion is reasonable, such as group scenes in which some perform anal, and some don\'t.',
}, },
{ {
key: 'movies', key: 'movies',

View File

@@ -31,6 +31,10 @@ export function getAffiliateSceneUrl(scene) {
return watchUrl; return watchUrl;
} }
if (scene.affiliate.parameters.channelScenes === false && scene.channel && scene.affiliate.entityId !== scene.channel.id) {
return watchUrl;
}
if (scene.affiliate.parameters.dynamicScene) { if (scene.affiliate.parameters.dynamicScene) {
const scenePath = new URL(watchUrl).pathname; const scenePath = new URL(watchUrl).pathname;
@@ -65,7 +69,7 @@ export function getAffiliateSceneUrl(scene) {
&& (!scene.channel.isIndependent || scene.channel.id === scene.affiliate.entityId)) { && (!scene.channel.isIndependent || scene.channel.id === scene.affiliate.entityId)) {
const { pathname, search } = new URL(watchUrl); const { pathname, search } = new URL(watchUrl);
return `${affiliateUrl}${pathname.replace(/^\/trial/, '')}${search}`; // replace needed for Jules Jordan, verify behavior on other sites return `${affiliateUrl}${pathname.replace(/^\/(trial|tour)/, '')}${search}`; // replace needed for Jules Jordan and HussiePass, verify behavior on other sites
} }
const affiliateUrlComponents = new URL(affiliateUrl); const affiliateUrlComponents = new URL(affiliateUrl);

View File

@@ -1,14 +1,8 @@
import initServer from './web/server.js'; import initServer from './web/server.js';
import { cacheTagIds } from './tags.js'; import { initCaches } from './cache.js';
import { cacheEntityIds } from './entities.js';
import { cacheCampaigns } from './campaigns.js';
async function init() { async function init() {
await Promise.all([ await initCaches();
cacheTagIds(),
cacheEntityIds(),
cacheCampaigns(),
]);
initServer(); initServer();
} }

View File

@@ -1,5 +1,9 @@
import redis from './redis.js'; import redis from './redis.js';
import { cacheTagIds } from './tags.js';
import { cacheEntityIds } from './entities.js';
import { cacheCampaigns } from './campaigns.js';
export async function getIdsBySlug(slugs, domain, toMap) { export async function getIdsBySlug(slugs, domain, toMap) {
if (!slugs) { if (!slugs) {
return []; return [];
@@ -25,3 +29,11 @@ export async function getIdsBySlug(slugs, domain, toMap) {
return ids.filter(Boolean); return ids.filter(Boolean);
} }
export async function initCaches() {
await Promise.all([
cacheTagIds(),
cacheEntityIds(),
cacheCampaigns(),
]);
}

View File

@@ -82,6 +82,7 @@ function curateScene(rawScene, assets, reqUser, context) {
slug: tag.slug, slug: tag.slug,
name: censor(tag.name, context.restriction), name: censor(tag.name, context.restriction),
priority: tag.priority, priority: tag.priority,
actorId: tag.actor_id,
})), })),
chapters: assets.chapters.map((chapter) => ({ chapters: assets.chapters.map((chapter) => ({
id: chapter.id, id: chapter.id,
@@ -137,7 +138,11 @@ function curateScene(rawScene, assets, reqUser, context) {
const isVideoRestricted = config.media.videoRestrictions.includes(curatedScene.channel.slug) || config.media.videoRestrictions.includes(`_${curatedScene.network?.slug}`); const isVideoRestricted = config.media.videoRestrictions.includes(curatedScene.channel.slug) || config.media.videoRestrictions.includes(`_${curatedScene.network?.slug}`);
if (!isVideoRestricted || reqUser?.abilities?.some((ability) => ability.trailerAccess)) { if (!isVideoRestricted || reqUser?.abilities?.some((ability) => ability.trailerAccess)) {
curatedScene.trailer = curateMedia(assets.trailer, { type: 'trailer', isRestricted: isVideoRestricted }); if (reqUser) {
// only show trailers to logged in users to curb S3 traffic
curatedScene.trailer = curateMedia(assets.trailer, { type: 'trailer', isRestricted: isVideoRestricted });
}
curatedScene.teaser = curateMedia(assets.teaser, { type: 'teaser', isRestricted: isVideoRestricted }); curatedScene.teaser = curateMedia(assets.teaser, { type: 'teaser', isRestricted: isVideoRestricted });
} }
@@ -214,7 +219,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
.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'),
tags: knex('releases_tags') tags: knex('releases_tags')
.select('tags.id', 'slug', 'name', 'priority', 'release_id') .select('tags.id', 'slug', 'name', 'priority', 'release_id', 'actor_id')
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id') .leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
.whereNotNull('tags.id') .whereNotNull('tags.id')
.whereIn('release_id', sceneIds) .whereIn('release_id', sceneIds)
@@ -397,6 +402,14 @@ function curateOptions(options) {
}; };
} }
function curateFacet(results, field, count = 'count(distinct id)') {
return results
.find((result) => result.columns[0][field] && result.columns[1][count])
?.data.map((row) => ({ key: row[field], doc_count: row[count] }))
.filter((row) => !!row.key)
|| [];
}
async function queryManticoreSql(filters, options, _reqUser) { async function queryManticoreSql(filters, options, _reqUser) {
const aggSize = config.database.manticore.maxAggregateSize; const aggSize = config.database.manticore.maxAggregateSize;
@@ -451,6 +464,13 @@ 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; probably not needed anyway (stashes only need global tags?)
builder
.leftJoin('scenes_tags', 'scenes_tags.scene_id', 'scenes_.id')
.groupBy('scenes.id');
*/
} }
if (filters.query) { if (filters.query) {
@@ -553,11 +573,17 @@ async function queryManticoreSql(filters, options, _reqUser) {
.limit(options.limit) .limit(options.limit)
.offset((options.page - 1) * options.limit), .offset((options.page - 1) * options.limit),
// 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 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 order by count(*) desc limit ?', [aggSize]) : null, actorsFacet: options.aggregateActors ? knex.raw('facet scenes.actor_ids as actors_facet distinct id order by count(distinct id) desc limit ?', [aggSize]) : null,
tagsFacet: options.aggregateTags ? knex.raw('facet scenes.tag_ids order by count(*) desc limit ?', [aggSize]) : null, // don't facet tags associated to other actors, actor ID 0 means global
channelsFacet: options.aggregateChannels ? knex.raw('facet scenes.channel_id order by count(*) desc limit ?', [aggSize]) : null, tagsFacet: options.aggregateTags ? knex.raw('facet scenes.tag_ids as tags_facet order by count(distinct id) desc limit ?', [aggSize]) : null,
studiosFacet: options.aggregateChannels ? knex.raw('facet scenes.studio_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(distinct id) desc limit ?`, [aggSize])
: null,
*/
channelsFacet: options.aggregateChannels ? knex.raw('facet scenes.channel_id as channels_facet distinct id order by count(distinct id) desc limit ?', [aggSize]) : null,
studiosFacet: options.aggregateChannels ? knex.raw('facet scenes.studio_id as studios_facet distinct id order by count(distinct id) desc limit ?', [aggSize]) : null,
maxMatches: config.database.manticore.maxMatches, maxMatches: config.database.manticore.maxMatches,
maxQueryTime: config.database.manticore.maxQueryTime, maxQueryTime: config.database.manticore.maxQueryTime,
}).toString(); }).toString();
@@ -565,7 +591,9 @@ async function queryManticoreSql(filters, options, _reqUser) {
// manticore does not seem to accept table.column syntax if 'table' is primary (yet?), crude work-around // manticore does not seem to accept table.column syntax if 'table' is primary (yet?), crude work-around
const curatedSqlQuery = filters.stashId const curatedSqlQuery = filters.stashId
? sqlQuery ? sqlQuery
: sqlQuery.replace(/scenes\./g, ''); : sqlQuery
.replace(/scenes\./g, '')
.replace(/scenes_\./g, 'scenes.');
if (process.env.NODE_ENV === 'development' && argv.debug) { if (process.env.NODE_ENV === 'development' && argv.debug) {
console.log(curatedSqlQuery); console.log(curatedSqlQuery);
@@ -575,32 +603,12 @@ async function queryManticoreSql(filters, options, _reqUser) {
// console.log(util.inspect(results, null, Infinity)); // console.log(util.inspect(results, null, Infinity));
const years = results const years = curateFacet(results, 'years_facet', 'count(*)');
.find((result) => (result.columns[0].years || result.columns[0]['scenes.years']) && result.columns[1]['count(*)']) const actorIds = curateFacet(results, 'actors_facet');
?.data.map((row) => ({ key: row.years || row['scenes.years'], doc_count: row['count(*)'] })) const tagIds = curateFacet(results, 'tags_facet');
|| []; const actorTagIds = curateFacet(results, 'actor_tags_facet');
const channelIds = curateFacet(results, 'channels_facet');
const actorIds = results const studioIds = curateFacet(results, 'studios_facet');
.find((result) => (result.columns[0].actor_ids || result.columns[0]['scenes.actor_ids']) && result.columns[1]['count(*)'])
?.data.map((row) => ({ key: row.actor_ids || row['scenes.actor_ids'], doc_count: row['count(*)'] }))
|| [];
const tagIds = results
.find((result) => (result.columns[0].tag_ids || result.columns[0]['scenes.tag_ids']) && result.columns[1]['count(*)'])
?.data.map((row) => ({ key: row.tag_ids || row['scenes.tag_ids'], doc_count: row['count(*)'] }))
|| [];
const channelIds = results
.find((result) => (result.columns[0].channel_id || result.columns[0]['scenes.channel_id']) && result.columns[1]['count(*)'])
?.data.map((row) => ({ key: row.channel_id || row['scenes.channel_id'], doc_count: row['count(*)'] }))
|| [];
const studioIds = results
.find((result) => (result.columns[0].studio_id || result.columns[0]['scenes.studio_id']) && result.columns[1]['count(*)'])
?.data
.map((row) => ({ key: row.studio_id || row['scenes.studio_id'], doc_count: row['count(*)'] }))
.filter((row) => !!row.key)
|| [];
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;
@@ -611,6 +619,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
years, years,
actorIds, actorIds,
tagIds, tagIds,
actorTagIds,
channelIds, channelIds,
studioIds, studioIds,
}, },
@@ -648,9 +657,10 @@ export async function fetchScenes(filters, rawOptions, reqUser, context) {
console.time('fetch aggregations'); console.time('fetch aggregations');
const [aggActors, aggTags, aggChannels] = await Promise.all([ const [aggActors, aggTags, aggActorTags, aggChannels] = await Promise.all([
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { shallow: true, order: ['slug', 'asc'], append: actorCounts }, reqUser) : [], options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { shallow: true, order: ['slug', 'asc'], append: actorCounts }, reqUser) : [],
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }, reqUser, context) : [], options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }, reqUser, context) : [],
options.aggregateTags ? fetchTagsById(result.aggregations.actorTagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }, reqUser, context) : [],
options.aggregateChannels ? fetchEntitiesById(entityIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }, reqUser, context) : [], options.aggregateChannels ? fetchEntitiesById(entityIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }, reqUser, context) : [],
]); ]);
@@ -666,6 +676,7 @@ export async function fetchScenes(filters, rawOptions, reqUser, context) {
aggYears, aggYears,
aggActors, aggActors,
aggTags, aggTags,
aggActorTags,
aggChannels, aggChannels,
total: result.total, total: result.total,
limit: options.limit, limit: options.limit,
@@ -780,9 +791,10 @@ async function applySceneTagsDelta(sceneId, delta, trx) {
if (delta.value.length > 0) { if (delta.value.length > 0) {
await knexOwner('releases_tags') await knexOwner('releases_tags')
.insert(delta.value.map((tagId) => ({ .insert(delta.value.map((tag) => ({
release_id: sceneId, release_id: sceneId,
tag_id: tagId, tag_id: tag.id,
actor_id: tag.actorId,
source: 'editor', source: 'editor',
}))) })))
.transacting(trx); .transacting(trx);

View File

@@ -1,82 +0,0 @@
import { indexApi, utilsApi } from '../manticore.js';
import rawvideos from './movies.json' with { type: 'json' };
async function fetchvideos() {
const videos = rawvideos
.filter((video) => video.cast.length > 0
&& video.genres.length > 0
&& video.cast.every((actor) => actor.charCodeAt(0) >= 65)) // throw out videos with non-alphanumerical actor names
.map((video, index) => ({ id: index + 1, ...video }));
const actors = Array.from(new Set(videos.flatMap((video) => video.cast))).sort();
const genres = Array.from(new Set(videos.flatMap((video) => video.genres)));
return {
videos,
actors,
genres,
};
}
async function init() {
await utilsApi.sql('drop table if exists videos');
await utilsApi.sql('drop table if exists videos_liked');
await utilsApi.sql(`create table videos (
id int,
title text,
actor_ids multi,
actors text,
genre_ids multi,
genres text
)`);
await utilsApi.sql(`create table videos_liked (
id int,
user_id int,
video_id int
)`);
const { videos, actors, genres } = await fetchvideos();
const likedvideoIds = Array.from(new Set(Array.from({ length: 10.000 }, () => videos[Math.round(Math.random() * videos.length)].id)));
const docs = videos
.map((video) => ({
replace: {
index: 'videos',
id: video.id,
doc: {
title: video.title,
actor_ids: video.cast.map((actor) => actors.indexOf(actor)),
actors: video.cast.join(','),
genre_ids: video.genres.map((genre) => genres.indexOf(genre)),
genres: video.genres.join(','),
},
},
}))
.concat(likedvideoIds.map((videoId, index) => ({
replace: {
index: 'videos_liked',
id: index + 1,
doc: {
user_id: Math.floor(Math.random() * 51),
video_id: videoId,
},
},
})));
const data = await indexApi.bulk(docs.map((doc) => JSON.stringify(doc)).join('\n'));
console.log('data', data);
const result = await utilsApi.sql(`
select * from videos_liked
limit 10
`);
console.log(result[0].data);
console.log(result[1]);
}
init();

View File

@@ -1,174 +0,0 @@
// import config from 'config';
import { format } from 'date-fns';
import { faker } from '@faker-js/faker';
import { indexApi } from '../manticore.js';
import { knexOwner as knex } from '../knex.js';
import slugify from '../utils/slugify.js';
import chunk from '../utils/chunk.js';
async function fetchScenes() {
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,
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
FROM releases
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 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
GROUP BY
releases.id,
releases.title,
releases.created_at,
releases.date,
releases.shoot_id,
scenes_meta.stashed,
entities.id,
entities.name,
entities.slug,
entities.alias,
parents.id,
parents.name,
parents.slug,
parents.alias;
`);
const actors = Object.fromEntries(scenes.rows.flatMap((row) => row.actors.map((actor) => [actor.f1, faker.person.fullName()])));
const tags = Object.fromEntries(scenes.rows.flatMap((row) => row.tags.map((tag) => [tag.f1, faker.word.adjective()])));
return scenes.rows.map((row) => {
const title = faker.lorem.lines(1);
const channelName = faker.company.name();
const channelSlug = slugify(channelName, '');
const networkName = faker.company.name();
const networkSlug = slugify(networkName, '');
const rowActors = row.actors.map((actor) => ({ f1: actor.f1, f2: actors[actor.f1] }));
const rowTags = row.tags.map((tag) => ({ f1: tag.f1, f2: tags[tag.f1], f3: tag.f3 }));
return {
...row,
title,
actors: rowActors,
tags: rowTags,
channel_name: channelName,
channel_slug: channelSlug,
network_name: networkName,
network_slug: networkSlug,
};
});
}
async function updateStashed(docs) {
await chunk(docs, 1000).reduce(async (chain, docsChunk) => {
await chain;
const sceneIds = docsChunk.map((doc) => doc.replace.id);
const stashes = await knex('stashes_scenes')
.select('stashes_scenes.id as stashed_id', 'stashes_scenes.scene_id', 'stashes.id as stash_id', 'stashes.user_id as user_id')
.leftJoin('stashes', 'stashes.id', 'stashes_scenes.stash_id')
.whereIn('scene_id', sceneIds);
if (stashes.length > 0) {
console.log(stashes);
}
const stashDocs = docsChunk.flatMap((doc) => {
const sceneStashes = stashes.filter((stash) => stash.scene_id === doc.replace.id);
if (sceneStashes.length === 0) {
return [];
}
const stashDoc = sceneStashes.map((stash) => ({
replace: {
index: 'scenes_stashed',
id: stash.stashed_id,
doc: {
// ...doc.replace.doc,
scene_id: doc.replace.id,
user_id: stash.user_id,
},
},
}));
return stashDoc;
});
console.log(stashDocs);
if (stashDocs.length > 0) {
await indexApi.bulk(stashDocs.map((doc) => JSON.stringify(doc)).join('\n'));
}
}, Promise.resolve());
}
async function init() {
const scenes = await fetchScenes();
const docs = scenes.map((scene) => {
const flatActors = scene.actors.flatMap((actor) => actor.f2.match(/[\w']+/g)); // match word characters to filter out brackets etc.
const flatTags = scene.tags.filter((tag) => tag.f3 > 6).flatMap((tag) => (tag.f4 ? `${tag.f2} ${tag.f4}` : tag.f2).match(/[\w']+/g)); // only make top tags searchable to minimize cluttered results
const filteredTitle = scene.title && [...flatActors, ...flatTags].reduce((accTitle, tag) => accTitle.replace(new RegExp(tag.replace(/[^\w\s]+/g, ''), 'i'), ''), scene.title).trim().replace(/\s{2,}/, ' ');
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),
// 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,
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(' '),
meta: scene.date ? format(scene.date, 'y yy M MMM MMMM d') : undefined,
liked: scene.stashed || 0,
},
},
};
});
const data = await indexApi.bulk(docs.map((doc) => JSON.stringify(doc)).join('\n'));
await updateStashed(docs);
console.log('data', data);
knex.destroy();
}
init();

View File

@@ -1,50 +0,0 @@
import { indexApi, utilsApi } from '../manticore.js';
import { knexOwner as knex } from '../knex.js';
import chunk from '../utils/chunk.js';
async function syncStashes(domain = 'scene') {
await utilsApi.sql(`truncate table ${domain}s_stashed`);
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`,
)
.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'));
console.log(`Synced ${index * 1000 + stashChunk.length}/${stashes.length} ${domain} stashes`);
}, Promise.resolve());
}
async function init() {
await syncStashes('scene');
await syncStashes('actor');
await syncStashes('movie');
console.log('Done!');
knex.destroy();
}
init();

View File

@@ -11,7 +11,7 @@ export default async function initRestrictionHandler() {
const reader = await Reader.open('assets/GeoLite2-City.mmdb'); const reader = await Reader.open('assets/GeoLite2-City.mmdb');
function getRestriction(req) { function getRestriction(req) {
if (req.session.restriction && req.session.country && req.session.restrictionIp === req.userIp) { if (Object.hasOwn(req.session, 'restriction') && Object.hasOwn(req.session, 'country') && req.session.restrictionIp === req.userIp) {
return { return {
restriction: req.session.restriction, restriction: req.session.restriction,
country: req.session.country, country: req.session.country,
@@ -71,6 +71,10 @@ export default async function initRestrictionHandler() {
req.country = country; req.country = country;
} catch (error) { } catch (error) {
logger.error(`Failed Maxmind IP lookup for ${req.ip}: ${error.message}`); logger.error(`Failed Maxmind IP lookup for ${req.ip}: ${error.message}`);
req.session.restrictionIp = req.userIp;
req.session.restriction = 0;
req.session.country = null;
} }
next(); next();

View File

@@ -59,6 +59,7 @@ async function fetchScenesApi(req, res) {
aggYears, aggYears,
aggActors, aggActors,
aggTags, aggTags,
aggActorTags,
aggChannels, aggChannels,
limit, limit,
total, total,
@@ -77,6 +78,7 @@ async function fetchScenesApi(req, res) {
aggYears, aggYears,
aggActors, aggActors,
aggTags, aggTags,
aggActorTags,
aggChannels, aggChannels,
limit, limit,
total, total,

View File

@@ -41,6 +41,8 @@ import { router as userRouter } from './users.js';
import { router as stashesRouter } from './stashes.js'; import { router as stashesRouter } from './stashes.js';
import { router as alertsRouter } from './alerts.js'; import { router as alertsRouter } from './alerts.js';
import { initCachesApi } from './system.js';
import initLogger from '../logger.js'; import initLogger from '../logger.js';
const logger = initLogger(); const logger = initLogger();
@@ -158,6 +160,8 @@ export default async function initServer() {
// TAGS // TAGS
router.get('/api/tags', fetchTagsApi); router.get('/api/tags', fetchTagsApi);
router.post('/api/caches', initCachesApi);
if (config.apiAccess.graphqlEnabled) { if (config.apiAccess.graphqlEnabled) {
router.post('/graphql', graphqlApi); router.post('/graphql', graphqlApi);
} }

12
src/web/system.js Normal file
View File

@@ -0,0 +1,12 @@
import { HttpError } from '../errors.js';
import { initCaches } from '../cache.js';
export async function initCachesApi(req, res) {
if (req.user?.role !== 'admin') {
throw new HttpError('You must be an admin to initialize caches', 404);
}
await initCaches();
res.status(204).send();
}

2
static

Submodule static updated: 4340a4799a...217845ef37