2024-03-17 21:15:11 +00:00
import config from 'config' ;
2023-12-30 05:29:53 +00:00
import { differenceInYears } from 'date-fns' ;
2024-01-10 01:00:38 +00:00
import { unit } from 'mathjs' ;
2023-12-30 05:29:53 +00:00
2024-03-21 01:54:05 +00:00
import { knexOwner as knex , knexManticore } from './knex.js' ;
import { utilsApi } from './manticore.js' ;
2023-12-30 05:29:53 +00:00
import { HttpError } from './errors.js' ;
2023-12-31 02:02:03 +00:00
import { fetchCountriesByAlpha2 } from './countries.js' ;
2024-03-21 01:54:05 +00:00
import { curateStash } from './stashes.js' ;
2024-03-31 23:50:24 +00:00
import escape from '../utils/escape-manticore.js' ;
2024-03-21 02:49:03 +00:00
import slugify from '../utils/slugify.js' ;
2023-12-30 05:29:53 +00:00
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 ) ,
2024-01-10 01:00:38 +00:00
bust : actor . bust ,
cup : actor . cup ,
waist : actor . waist ,
hip : actor . hip ,
naturalBoobs : actor . naturalBoobs ,
height : actor . height && {
metric : actor . height ,
imperial : unit ( actor . height , 'cm' ) . splitUnit ( [ 'ft' , 'in' ] ) . map ( ( value ) => Math . round ( value . toNumber ( ) ) ) ,
2023-12-30 05:29:53 +00:00
} ,
2024-01-10 01:00:38 +00:00
weight : actor . weight && {
metric : actor . weight ,
imperial : Math . round ( unit ( actor . weight , 'kg' ) . toNumeric ( 'lbs' ) ) ,
} ,
eyes : actor . eyes ,
hairColor : actor . hairColor ,
hasTattoos : actor . has _tattoos ,
tattoos : actor . tattoos ,
hasPiercings : actor . has _piercings ,
piercings : actor . piercings ,
2024-01-25 02:07:26 +00:00
origin : actor . birth _country _alpha2 && {
2024-01-10 01:00:38 +00:00
country : actor . birth _country _alpha2 && {
alpha2 : actor . birth _country _alpha2 ,
name : actor . birth _country _name ,
2024-04-02 01:01:15 +00:00
alias : actor . birth _country _alias ,
2024-01-10 01:00:38 +00:00
} ,
} ,
2024-01-25 02:07:26 +00:00
residence : actor . residence _country _alpha2 && {
2024-01-10 01:00:38 +00:00
country : actor . residence _country _alpha2 && {
alpha2 : actor . residence _country _alpha2 ,
name : actor . residence _country _name ,
2024-04-02 01:01:15 +00:00
alias : actor . residence _country _alias ,
2024-01-10 01:00:38 +00:00
} ,
2023-12-30 05:29:53 +00:00
} ,
2024-01-05 23:30:30 +00:00
avatar : actor . avatar && {
id : actor . avatar . id ,
path : actor . avatar . path ,
thumbnail : actor . avatar . thumbnail ,
lazy : actor . avatar . lazy ,
isS3 : actor . avatar . is _s3 ,
} ,
2024-01-10 01:00:38 +00:00
createdAt : actor . created _at ,
updatedAt : actor . updated _at ,
2024-01-05 23:30:30 +00:00
likes : actor . stashed ,
2024-03-21 01:54:05 +00:00
stashes : context . stashes ? . map ( ( stash ) => curateStash ( stash ) ) || [ ] ,
2024-01-07 22:44:33 +00:00
... context . append ? . [ actor . id ] ,
2023-12-30 05:29:53 +00:00
} ;
}
2024-03-21 02:49:03 +00:00
export function sortActorsByGender ( actors , context = { } ) {
2023-12-30 05:29:53 +00:00
if ( ! actors ) {
return actors ;
}
const alphaActors = actors . sort ( ( actorA , actorB ) => actorA . name . localeCompare ( actorB . name , 'en' ) ) ;
2024-03-24 03:22:37 +00:00
const genderActors = [ 'transsexual' , 'female' , undefined , null , 'male' ] . flatMap ( ( gender ) => alphaActors . filter ( ( actor ) => actor . gender === gender ) ) ;
2023-12-30 05:29:53 +00:00
2024-03-21 02:49:03 +00:00
const titleSlug = slugify ( context . title ) ;
const titleActors = titleSlug ? genderActors . sort ( ( actorA , actorB ) => {
2024-03-23 01:47:52 +00:00
const actorASlug = actorA . slug . split ( '-' ) [ 0 ] ;
const actorBSlug = actorB . slug . split ( '-' ) [ 0 ] ;
if ( titleSlug . includes ( actorASlug ) && ! titleSlug . includes ( actorBSlug ) ) {
2024-03-21 02:49:03 +00:00
return - 1 ;
}
2024-03-23 01:47:52 +00:00
if ( titleSlug . includes ( actorBSlug ) && ! titleSlug . includes ( actorASlug ) ) {
2024-03-21 02:49:03 +00:00
return 1 ;
}
return 0 ;
} ) : alphaActors ;
return titleActors ;
2023-12-30 05:29:53 +00:00
}
2024-03-21 01:54:05 +00:00
export async function fetchActorsById ( actorIds , options = { } , reqUser ) {
const [ actors , stashes ] = await Promise . all ( [
2024-02-27 00:20:15 +00:00
knex ( 'actors' )
2024-02-22 04:08:06 +00:00
. select (
2024-02-27 00:20:15 +00:00
'actors.*' ,
2024-02-22 04:08:06 +00:00
'actors_meta.*' ,
'birth_countries.alpha2 as birth_country_alpha2' ,
knex . raw ( 'COALESCE(birth_countries.alias, birth_countries.name) as birth_country_name' ) ,
'residence_countries.alpha2 as residence_country_alpha2' ,
knex . raw ( 'COALESCE(residence_countries.alias, residence_countries.name) as residence_country_name' ) ,
)
2024-02-27 00:20:15 +00:00
. leftJoin ( 'actors_meta' , 'actors_meta.actor_id' , 'actors.id' )
. leftJoin ( 'countries as birth_countries' , 'birth_countries.alpha2' , 'actors.birth_country_alpha2' )
. leftJoin ( 'countries as residence_countries' , 'residence_countries.alpha2' , 'actors.residence_country_alpha2' )
. whereIn ( 'actors.id' , actorIds )
2024-01-07 05:13:40 +00:00
. modify ( ( builder ) => {
if ( options . order ) {
builder . orderBy ( ... options . order ) ;
}
} ) ,
2024-03-21 01:54:05 +00:00
reqUser
? knex ( 'stashes_actors' )
. leftJoin ( 'stashes' , 'stashes.id' , 'stashes_actors.stash_id' )
. where ( 'stashes.user_id' , reqUser . id )
. whereIn ( 'stashes_actors.actor_id' , actorIds )
: [ ] ,
2023-12-30 05:29:53 +00:00
] ) ;
2024-01-07 05:13:40 +00:00
if ( options . order ) {
2024-03-21 01:54:05 +00:00
return actors . map ( ( actorEntry ) => curateActor ( actorEntry , {
stashes : stashes . filter ( ( stash ) => stash . actor _id === actorEntry . id ) ,
append : options . append ,
} ) ) ;
2024-01-07 05:13:40 +00:00
}
2024-01-04 00:49:16 +00:00
const curatedActors = actorIds . map ( ( actorId ) => {
2023-12-30 05:29:53 +00:00
const actor = actors . find ( ( actorEntry ) => actorEntry . id === actorId ) ;
if ( ! actor ) {
2024-01-08 01:21:57 +00:00
console . warn ( ` Can't match actor ${ actorId } ` ) ;
2023-12-30 05:29:53 +00:00
return null ;
}
2024-03-21 01:54:05 +00:00
return curateActor ( actor , {
stashes : stashes . filter ( ( stash ) => stash . actor _id === actor . id ) ,
append : options . append ,
} ) ;
2023-12-30 05:29:53 +00:00
} ) . filter ( Boolean ) ;
2024-01-04 00:49:16 +00:00
return curatedActors ;
2023-12-30 05:29:53 +00:00
}
function curateOptions ( options ) {
2024-01-04 00:49:16 +00:00
if ( options ? . limit > 120 ) {
throw new HttpError ( 'Limit must be <= 120' , 400 ) ;
2023-12-30 05:29:53 +00:00
}
return {
page : options ? . page || 1 ,
limit : options ? . limit || 30 ,
requireAvatar : options ? . requireAvatar || false ,
2024-03-31 23:50:24 +00:00
order : [ escape ( options . order ? . [ 0 ] ) || 'name' , escape ( options . order ? . [ 1 ] ) || 'asc' ] ,
2023-12-30 05:29:53 +00:00
} ;
}
2024-03-21 01:54:05 +00:00
/ *
const sortMap = {
likes : 'stashed' ,
scenes : 'scenes' ,
relevance : '_score' ,
} ;
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
} ,
] ;
}
2023-12-30 05:29:53 +00:00
function buildQuery ( filters ) {
const query = {
bool : {
must : [ ] ,
} ,
} ;
2023-12-31 02:02:03 +00:00
const expressions = {
age : 'if(date_of_birth, floor((now() - date_of_birth) / 31556952), 0)' ,
} ;
2023-12-30 05:29:53 +00:00
if ( filters . query ) {
query . bool . must . push ( {
match : {
name : filters . query ,
} ,
} ) ;
}
2024-01-03 01:52:41 +00:00
[ 'gender' , 'country' ] . forEach ( ( attribute ) => {
if ( filters [ attribute ] ) {
query . bool . must . push ( {
equals : {
[ attribute ] : filters [ attribute ] ,
} ,
} ) ;
}
} ) ;
2023-12-31 02:02:03 +00:00
2023-12-30 05:29:53 +00:00
[ 'age' , 'height' , 'weight' ] . forEach ( ( attribute ) => {
if ( filters [ attribute ] ) {
query . bool . must . push ( {
range : {
[ attribute ] : {
gte : filters [ attribute ] [ 0 ] ,
lte : filters [ attribute ] [ 1 ] ,
} ,
} ,
} ) ;
}
} ) ;
2024-01-03 01:52:41 +00:00
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 ,
} ,
} ,
] ,
} ,
} ) ;
}
2023-12-31 02:02:03 +00:00
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)
} ,
} ) ;
}
2023-12-30 05:29:53 +00:00
if ( filters . requireAvatar ) {
query . bool . must . push ( {
equals : {
has _avatar : 1 ,
} ,
} ) ;
}
2023-12-31 02:02:03 +00:00
return { query , expressions } ;
2023-12-30 05:29:53 +00:00
}
2024-03-21 01:54:05 +00:00
async function queryManticoreJson ( filters , options ) {
2023-12-31 02:02:03 +00:00
const { query , expressions } = buildQuery ( filters ) ;
2023-12-30 05:29:53 +00:00
const result = await searchApi . search ( {
index : 'actors' ,
query ,
2023-12-31 02:02:03 +00:00
expressions ,
2023-12-30 05:29:53 +00:00
limit : options . limit ,
offset : ( options . page - 1 ) * options . limit ,
2024-01-08 01:21:57 +00:00
sort : getSort ( options . order , filters ) ,
2023-12-31 02:02:03 +00:00
aggs : {
countries : {
terms : {
field : 'country' ,
size : 300 ,
} ,
sort : [ { country : { order : 'asc' } } ] ,
} ,
} ,
2024-03-17 21:15:11 +00:00
options : {
max _matches : config . database . manticore . maxMatches ,
max _query _time : config . database . manticore . maxQueryTime ,
} ,
2023-12-30 05:29:53 +00:00
} ) ;
2024-03-21 01:54:05 +00:00
const actors = result . hits . hits . map ( ( hit ) => ( {
id : hit . _id ,
... hit . _source ,
_score : hit . _score ,
} ) ) ;
return {
actors ,
total : result . hits . total ,
aggregations : result . aggregations && Object . fromEntries ( Object . entries ( result . aggregations ) . map ( ( [ key , { buckets } ] ) => [ key , buckets ] ) ) ,
} ;
}
* /
async function queryManticoreSql ( filters , options , _reqUser ) {
const aggSize = config . database . manticore . maxAggregateSize ;
const sqlQuery = knexManticore . raw ( `
: query :
OPTION
max _matches = : maxMatches : ,
max _query _time = : maxQueryTime :
: countriesFacet : ;
show meta ;
` , {
query : knexManticore ( filters . stashId ? 'actors_stashed' : 'actors' )
. modify ( ( builder ) => {
if ( filters . stashId ) {
builder . select ( knex . raw ( `
actors . id a s id ,
2024-03-23 21:31:14 +00:00
actors . slug ,
2024-03-21 02:27:01 +00:00
actors . gender as gender ,
2024-03-21 01:54:05 +00:00
actors . country as country ,
2024-03-21 02:27:01 +00:00
actors . height as height ,
2024-03-24 03:22:37 +00:00
actors . mass as mass ,
2024-03-21 02:27:01 +00:00
actors . cup as cup ,
actors . natural _boobs as natural _boobs ,
actors . date _of _birth as date _of _birth ,
actors . has _avatar as has _avatar ,
2024-03-21 01:54:05 +00:00
actors . scenes as scenes ,
actors . stashed as stashed ,
2024-03-21 02:27:01 +00:00
created _at as stashed _at ,
2024-03-24 03:22:37 +00:00
if ( actors . date _of _birth , floor ( ( now ( ) - actors . date _of _birth ) / 31556952 ) , 0 ) as age ,
weight ( ) as _score
2024-03-21 01:54:05 +00:00
` ));
builder
. innerJoin ( 'actors' , 'actors.id' , 'actors_stashed.actor_id' )
. where ( 'stash_id' , filters . stashId ) ;
} else {
2024-03-24 03:22:37 +00:00
builder . select ( knex . raw ( '*, weight() as _score' ) ) ;
2024-03-21 01:54:05 +00:00
}
if ( filters . query ) {
2024-03-31 23:50:24 +00:00
builder . whereRaw ( 'match(\'@name :query:\', actors)' , { query : escape ( filters . query ) } ) ;
2024-03-21 01:54:05 +00:00
}
2024-03-24 17:16:10 +00:00
// attribute filters
[ 'country' ] . forEach ( ( attribute ) => {
2024-03-21 01:54:05 +00:00
if ( filters [ attribute ] ) {
builder . where ( attribute , filters [ attribute ] ) ;
}
} ) ;
2024-03-24 17:16:10 +00:00
if ( filters . gender === 'other' ) {
builder . whereNull ( 'gender' ) ;
} else if ( filters . gender ) {
builder . where ( 'gender' , filters . gender ) ;
}
if ( filters . age ) {
builder . select ( 'if(date_of_birth, floor((now() - date_of_birth) / 31556952), 0) as age' ) ;
}
// range filters
2024-03-24 03:22:37 +00:00
[ 'age' , 'height' ] . forEach ( ( attribute ) => {
2024-03-21 01:54:05 +00:00
if ( filters [ attribute ] ) {
builder
. where ( attribute , '>=' , filters [ attribute ] [ 0 ] )
. where ( attribute , '<=' , filters [ attribute ] [ 1 ] ) ;
}
} ) ;
2024-03-24 03:22:37 +00:00
if ( filters . weight ) {
// weight is a reserved keyword in manticore
builder
. where ( 'mass' , '>=' , filters . weight [ 0 ] )
. where ( 'mass' , '<=' , filters . weight [ 1 ] ) ;
}
2024-03-21 01:54:05 +00:00
if ( filters . dateOfBirth && filters . dobType === 'dateOfBirth' ) {
builder . where ( 'date_of_birth' , Math . floor ( filters . dateOfBirth . getTime ( ) / 1000 ) ) ;
}
if ( filters . dateOfBirth && filters . dobType === 'birthday' ) {
const month = filters . dateOfBirth . getMonth ( ) + 1 ;
const day = filters . dateOfBirth . getDate ( ) ;
2024-03-24 17:16:10 +00:00
builder . select ( 'month(date_of_birth) as month_of_birth, day(date_of_birth) as day_of_birth' ) ;
2024-03-21 01:54:05 +00:00
builder
2024-03-24 17:16:10 +00:00
. where ( 'month_of_birth' , month )
. where ( 'day_of_birth' , day ) ;
2024-03-21 01:54:05 +00:00
}
if ( filters . cup ) {
2024-03-21 02:27:01 +00:00
builder . select ( ` regex(actors.cup, '^[ ${ filters . cup [ 0 ] } - ${ filters . cup [ 1 ] } ]') as cup_in_range ` ) ;
builder . where ( 'cup_in_range' , 1 ) ;
2024-03-21 01:54:05 +00:00
}
if ( typeof filters . naturalBoobs === 'boolean' ) {
builder . where ( 'natural_boobs' , filters . naturalBoobs ? 2 : 1 ) ; // manticore boolean does not support null, so 0 = null, 1 = false (enhanced), 2 = true (natural)
}
if ( filters . requireAvatar ) {
builder . where ( 'has_avatar' , 1 ) ;
}
if ( options . order ? . [ 0 ] === 'name' ) {
builder . orderBy ( 'actors.slug' , options . order [ 1 ] ) ;
} else if ( options . order ? . [ 0 ] === 'likes' ) {
builder . orderBy ( [
{ column : 'actors.stashed' , order : options . order [ 1 ] } ,
{ column : 'actors.slug' , order : 'asc' } ,
] ) ;
} else if ( options . order ? . [ 0 ] === 'scenes' ) {
builder . orderBy ( [
{ column : 'actors.scenes' , order : options . order [ 1 ] } ,
{ column : 'actors.slug' , order : 'asc' } ,
] ) ;
2024-03-24 03:22:37 +00:00
} else if ( options . order ? . [ 0 ] === 'results' ) {
builder . orderBy ( [
2024-03-24 22:36:25 +00:00
{ column : '_score' , order : options . order [ 1 ] } ,
2024-06-02 03:22:08 +00:00
{ column : 'actors.stashed' , order : 'desc' } ,
2024-03-24 03:22:37 +00:00
{ column : 'actors.slug' , order : 'asc' } ,
] ) ;
2024-03-21 01:54:05 +00:00
} else if ( options . order ? . [ 0 ] === 'stashed' && filters . stashId ) {
builder . orderBy ( [
{ column : 'stashed_at' , order : options . order [ 1 ] } ,
{ column : 'actors.slug' , order : 'asc' } ,
] ) ;
} else if ( options . order ) {
builder . orderBy ( [
{ column : ` actors. ${ options . order [ 0 ] } ` , order : options . order [ 1 ] } ,
{ column : 'actors.slug' , order : 'asc' } ,
] ) ;
} else {
builder . orderBy ( 'actors.slug' , 'asc' ) ;
}
} )
. limit ( options . limit )
. offset ( ( options . page - 1 ) * options . limit )
. toString ( ) ,
// option threads=1 fixes actors, but drastically slows down performance, wait for fix
countriesFacet : options . aggregateActors ? knex . raw ( 'facet actors.country order by count(*) desc limit 300' , [ aggSize ] ) : null ,
maxMatches : config . database . manticore . maxMatches ,
maxQueryTime : config . database . manticore . maxQueryTime ,
} ) . toString ( ) ;
// manticore does not seem to accept table.column syntax if 'table' is primary (yet?), crude work-around
const curatedSqlQuery = filters . stashId
? sqlQuery
: sqlQuery . replace ( /actors\./g , '' ) ;
if ( process . env . NODE _ENV === 'development' ) {
console . log ( curatedSqlQuery ) ;
}
const results = await utilsApi . sql ( curatedSqlQuery ) ;
// console.log(results[0]);
const countries = results
. find ( ( result ) => ( result . columns [ 0 ] . actor _ids || result . columns [ 0 ] [ 'scenes.country' ] ) && result . columns [ 1 ] [ 'count(*)' ] )
? . data . map ( ( row ) => ( { key : row . actor _ids || row [ 'scenes.country' ] , doc _count : row [ 'count(*)' ] } ) )
|| [ ] ;
const total = Number ( results . at ( - 1 ) . data . find ( ( entry ) => entry . Variable _name === 'total_found' ) . Value ) ;
return {
actors : results [ 0 ] . data ,
total ,
aggregations : {
countries ,
} ,
} ;
}
export async function fetchActors ( filters , rawOptions , reqUser ) {
const options = curateOptions ( rawOptions ) ;
console . log ( 'filters' , filters ) ;
console . log ( 'options' , options ) ;
const result = await queryManticoreSql ( filters , options , reqUser ) ;
2024-03-21 02:27:01 +00:00
// console.log('result', result);
2024-03-21 01:54:05 +00:00
const actorIds = result . actors . map ( ( actor ) => Number ( actor . id ) ) ;
2023-12-31 02:02:03 +00:00
const [ actors , countries ] = await Promise . all ( [
2024-03-21 01:54:05 +00:00
fetchActorsById ( actorIds , { } , reqUser ) ,
fetchCountriesByAlpha2 ( result . aggregations . countries . map ( ( bucket ) => bucket . key ) ) ,
2023-12-31 02:02:03 +00:00
] ) ;
2023-12-30 05:29:53 +00:00
return {
actors ,
2023-12-31 02:02:03 +00:00
countries ,
2024-03-21 01:54:05 +00:00
total : result . total ,
2023-12-30 05:29:53 +00:00
limit : options . limit ,
} ;
}