traxxx-web/src/actors.js

257 lines
5.5 KiB
JavaScript

import { differenceInYears } from 'date-fns';
import knex from './knex.js';
import { searchApi } from './manticore.js';
import { HttpError } from './errors.js';
import { fetchCountriesByAlpha2 } from './countries.js';
export function curateActor(actor, context = {}) {
return {
id: actor.id,
slug: actor.slug,
name: actor.name,
gender: actor.gender,
age: actor.age,
dateOfBirth: actor.date_of_birth,
ageFromBirth: actor.date_of_birth && differenceInYears(Date.now(), actor.date_of_birth),
ageThen: context.sceneDate && actor.date_of_birth && differenceInYears(context.sceneDate, actor.date_of_birth),
birthCountry: actor.birth_country_alpha2 && {
alpha2: actor.birth_country_alpha2,
name: actor.birth_country_name,
},
residenceCountry: actor.residence_country_alpha2 && {
alpha2: actor.residence_country_alpha2,
name: actor.residence_country_name,
},
avatar: actor.avatar && {
id: actor.avatar.id,
path: actor.avatar.path,
thumbnail: actor.avatar.thumbnail,
lazy: actor.avatar.lazy,
isS3: actor.avatar.is_s3,
},
likes: actor.stashed,
...context.append?.[actor.id],
};
}
export function sortActorsByGender(actors) {
if (!actors) {
return actors;
}
const alphaActors = actors.sort((actorA, actorB) => actorA.name.localeCompare(actorB.name, 'en'));
const genderActors = ['transsexual', 'female', 'male', undefined].flatMap((gender) => alphaActors.filter((actor) => actor.gender === gender));
return genderActors;
}
export async function fetchActorsById(actorIds, options = {}) {
const [actors] = await Promise.all([
knex('actors_meta')
.select('actors_meta.*')
.whereIn('actors_meta.id', actorIds)
.modify((builder) => {
if (options.order) {
builder.orderBy(...options.order);
}
}),
]);
if (options.order) {
return actors.map((actorEntry) => curateActor(actorEntry, { append: options.append }));
}
const curatedActors = actorIds.map((actorId) => {
const actor = actors.find((actorEntry) => actorEntry.id === actorId);
if (!actor) {
console.warn(`Can't find ${actorId}`);
return null;
}
return curateActor(actor, { append: options.append });
}).filter(Boolean);
return curatedActors;
}
function curateOptions(options) {
if (options?.limit > 120) {
throw new HttpError('Limit must be <= 120', 400);
}
return {
page: options?.page || 1,
limit: options?.limit || 30,
requireAvatar: options?.requireAvatar || false,
order: [options.order?.[0] || 'name', options.order?.[1] || 'asc'],
};
}
function buildQuery(filters) {
console.log('filters', filters);
const query = {
bool: {
must: [],
},
};
const expressions = {
age: 'if(date_of_birth, floor((now() - date_of_birth) / 31556952), 0)',
};
if (filters.query) {
query.bool.must.push({
match: {
name: filters.query,
},
});
}
['gender', 'country'].forEach((attribute) => {
if (filters[attribute]) {
query.bool.must.push({
equals: {
[attribute]: filters[attribute],
},
});
}
});
['age', 'height', 'weight'].forEach((attribute) => {
if (filters[attribute]) {
query.bool.must.push({
range: {
[attribute]: {
gte: filters[attribute][0],
lte: filters[attribute][1],
},
},
});
}
});
if (filters.dateOfBirth && filters.dobType === 'dateOfBirth') {
query.bool.must.push({
equals: {
date_of_birth: Math.floor(filters.dateOfBirth.getTime() / 1000),
},
});
}
if (filters.dateOfBirth && filters.dobType === 'birthday') {
expressions.month_of_birth = 'month(date_of_birth)';
expressions.day_of_birth = 'day(date_of_birth)';
const month = filters.dateOfBirth.getMonth() + 1;
const day = filters.dateOfBirth.getDate();
query.bool.must.push({
bool: {
must: [
{
equals: {
month_of_birth: month,
},
},
{
equals: {
day_of_birth: day,
},
},
],
},
});
}
if (filters.cup) {
expressions.cup_in_range = `regex(cup, '^[${filters.cup[0]}-${filters.cup[1]}]')`;
query.bool.must.push({
equals: {
cup_in_range: 1,
},
});
}
if (typeof filters.naturalBoobs === 'boolean') {
query.bool.must.push({
equals: {
natural_boobs: filters.naturalBoobs ? 2 : 1, // manticore boolean does not support null, so 0 = null, 1 = false (enhanced), 2 = true (natural)
},
});
}
if (filters.requireAvatar) {
query.bool.must.push({
equals: {
has_avatar: 1,
},
});
}
return { query, expressions };
}
const sortMap = {
likes: 'stashed',
scenes: 'scenes',
};
function getSort(order) {
if (order[0] === 'name') {
return [{
slug: order[1],
}];
}
return [
{
[sortMap[order[0]]]: order[1],
},
{
slug: 'asc', // sort by name where primary order is equal
},
];
}
export async function fetchActors(filters, rawOptions) {
const options = curateOptions(rawOptions);
const { query, expressions } = buildQuery(filters);
console.log(options);
const result = await searchApi.search({
index: 'actors',
query,
expressions,
limit: options.limit,
offset: (options.page - 1) * options.limit,
sort: getSort(options.order),
aggs: {
countries: {
terms: {
field: 'country',
size: 300,
},
sort: [{ country: { order: 'asc' } }],
},
},
});
const actorIds = result.hits.hits.map((hit) => Number(hit._id));
const [actors, countries] = await Promise.all([
fetchActorsById(actorIds),
fetchCountriesByAlpha2(result.aggregations.countries.buckets.map((bucket) => bucket.key)),
]);
return {
actors,
countries,
total: result.hits.total,
limit: options.limit,
};
}