Compare commits

...

25 Commits

Author SHA1 Message Date
35ffc2b0f7 0.49.7 2026-05-06 17:57:36 +02:00
383844dda8 Not showing parent campaigns on independent sites. 2026-05-06 17:57:33 +02:00
77fb6595a2 Added inauthenticated user handling to createSceneRevision. 2026-04-01 00:05:30 +02:00
aa3adbe634 Changed bio location phrasing for deceased actors. 2026-03-30 22:15:40 +02:00
59a700c2f3 0.49.6 2026-03-30 17:00:19 +02:00
18f5a6f476 Fixed age of passing in actor tile. 2026-03-30 17:00:17 +02:00
63a178ca57 0.49.5 2026-03-28 16:29:59 +01:00
0ae949a616 Enabled extreme insertion tag filter by default. 2026-03-28 16:29:57 +01:00
edc9720623 Added RTA logo file to repo. 2026-03-27 05:44:15 +01:00
bbc3fbb0a5 0.49.4 2026-03-27 04:26:19 +01:00
1fc468efac Added RTA Restrict To Adults marker. 2026-03-27 04:26:16 +01:00
143c415797 0.49.3 2026-03-23 17:26:25 +01:00
e79a4d48e1 Removed handle from bio socials to save space. 2026-03-23 17:26:23 +01:00
343325440e 0.49.2 2026-03-22 06:24:07 +01:00
5c018892d3 Fixed revision tag modified highlight for actor association change. 2026-03-22 06:24:05 +01:00
be61293cbe 0.49.1 2026-03-22 06:14:46 +01:00
e493194ce1 Added scene revision tag fix tool. 2026-03-22 06:14:41 +01:00
b61631c33c Fixed scene actor tag revision display. 2026-03-22 05:53:13 +01:00
fa65da75bc Fixed scene tag delta storage format for actor associations. 2026-03-22 05:39:22 +01:00
a4468f18dc 0.49.0 2026-03-22 04:50:51 +01:00
fea28b71ba Restored actor tag filtering with performance fixes. 2026-03-22 04:50:35 +01:00
884ad891f3 0.48.2 2026-03-22 02:09:22 +01:00
058161f798 Patched manticore death queries. 2026-03-22 02:09:20 +01:00
aa68748817 0.48.1 2026-03-20 01:42:02 +01:00
928857596f Fixed fallback watch URL not generated for scenes without URL or network. 2026-03-20 01:42:00 +01:00
17 changed files with 228 additions and 67 deletions

View File

@@ -19,6 +19,9 @@
<meta name="viewport" content="width=device-width,height=device-height,initial-scale=1.0,interactive-widget=resizes-content" /> <meta name="viewport" content="width=device-width,height=device-height,initial-scale=1.0,interactive-widget=resizes-content" />
<!-- RTA restricted to adults label -->
<meta name="RATING" content="RTA-5042-1996-1400-1577-RTA" />
<title>traxxx - Consent</title> <title>traxxx - Consent</title>
<style> <style>
:root { :root {
@@ -156,6 +159,12 @@
text-decoration: underline; text-decoration: underline;
} }
.rta {
position: fixed;
bottom: .5rem;
right: .5rem;
}
@media(max-width: 800px) { @media(max-width: 800px) {
.heading { .heading {
font-size: 1.25rem; font-size: 1.25rem;
@@ -219,5 +228,12 @@
</a> </a>
</div> </div>
</div> </div>
<img
src="/img/rta.gif"
alt="RTA Restricted To Adults"
title="RTA Restricted To Adults"
class="rta"
>
</body> </body>
</html> </html>

2
common

Submodule common updated: ec4b15ce33...e4d6ff6ad1

View File

@@ -105,7 +105,7 @@
class="bio-item residence" class="bio-item residence"
:class="{ hideable: !!actor.origin }" :class="{ hideable: !!actor.origin }"
> >
<dfn class="bio-label"><Icon icon="location" />Lives in</dfn> <dfn class="bio-label"><Icon icon="location" />{{ actor.dateOfDeath ? 'Lived' : 'Lives' }} in</dfn>
<span> <span>
<span <span
@@ -312,10 +312,10 @@
<a <a
v-for="social in socials" v-for="social in socials"
:key="`social-${social.id}`" :key="`social-${social.id}`"
v-tooltip="social.platform ? `${social.platform} ${env.socials.prefix[social.platform] || env.socials.prefix.default}${social.handle}` : social.url"
:href="getSocialUrl(social)" :href="getSocialUrl(social)"
target="_blank" target="_blank"
rel="noopener" rel="noopener"
:title="social.platform || social.url"
class="social ellipsis" class="social ellipsis"
> >
<Icon <Icon
@@ -339,7 +339,9 @@
:class="`icon-social icon-${social.platform} icon-generic`" :class="`icon-social icon-${social.platform} icon-generic`"
/> />
<!--
<template v-if="social.platform">{{ env.socials.prefix[social.platform] || env.socials.prefix.default }}</template>{{ social.handle }} <template v-if="social.platform">{{ env.socials.prefix[social.platform] || env.socials.prefix.default }}</template>{{ social.handle }}
-->
</a> </a>
</ul> </ul>
</div> </div>
@@ -498,7 +500,7 @@ function getSocialUrl(social) {
return null; return null;
} }
const socials = props.actor.socials.map((social) => ({ const socials = props.actor.socials.slice(0, 10).map((social) => ({
...social, ...social,
handle: social.url handle: social.url
? new URL(social.url).hostname ? new URL(social.url).hostname
@@ -735,19 +737,23 @@ const socials = props.actor.socials.map((social) => ({
} }
.socials { .socials {
display: flex;
flex-wrap: wrap;
/*
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
grid-gap: 0 0; grid-gap: 0 0;
overflow: hidden; overflow: hidden;
*/
gap: .25rem; gap: .25rem;
padding: 0; padding: 0;
} }
.social { .social {
display: flex; display: inline-flex;
height: 2rem;
align-items: center; align-items: center;
padding: .1rem .5rem; justify-content: center;
padding: .75rem .75rem;
border-radius: .25rem; border-radius: .25rem;
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
@@ -755,10 +761,6 @@ const socials = props.actor.socials.map((social) => ({
font-weight: normal; font-weight: normal;
background: var(--highlight-weak-40); background: var(--highlight-weak-40);
.icon {
margin-right: .5rem;
}
.icon-generic { .icon-generic {
fill: var(--highlight); fill: var(--highlight);
} }

View File

@@ -41,7 +41,7 @@
<span <span
v-if="actor.ageAtDeath" v-if="actor.ageAtDeath"
:title="`Passed ${formatDate(actor.ageAtDeath, 'MMMM d, yyyy')}`" :title="`Passed ${formatDate(actor.dateOfDeath, 'MMMM d, yyyy')}`"
class="age age-death" class="age age-death"
>{{ actor.ageAtDeath }}</span> >{{ actor.ageAtDeath }}</span>

View File

@@ -266,7 +266,7 @@ const expanded = ref(new Set());
const mappedKeys = { const mappedKeys = {
actors: actorsById, actors: actorsById,
tags: tagsById, // tags: tagsById,
movies: moviesById, movies: moviesById,
}; };
@@ -292,6 +292,16 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
}))]; }))];
} }
if (key === 'tags') {
return [key, value.map((tag) => ({
id: tag.id,
name: tag.actorId
? `${actorsById.value[tag.actorId]?.name}: ${tagsById.value[tag.id]?.name}`
: tagsById.value[tag.id]?.name,
modified: revision.deltas.some((delta) => delta.key === key && !delta.value.some((deltaTag) => deltaTag.id === tag.id && (!Object.hasOwn(tag, 'actorId') || deltaTag.actorId === tag.actorId))),
}))];
}
if (key === 'socials') { if (key === 'socials') {
// new socials don't have IDs yet, so we need to compare the values // new socials don't have IDs yet, so we need to compare the values
return [key, value.map((item) => ({ return [key, value.map((item) => ({
@@ -323,6 +333,19 @@ const curatedRevisions = computed(() => revisions.value.map((revision) => {
}; };
} }
if (delta.key === 'tags') {
return {
...delta,
value: delta.value.map((tag) => ({
id: tag.id,
name: tag.actorId
? `${actorsById.value[tag.actorId]?.name}: ${tagsById.value[tag.id]?.name}`
: tagsById.value[tag.id]?.name,
modified: !revision.base[delta.key].some((baseTag) => baseTag.id === tag.id && (!Object.hasOwn(baseTag, 'actorId') || baseTag.actorId === tag.actorId)),
})),
};
}
if (delta.key === 'socials') { if (delta.key === 'socials') {
// new socials don't have IDs yet, so we need to compare the values // new socials don't have IDs yet, so we need to compare the values
return { return {

View File

@@ -12,25 +12,25 @@
<Icon icon="search" /> <Icon icon="search" />
</label> </label>
<!-- <template v-if="isActorTagsAvailable">
<div <div
v-show="showActorTags" v-show="showActorTags"
v-tooltip="'Tags relevant to the selected actors'" v-tooltip="'Tags relevant to the selected actors'"
class="filter-sort order noselect" class="filter-sort order noselect"
@click="showActorTags = false" @click="showActorTags = false"
> >
<Icon icon="user-tags" /> <Icon icon="user-tags" />
</div> </div>
<div <div
v-show="!showActorTags" v-show="!showActorTags"
v-tooltip="'All tags'" v-tooltip="'All tags'"
class="filter-sort order noselect" class="filter-sort order noselect"
@click="showActorTags = true" @click="showActorTags = true"
> >
<Icon icon="price-tags" /> <Icon icon="price-tags" />
</div> </div>
--> </template>
<div <div
v-show="order === 'priority'" v-show="order === 'priority'"
@@ -142,13 +142,17 @@ const props = defineProps({
const emit = defineEmits(['update']); const emit = defineEmits(['update']);
const { pageProps } = inject('pageContext'); const { pageProps } = inject('pageContext');
// const { tag: pageTag, actor: pageActor } = pageProps;
const { tag: pageTag } = pageProps; const {
tag: pageTag,
actor: pageActor,
stash: pageStash,
} = 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 showActorTags = ref(!!pageActor);
const priorityTags = [ const priorityTags = [
'anal', 'anal',
@@ -173,13 +177,13 @@ const priorityTags = [
'lesbian', 'lesbian',
]; ];
const isActorTagsAvailable = computed(() => props.actorTags && (props.filters.actors.length > 0 || pageActor) && !pageStash);
const groupedTags = computed(() => { const groupedTags = computed(() => {
/* // can't show actor tags inside stash, because both require a join, and manticore currently only supports one
const tags = showActorTags.value && props.actorTags && (props.filters.actors.length > 0 || pageActor) const tags = showActorTags.value && isActorTagsAvailable.value
? props.actorTags ? props.actorTags
: props.tags; : props.tags;
*/
const tags = props.tags;
const selected = tags.filter((tag) => props.filters.tags.includes(tag.slug)); const selected = tags.filter((tag) => props.filters.tags.includes(tag.slug));
const filtered = tags.filter((tag) => !props.filters.tags.includes(tag.slug) const filtered = tags.filter((tag) => !props.filters.tags.includes(tag.slug)

4
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{ {
"name": "traxxx-web", "name": "traxxx-web",
"version": "0.48.0", "version": "0.49.7",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"version": "0.48.0", "version": "0.49.7",
"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.48.0", "version": "0.49.7",
"imports": { "imports": {
"#/*": "./*.js" "#/*": "./*.js"
} }

View File

@@ -57,19 +57,23 @@ export async function onBeforeRender(pageContext) {
fetchReleases(pageContext, entityId), fetchReleases(pageContext, entityId),
]); ]);
const entityIds = entity.isIndependent || !entity.parent
? [entity.id]
: [entity.id, entity.parent.id];
const campaigns = await getRandomCampaigns([ const campaigns = await getRandomCampaigns([
{ {
entityIds: [entity.id, entity.parent?.id].filter(Boolean), entityIds,
minRatio: 3, minRatio: 3,
allowRandomFallback: false, allowRandomFallback: false,
}, },
{ {
entityIds: [entity.id, entity.parent?.id].filter(Boolean), entityIds,
minRatio: 3, minRatio: 3,
allowRandomFallback: false, allowRandomFallback: false,
}, },
pageContext.routeParams.domain === 'scenes' ? { pageContext.routeParams.domain === 'scenes' ? {
entityIds: [entity.id, entity.parent?.id].filter(Boolean), entityIds,
minRatio: 0.75, minRatio: 0.75,
maxRatio: 1.25, maxRatio: 1.25,
allowRandomFallback: false, allowRandomFallback: false,

BIN
public/img/rta.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -70,6 +70,9 @@ async function onRenderHtml(pageContext) {
<meta name="description" content="Keep track of new porn releases and re-discover classics from your favorite porn stars and sites" /> <meta name="description" content="Keep track of new porn releases and re-discover classics from your favorite porn stars and sites" />
<!-- RTA restricted to adults label -->
<meta name="RATING" content="RTA-5042-1996-1400-1577-RTA" />
${config.analytics.enabled ? dangerouslySkipEscape(`<script src="${config.analytics.address}" data-website-id="${config.analytics.siteId}" data-exclude-hash="true" async></script>`) : ''} ${config.analytics.enabled ? dangerouslySkipEscape(`<script src="${config.analytics.address}" data-website-id="${config.analytics.siteId}" data-exclude-hash="true" async></script>`) : ''}
<title>${title}</title> <title>${title}</title>

View File

@@ -55,7 +55,7 @@ const keyMap = {
isCircumcised: 'circumcised', isCircumcised: 'circumcised',
}; };
const socialsOrder = ['onlyfans', 'twitter', 'fansly', 'loyalfans', 'manyvids', 'pornhub', 'linktree', null]; const socialsOrder = ['onlyfans', 'fansly', 'twitter', 'instagram', 'loyalfans', 'manyvids', 'pornhub', 'linktree', null];
export function curateActor(actor, context = {}) { export function curateActor(actor, context = {}) {
return { return {
@@ -80,6 +80,8 @@ export function curateActor(actor, context = {}) {
ageThen: context.sceneDate && actor.date_of_birth && actor.date_of_birth.getFullYear() > 1 ageThen: context.sceneDate && actor.date_of_birth && actor.date_of_birth.getFullYear() > 1
? differenceInYears(context.sceneDate, actor.date_of_birth) ? differenceInYears(context.sceneDate, actor.date_of_birth)
: null, : null,
dateOfBirth: actor.date_of_birth,
dateOfDeath: actor.date_of_death,
bust: actor.bust, bust: actor.bust,
cup: actor.cup, cup: actor.cup,
waist: actor.waist, waist: actor.waist,

View File

@@ -6,7 +6,7 @@ function getWatchUrl(scene) {
return new URL(scene.url).href; return new URL(scene.url).href;
} }
if (scene.channel && (scene.channel.isIndependent || scene.channel.type === 'network')) { if (scene.channel && (scene.channel.isIndependent || scene.channel.type === 'network' || !scene.network)) {
return new URL(scene.channel.url).href; return new URL(scene.channel.url).href;
} }

View File

@@ -45,6 +45,7 @@ function curateScene(rawScene, assets, reqUser, context) {
slug: assets.channel.slug, slug: assets.channel.slug,
name: censor(assets.channel.name, context.restriction), name: censor(assets.channel.name, context.restriction),
type: assets.channel.type, type: assets.channel.type,
url: assets.channel.url,
isIndependent: assets.channel.independent, isIndependent: assets.channel.independent,
hasLogo: assets.channel.has_logo, hasLogo: assets.channel.has_logo,
}, },
@@ -52,6 +53,7 @@ function curateScene(rawScene, assets, reqUser, context) {
id: assets.channel.network_id, id: assets.channel.network_id,
slug: assets.channel.network_slug, slug: assets.channel.network_slug,
name: censor(assets.channel.network_name, context.restriction), name: censor(assets.channel.network_name, context.restriction),
url: assets.network_url,
type: assets.channel.network_type, type: assets.channel.network_type,
hasLogo: assets.channel.network_has_logo, hasLogo: assets.channel.network_has_logo,
} : null, } : null,
@@ -179,6 +181,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
'networks.slug as network_slug', 'networks.slug as network_slug',
'networks.name as network_name', 'networks.name as network_name',
'networks.type as network_type', 'networks.type as network_type',
'networks.url as network_url',
'networks.has_logo as network_has_logo', 'networks.has_logo as network_has_logo',
knex.raw('row_to_json(affiliates) as affiliate'), knex.raw('row_to_json(affiliates) as affiliate'),
) )
@@ -402,10 +405,11 @@ function curateOptions(options) {
}; };
} }
function curateFacet(results, field, count = 'count(distinct id)') { // function curateFacet(results, field, count = 'count(distinct id)') {
function curateFacet(results, field) {
return results return results
.find((result) => result.columns[0][field] && result.columns[1][count]) .find((result) => result.columns[0][field] && (result.columns[1]['count(distinct id)'] || result.columns[1]['count(*)']))
?.data.map((row) => ({ key: row[field], doc_count: row[count] })) ?.data.map((row) => ({ key: row[field], doc_count: row['count(distinct id)'] || row['count(*)'] }))
.filter((row) => !!row.key) .filter((row) => !!row.key)
|| []; || [];
} }
@@ -432,6 +436,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
:yearsFacet: :yearsFacet:
:actorsFacet: :actorsFacet:
:tagsFacet: :tagsFacet:
:actorTagsFacet:
:channelsFacet: :channelsFacet:
:studiosFacet:; :studiosFacet:;
show meta; show meta;
@@ -465,15 +470,14 @@ async function queryManticoreSql(filters, options, _reqUser) {
weight() as _score weight() as _score
`)); `));
/* // manticore only supports one joined table, so we can't use it inside stashes
// manticore only supports one joined table, so we can't use it inside stashes; probably not needed anyway (stashes only need global tags?)
builder builder
.leftJoin('scenes_tags', 'scenes_tags.scene_id', 'scenes_.id') .leftJoin('scenes_tags', 'scenes_tags.scene_id', 'scenes_.id')
.groupBy('scenes.id'); .groupBy('scenes.id');
*/
} }
if (filters.query) { if (filters.query) {
// we exclude title because we have a curated title_filtered field for more effective results
builder.whereRaw('match(\'@!title :query:\', scenes)', { query: escape(filters.query) }); builder.whereRaw('match(\'@!title :query:\', scenes)', { query: escape(filters.query) });
} }
@@ -574,16 +578,14 @@ async function queryManticoreSql(filters, options, _reqUser) {
.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_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(distinct id) 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 // don't facet tags associated to other actors, actor ID 0 means global
tagsFacet: options.aggregateTags ? knex.raw('facet scenes.tag_ids as tags_facet order by count(distinct id) 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 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]) ? 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, : 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(distinct id) 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(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();
@@ -601,9 +603,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
const results = await utilsApi.sql(curatedSqlQuery); const results = await utilsApi.sql(curatedSqlQuery);
// console.log(util.inspect(results, null, Infinity)); const years = curateFacet(results, 'years_facet');
const years = curateFacet(results, 'years_facet', 'count(*)');
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 actorTagIds = curateFacet(results, 'actor_tags_facet');
@@ -729,8 +729,14 @@ export async function fetchSceneRevisions(revisionId, filters = {}, reqUser) {
.limit(limit) .limit(limit)
.offset((page - 1) * limit); .offset((page - 1) * limit);
const actorIds = Array.from(new Set(revisions.flatMap((revision) => [...revision.base.actors, ...(revision.deltas.find((delta) => delta.key === 'actors')?.value || [])]))); const actorIds = Array.from(new Set(revisions.flatMap((revision) => [
const tagIds = Array.from(new Set(revisions.flatMap((revision) => [...revision.base.tags, ...(revision.deltas.find((delta) => delta.key === 'tags')?.value || [])]))); ...revision.base.actors,
...(revision.deltas.find((delta) => delta.key === 'actors')?.value || []),
...revision.base.tags.map((tag) => tag.actorId),
...revision.deltas.find((delta) => delta.key === 'tags')?.value.map((tag) => tag.actorId) || [],
].filter(Boolean))));
const tagIds = Array.from(new Set(revisions.flatMap((revision) => [...revision.base.tags, ...(revision.deltas.find((delta) => delta.key === 'tags')?.value || [])].map((tag) => tag.id))));
const movieIds = Array.from(new Set(revisions.flatMap((revision) => [...revision.base.movies, ...(revision.deltas.find((delta) => delta.key === 'movies')?.value || [])]))); const movieIds = Array.from(new Set(revisions.flatMap((revision) => [...revision.base.movies, ...(revision.deltas.find((delta) => delta.key === 'movies')?.value || [])])));
const [actors, tags, movies] = await Promise.all([ const [actors, tags, movies] = await Promise.all([
@@ -926,6 +932,10 @@ export async function reviewSceneRevision(revisionId, isApproved, { feedback },
} }
export async function createSceneRevision(sceneId, { edits, comment, apply }, reqUser) { export async function createSceneRevision(sceneId, { edits, comment, apply }, reqUser) {
if (!reqUser) {
throw new HttpError('Must be authenticated to create scene revision', 401);
}
const [ const [
[scene], [scene],
openRevisions, openRevisions,
@@ -967,6 +977,13 @@ export async function createSceneRevision(sceneId, { edits, comment, apply }, re
return [key, values.id]; return [key, values.id];
} }
if (key === 'tags') {
return [key, values.map((tag) => ({
id: tag.id,
actorId: tag.actorId,
}))];
}
if (Array.isArray(values)) { if (Array.isArray(values)) {
return [key, values.map((value) => value?.hash || value?.id || value)]; return [key, values.map((value) => value?.hash || value?.id || value)];
} }
@@ -983,6 +1000,16 @@ export async function createSceneRevision(sceneId, { edits, comment, apply }, re
return null; return null;
} }
if (key === 'tags') {
return {
key,
value: value.map((tag) => ({
id: tag.id,
actorId: tag.actorId,
})),
};
}
if (Array.isArray(value)) { if (Array.isArray(value)) {
const valueSet = new Set(value); const valueSet = new Set(value);
const baseSet = new Set(baseScene[key]); const baseSet = new Set(baseScene[key]);

View File

@@ -0,0 +1,80 @@
import { MerkleJson } from 'merkle-json';
import knex from '../knex.js';
const mj = new MerkleJson();
function curateTag(tag) {
if (Object.hasOwn(tag, 'actorId')) {
return {
id: tag.id,
actorId: tag.actorId,
};
}
if (typeof tag === 'number') {
return {
id: tag,
// can't restore actorId, don't set to null to hint at missing data
};
}
throw new Error(`Unrecognized tag delta: ${JSON.stringify(tag)}`);
}
async function init() {
const revisions = await knex('scenes_revisions');
// console.log(revisions);
const fixedRevisions = revisions.map((revision) => {
if (revision.base.tags.length === 0 && !revision.deltas.some((delta) => delta.key === 'tags')) {
return null;
}
const newDeltas = revision.deltas.map((delta) => {
if (delta.key !== 'tags') {
return delta;
}
return {
...delta,
value: delta.value.map((tag) => curateTag(tag)),
};
});
const newBase = {
...revision.base,
tags: revision.base.tags.map((tag) => curateTag(tag)),
};
return {
...revision,
deltas: newDeltas,
base: newBase,
};
}).filter(Boolean);
const entries = fixedRevisions.map((revision) => ({
id: revision.id,
base: JSON.stringify(revision.base),
deltas: JSON.stringify(revision.deltas),
hash: mj.hash({
base: revision.base,
deltas: revision.deltas,
}),
}));
console.log(entries);
await knex('scenes_revisions')
.insert(entries)
.onConflict('id')
.merge(['base', 'deltas', 'hash']);
console.log(`Fixed ${entries.length} revisions`);
await knex.destroy();
}
init();

View File

@@ -2,14 +2,14 @@ export default function consentHandler(req, res, next) {
const redirect = req.headers.referer && new URL(req.headers.referer).searchParams.get('redirect'); const redirect = req.headers.referer && new URL(req.headers.referer).searchParams.get('redirect');
if (Object.hasOwn(req.query, 'lgbt')) { if (Object.hasOwn(req.query, 'lgbt')) {
const lgbtFilters = (req.tagFilter || []).filter((tag) => !['gay', 'bisexual', 'transsexual'].includes(tag)); const lgbtFilters = Array.from(new Set([...(req.tagFilter || []).filter((tag) => !['gay', 'bisexual', 'transsexual'].includes(tag)), 'extreme-insertion']));
req.tagFilter = lgbtFilters; // eslint-disable-line no-param-reassign req.tagFilter = lgbtFilters; // eslint-disable-line no-param-reassign
res.cookie('tags', JSON.stringify(lgbtFilters)); res.cookie('tags', JSON.stringify(lgbtFilters));
} }
if (Object.hasOwn(req.query, 'straight')) { if (Object.hasOwn(req.query, 'straight')) {
const straightFilters = Array.from(new Set([...(req.tagFilter || []), 'gay', 'bisexual', 'transsexual'])); const straightFilters = Array.from(new Set([...(req.tagFilter || []), 'gay', 'bisexual', 'transsexual', 'extreme-insertion']));
req.tagFilter = straightFilters; // eslint-disable-line no-param-reassign req.tagFilter = straightFilters; // eslint-disable-line no-param-reassign
res.cookie('tags', JSON.stringify(straightFilters)); res.cookie('tags', JSON.stringify(straightFilters));

2
static

Submodule static updated: d9fd99c504...d77e9faeb9