Compare commits

...

2 Commits

17 changed files with 196 additions and 73 deletions

View File

@ -0,0 +1,107 @@
<template>
<div class="years-container">
<select
v-model="selected"
class="years nobar"
multiple
@change="updateYears"
>
<option
v-for="year in years"
:key="`year-${year.year}`"
class="year"
:value="year.year"
>{{ year.year }}</option>
</select>
<Icon
v-show="selected.length > 0"
icon="cross2"
class="clear"
@click="clearYears"
/>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
filters: {
type: Object,
default: null,
},
years: {
type: Array,
default: null,
},
});
const emit = defineEmits(['update']);
const selected = ref(props.filters.years);
function updateYears() {
emit('update', 'years', selected.value);
}
function clearYears() {
selected.value = [];
emit('update', 'years', selected.value);
}
</script>
<style scoped>
.years-container {
display: flex;
align-items: stretch;
border-bottom: solid 1px var(--shadow-weak-30);
}
.years {
height: 2.5rem;
padding: .5rem;
flex-grow: 1;
border: none;
overflow-x: auto;
overflow-y: hidden;
background: none;
&:focus .year:checked {
background: var(--primary) linear-gradient(0deg, var(--primary) 0%, var(--primary) 100%);
}
&:focus {
outline: none;
}
}
.year {
display: inline-block;
padding: .25rem;
font-size: .8rem;
&:hover {
color: var(--primary);
cursor: pointer;
}
&:checked {
color: var(--primary);
background: none;
font-weight: bold;
}
}
.clear {
height: auto;
display: flex;
align-items: center;
padding: 0 .5rem;
fill: var(--shadow);
&:hover {
fill: var(--primary);
cursor: pointer;
}
}
</style>

View File

@ -11,11 +11,11 @@
><Icon icon="discord" /></a>
<a
v-if="env.links.dmca"
:href="env.links.dmca"
v-if="env.links.content"
:href="env.links.content"
target="_blank"
class="footer-segment footer-link"
>dmca</a>
>Content Removal / DMCA</a>
</footer>
</template>

View File

@ -14,6 +14,12 @@
>
</div>
<YearsFilter
:filters="filters"
:years="aggYears"
@update="updateFilter"
/>
<TagsFilter
:filters="filters"
:tags="aggTags"
@ -90,6 +96,7 @@ import events from '#/src/events.js';
import MovieTile from '#/components/movies/tile.vue';
import Filters from '#/components/filters/filters.vue';
import YearsFilter from '#/components/filters/years.vue';
import ActorsFilter from '#/components/filters/actors.vue';
import TagsFilter from '#/components/filters/tags.vue';
import ChannelsFilter from '#/components/filters/channels.vue';
@ -106,6 +113,7 @@ const {
} = pageProps;
const movies = ref(pageProps.movies);
const aggYears = ref(pageProps.aggYears || []);
const aggActors = ref(pageProps.aggActors || []);
const aggTags = ref(pageProps.aggTags || []);
const aggChannels = ref(pageProps.aggChannels || []);
@ -127,6 +135,7 @@ const queryEntity = networks[urlParsed.search.e] || channels[urlParsed.search.e]
const filters = ref({
search: urlParsed.search.q,
years: urlParsed.search.years?.split(',').map((year) => Number(year)).filter(Boolean) || [],
tags: urlParsed.search.tags?.split(',').filter(Boolean) || [],
entity: queryEntity,
actors: queryActors,
@ -178,6 +187,7 @@ async function search(options = {}) {
navigate(getPath(scope.value, false), {
...query,
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
tags: filters.value.tags.join(',') || undefined,
e: filters.value.entity?.type === 'network' ? `_${filters.value.entity.slug}` : (filters.value.entity?.slug || undefined),
@ -185,6 +195,7 @@ async function search(options = {}) {
const res = await get('/movies', {
...query,
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
tags: [pageTag?.slug, ...filters.value.tags].filter(Boolean).join(','),
e: entitySlug,
@ -194,6 +205,7 @@ async function search(options = {}) {
});
movies.value = res.movies;
aggYears.value = res.aggYears;
aggActors.value = res.aggActors;
aggTags.value = res.aggTags;
aggChannels.value = res.aggChannels;

View File

@ -24,6 +24,12 @@
/>
</div>
<YearsFilter
:filters="filters"
:years="aggYears"
@update="updateFilter"
/>
<TagsFilter
:filters="filters"
:tags="aggTags"
@ -157,6 +163,7 @@ import events from '#/src/events.js';
import { getActorIdentifier, parseActorIdentifier } from '#/src/query.js';
import Filters from '#/components/filters/filters.vue';
import YearsFilter from '#/components/filters/years.vue';
import ActorsFilter from '#/components/filters/actors.vue';
import TagsFilter from '#/components/filters/tags.vue';
import ChannelsFilter from '#/components/filters/channels.vue';
@ -199,6 +206,7 @@ const {
} = pageProps;
const scenes = ref(pageProps.scenes);
const aggYears = ref(pageProps.aggYears || []);
const aggActors = ref(pageProps.aggActors || []);
const aggTags = ref(pageProps.aggTags || []);
const aggChannels = ref(pageProps.aggChannels || []);
@ -218,6 +226,7 @@ const queryEntity = networks[urlParsed.search.e] || channels[urlParsed.search.e]
const filters = ref({
search: urlParsed.search.q,
years: urlParsed.search.years?.split(',').filter(Boolean).map(Number) || [],
tags: urlParsed.search.tags?.split(',').filter(Boolean) || [],
entity: queryEntity,
actors: queryActors,
@ -274,6 +283,7 @@ async function search(options = {}) {
navigate(getPath(scope.value, false), {
...query,
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
tags: filters.value.tags.join(',') || undefined,
e: filters.value.entity?.type === 'network' ? `_${filters.value.entity.slug}` : (filters.value.entity?.slug || undefined),
@ -281,6 +291,7 @@ async function search(options = {}) {
const res = await get('/scenes', {
...query,
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
tags: [pageTag?.slug, ...filters.value.tags].filter(Boolean).join(','),
stashId: pageStash?.id,
@ -290,6 +301,7 @@ async function search(options = {}) {
});
scenes.value = res.scenes;
aggYears.value = res.aggYears;
aggActors.value = res.aggActors;
aggTags.value = res.aggTags;
aggChannels.value = res.aggChannels;

View File

@ -70,7 +70,7 @@ module.exports = {
enabled: false,
},
links: {
dmca: 'mailto:dmca@traxxx.me',
content: 'mailto:content@traxxx.me',
discord: 'https://discord.gg/gY6fnq6jJV',
},
stashes: {

4
package-lock.json generated
View File

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

View File

@ -78,5 +78,5 @@
"postcss-custom-media": "^10.0.2",
"postcss-nesting": "^12.0.2"
},
"version": "0.26.6"
"version": "0.27.0"
}

View File

@ -23,26 +23,12 @@ export async function onBeforeRender(pageContext) {
throw render(404, `Cannot find actor '${pageContext.routeParams.actorId}'.`);
}
const {
scenes,
aggActors,
aggTags,
aggChannels,
total,
limit,
} = actorScenes;
return {
pageContext: {
title: actor.name,
pageProps: {
actor,
scenes,
aggActors,
aggTags,
aggChannels,
total,
limit,
...actorScenes,
},
},
};

View File

@ -51,11 +51,6 @@ export async function onBeforeRender(pageContext) {
const {
scenes,
aggActors,
aggTags,
aggChannels,
total,
limit,
} = entityScenes;
const campaignIndex = Math.floor((Math.random() * (0.5 - 0.2) + 0.2) * scenes.length);
@ -66,12 +61,7 @@ export async function onBeforeRender(pageContext) {
title: entity.name,
pageProps: {
entity,
scenes,
aggActors,
aggTags,
aggChannels,
total,
limit,
...entityScenes,
},
campaigns: {
index: campaignIndex,

View File

@ -10,25 +10,11 @@ export async function onBeforeRender(pageContext) {
limit: Number(pageContext.urlParsed.search.limit) || 50,
}, pageContext.user);
const {
movies,
aggActors,
aggTags,
aggChannels,
total,
limit,
} = movieResults;
return {
pageContext: {
title: 'Movies',
pageProps: {
movies,
aggActors,
aggTags,
aggChannels,
limit,
total,
...movieResults,
},
},
};

View File

@ -32,6 +32,7 @@ export async function onBeforeRender(pageContext) {
const {
scenes,
aggYears,
aggActors,
aggTags,
aggChannels,
@ -55,6 +56,7 @@ export async function onBeforeRender(pageContext) {
actors,
scenes,
movies,
aggYears,
aggActors,
aggTags,
aggChannels,

View File

@ -32,11 +32,6 @@ export async function onBeforeRender(pageContext) {
const {
scenes,
aggActors,
aggTags,
aggChannels,
total,
limit,
} = tagScenes;
const description = tag.description && md.renderInline(tag.description);
@ -50,12 +45,7 @@ export async function onBeforeRender(pageContext) {
pageProps: {
tag,
description,
scenes,
aggActors,
aggTags,
aggChannels,
total,
limit,
...tagScenes,
},
campaigns: {
index: campaignIndex,

View File

@ -28,11 +28,6 @@ export async function onBeforeRender(pageContext) {
const {
scenes,
aggTags,
aggChannels,
aggActors,
limit,
total,
} = sceneResults;
const campaignIndex = getCampaignIndex(scenes.length);
@ -42,12 +37,7 @@ export async function onBeforeRender(pageContext) {
pageContext: {
title: pageContext.routeParams.scope,
pageProps: {
scenes,
aggTags,
aggChannels,
aggActors,
limit,
total,
...sceneResults,
},
campaigns: {
index: campaignIndex,

View File

@ -181,6 +181,7 @@ function curateOptions(options) {
limit: options?.limit || 30,
page: Number(options?.page) || 1,
aggregate: options.aggregate ?? true,
aggregateYears: (options.aggregate ?? true) && (options.aggregateYears ?? true),
aggregateActors: (options.aggregate ?? true) && (options.aggregateActors ?? true),
aggregateTags: (options.aggregate ?? true) && (options.aggregateTags ?? true),
aggregateChannels: (options.aggregate ?? true) && (options.aggregateChannels ?? true),
@ -205,6 +206,7 @@ async function queryManticoreSql(filters, options) {
),
max_matches=:maxMatches:,
max_query_time=:maxQueryTime:
:yearsFacet:
:actorsFacet:
:tagsFacet:
:channelsFacet:;
@ -225,6 +227,7 @@ async function queryManticoreSql(filters, options) {
movies.stashed as stashed,
movies.created_at,
created_at as stashed_at,
year(effective_date) as effective_year,
weight() as _score
`));
@ -232,13 +235,21 @@ async function queryManticoreSql(filters, options) {
.innerJoin('movies', 'movies.id', 'movies_stashed.movie_id')
.where('stash_id', filters.stashId);
} else {
builder.select(knex.raw('*, weight() as _score'));
builder.select(knex.raw(`
*,
year(effective_date) as effective_year,
weight() as _score
`));
}
if (filters.query) {
builder.whereRaw('match(\'@!title :query:\', movies)', { query: escape(filters.query) });
}
if (filters.years?.length > 0) {
builder.whereIn('effective_year', filters.years);
}
filters.tagIds?.forEach((tagId) => {
builder.where('any(tag_ids)', tagId);
});
@ -292,6 +303,7 @@ async function queryManticoreSql(filters, options) {
.offset((options.page - 1) * options.limit)
.toString(),
// option threads=1 fixes actors, but drastically slows down performance, wait for fix
yearsFacet: options.aggregateYears ? knex.raw('facet year(effective_date) as years order by years desc limit ?', [aggSize]) : null,
actorsFacet: options.aggregateActors ? knex.raw('facet movies.actor_ids order by count(*) desc limit ?', [aggSize]) : null,
tagsFacet: options.aggregateTags ? knex.raw('facet movies.tag_ids order by count(*) desc limit ?', [aggSize]) : null,
channelsFacet: options.aggregateChannels ? knex.raw('facet movies.channel_id order by count(*) desc limit ?', [aggSize]) : null,
@ -311,6 +323,10 @@ async function queryManticoreSql(filters, options) {
const results = await utilsApi.sql(curatedSqlQuery);
// console.log(results[0]);
const years = results
.find((result) => (result.columns[0].years || result.columns[0]['movies.years']) && result.columns[1]['count(*)'])
?.data.map((row) => ({ key: row.years || row['movies.years'], doc_count: row['count(*)'] }))
|| [];
const actorIds = results
.find((result) => (result.columns[0].actor_ids || result.columns[0]['movies.actor_ids']) && result.columns[1]['count(*)'])
@ -333,6 +349,7 @@ async function queryManticoreSql(filters, options) {
movies: results[0].data,
total,
aggregations: {
years,
actorIds,
tagIds,
channelIds,
@ -356,6 +373,8 @@ export async function fetchMovies(filters, rawOptions, reqUser) {
const result = await queryManticoreSql(filters, options);
const aggYears = options.aggregateYears && result.aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count }));
const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds);
const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds);
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds);
@ -371,6 +390,7 @@ export async function fetchMovies(filters, rawOptions, reqUser) {
return {
movies,
aggYears,
aggActors,
aggTags,
aggChannels,

View File

@ -306,6 +306,7 @@ function curateOptions(options) {
limit: options?.limit || 30,
page: Number(options?.page) || 1,
aggregate: options.aggregate ?? true,
aggregateYears: (options.aggregate ?? true) && (options.aggregateYears ?? true),
aggregateActors: (options.aggregate ?? true) && (options.aggregateActors ?? true),
aggregateTags: (options.aggregate ?? true) && (options.aggregateTags ?? true),
aggregateChannels: (options.aggregate ?? true) && (options.aggregateChannels ?? true),
@ -332,6 +333,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
),
max_matches=:maxMatches:,
max_query_time=:maxQueryTime:
:yearsFacet:
:actorsFacet:
:tagsFacet:
:channelsFacet:;
@ -352,6 +354,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
scenes.stashed as stashed,
scenes.created_at,
created_at as stashed_at,
year(effective_date) as effective_year,
weight() as _score
`));
@ -359,13 +362,21 @@ async function queryManticoreSql(filters, options, _reqUser) {
.innerJoin('scenes', 'scenes.id', 'scenes_stashed.scene_id')
.where('stash_id', filters.stashId);
} else {
builder.select(knex.raw('*, weight() as _score'));
builder.select(knex.raw(`
*,
year(effective_date) as effective_year,
weight() as _score
`));
}
if (filters.query) {
builder.whereRaw('match(\'@!title :query:\', scenes)', { query: escape(filters.query) });
}
if (filters.years?.length > 0) {
builder.whereIn('effective_year', filters.years);
}
filters.tagIds?.forEach((tagId) => {
builder.where('any(tag_ids)', tagId);
});
@ -452,6 +463,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
.limit(options.limit)
.offset((options.page - 1) * options.limit),
// option threads=1 fixes actors, but drastically slows down performance, wait for fix
yearsFacet: options.aggregateYears ? knex.raw('facet year(effective_date) as years order by years desc limit ?', [aggSize]) : null,
actorsFacet: options.aggregateActors ? knex.raw('facet scenes.actor_ids order by count(*) desc limit ?', [aggSize]) : null,
tagsFacet: options.aggregateTags ? knex.raw('facet scenes.tag_ids order by count(*) desc limit ?', [aggSize]) : null,
channelsFacet: options.aggregateChannels ? knex.raw('facet scenes.channel_id order by count(*) desc limit ?', [aggSize]) : null,
@ -470,7 +482,12 @@ async function queryManticoreSql(filters, options, _reqUser) {
const results = await utilsApi.sql(curatedSqlQuery);
// console.log(results[0]);
// console.log(util.inspect(results, null, Infinity));
const years = results
.find((result) => (result.columns[0].years || result.columns[0]['scenes.years']) && result.columns[1]['count(*)'])
?.data.map((row) => ({ key: row.years || row['scenes.years'], doc_count: row['count(*)'] }))
|| [];
const actorIds = results
.find((result) => (result.columns[0].actor_ids || result.columns[0]['scenes.actor_ids']) && result.columns[1]['count(*)'])
@ -493,6 +510,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
scenes: results[0].data,
total,
aggregations: {
years,
actorIds,
tagIds,
channelIds,
@ -518,6 +536,8 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
const result = await queryManticoreSql(filters, options, reqUser);
console.timeEnd('manticore sql');
const aggYears = options.aggregateYears && result.aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count }));
const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds);
const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds);
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds);
@ -539,6 +559,7 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
return {
scenes,
aggYears,
aggActors,
aggTags,
aggChannels,

View File

@ -8,6 +8,7 @@ export async function curateMoviesQuery(query) {
return {
scope: query.scope || 'latest',
query: query.q,
years: query.years?.split(',')?.map((year) => Number(year)).filter(Boolean) || [],
actorIds: [query.actorId, ...(query.actors?.split(',') || []).map((identifier) => parseActorIdentifier(identifier)?.id)].filter(Boolean),
tagIds: await getIdsBySlug([query.tagSlug, ...(query.tags?.split(',') || [])], 'tags'),
entityId: query.e ? await getIdsBySlug([query.e], 'entities').then(([id]) => id) : query.entityId,
@ -19,6 +20,7 @@ export async function curateMoviesQuery(query) {
export async function fetchMoviesApi(req, res) {
const {
movies,
aggYears,
aggActors,
aggTags,
aggChannels,
@ -31,6 +33,7 @@ export async function fetchMoviesApi(req, res) {
res.send(stringify({
movies,
aggYears,
aggActors,
aggTags,
aggChannels,

View File

@ -7,6 +7,7 @@ import slugify from '../../utils/slugify.js';
import promiseProps from '../../utils/promise-props.js';
export async function curateScenesQuery(query) {
const splitYears = query.years?.split(',') || [];
const splitTags = query.tags?.split(',') || [];
const splitActors = query.actors?.split(',') || [];
const splitEntities = query.e?.split(',') || [];
@ -27,6 +28,7 @@ export async function curateScenesQuery(query) {
return {
scope: query.scope || 'latest',
query: query.q,
years: splitYears.map((year) => Number(year)).filter(Boolean) || [],
actorIds: [query.actorId, ...splitActors.filter((actor) => actor.charAt(0) !== '!').map((identifier) => parseActorIdentifier(identifier)?.id)].filter(Boolean),
notActorIds: splitActors.filter((actor) => actor.charAt(0) === '!').map((identifier) => parseActorIdentifier(identifier.slice(1))?.id).filter(Boolean),
tagIds,
@ -43,6 +45,7 @@ export async function curateScenesQuery(query) {
export async function fetchScenesApi(req, res) {
const {
scenes,
aggYears,
aggActors,
aggTags,
aggChannels,
@ -58,6 +61,7 @@ export async function fetchScenesApi(req, res) {
res.send(stringify({
scenes,
aggYears,
aggActors,
aggTags,
aggChannels,