2023-12-30 05:29:53 +00:00
|
|
|
import knex from './knex.js';
|
|
|
|
import { searchApi } from './manticore.js';
|
|
|
|
import { HttpError } from './errors.js';
|
2024-01-07 05:13:40 +00:00
|
|
|
import { fetchActorsById, curateActor, sortActorsByGender } from './actors.js';
|
2024-01-08 01:21:57 +00:00
|
|
|
import { fetchTagsById } from './tags.js';
|
2024-01-09 01:26:32 +00:00
|
|
|
import { fetchEntitiesById } from './entities.js';
|
2023-12-30 05:29:53 +00:00
|
|
|
|
|
|
|
function curateMedia(media) {
|
|
|
|
if (!media) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
id: media.id,
|
|
|
|
path: media.path,
|
|
|
|
thumbnail: media.thumbnail,
|
|
|
|
lazy: media.lazy,
|
|
|
|
isS3: media.is_s3,
|
|
|
|
width: media.width,
|
|
|
|
height: media.height,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function curateScene(rawScene, assets) {
|
|
|
|
if (!rawScene) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
id: rawScene.id,
|
|
|
|
title: rawScene.title,
|
|
|
|
slug: rawScene.slug,
|
|
|
|
url: rawScene.url,
|
|
|
|
date: rawScene.date,
|
|
|
|
createdAt: rawScene.created_at,
|
|
|
|
effectiveDate: rawScene.effective_date,
|
|
|
|
description: rawScene.description,
|
|
|
|
duration: rawScene.duration,
|
|
|
|
channel: {
|
|
|
|
id: assets.channel.id,
|
|
|
|
slug: assets.channel.slug,
|
|
|
|
name: assets.channel.name,
|
|
|
|
type: assets.channel.type,
|
|
|
|
isIndependent: assets.channel.independent,
|
|
|
|
hasLogo: assets.channel.has_logo,
|
|
|
|
},
|
|
|
|
network: assets.channel.network_id ? {
|
|
|
|
id: assets.channel.network_id,
|
|
|
|
slug: assets.channel.network_slug,
|
|
|
|
name: assets.channel.network_name,
|
|
|
|
type: assets.channel.network_type,
|
|
|
|
hasLogo: assets.channel.has_logo,
|
|
|
|
} : null,
|
|
|
|
actors: sortActorsByGender(assets.actors.map((actor) => curateActor(actor, { sceneDate: rawScene.effective_date }))),
|
|
|
|
directors: assets.directors.map((director) => ({
|
|
|
|
id: director.id,
|
|
|
|
slug: director.slug,
|
|
|
|
name: director.name,
|
|
|
|
})),
|
|
|
|
tags: assets.tags.map((tag) => ({
|
|
|
|
id: tag.id,
|
|
|
|
slug: tag.slug,
|
|
|
|
name: tag.name,
|
|
|
|
})),
|
|
|
|
poster: curateMedia(assets.poster),
|
|
|
|
photos: assets.photos.map((photo) => curateMedia(photo)),
|
|
|
|
createdBatchId: rawScene.created_batch_id,
|
|
|
|
updatedBatchId: rawScene.updated_batch_id,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function fetchScenesById(sceneIds) {
|
|
|
|
const [scenes, channels, actors, directors, tags, posters, photos] = await Promise.all([
|
|
|
|
knex('releases').whereIn('id', sceneIds),
|
|
|
|
knex('releases')
|
|
|
|
.select('channels.*', 'networks.id as network_id', 'networks.slug as network_slug', 'networks.name as network_name', 'networks.type as network_type')
|
|
|
|
.whereIn('releases.id', sceneIds)
|
|
|
|
.leftJoin('entities as channels', 'channels.id', 'releases.entity_id')
|
|
|
|
.leftJoin('entities as networks', 'networks.id', 'channels.parent_id')
|
|
|
|
.groupBy('channels.id', 'networks.id'),
|
|
|
|
knex('releases_actors')
|
|
|
|
.select(
|
2024-01-10 01:00:38 +00:00
|
|
|
'actors_meta.*',
|
2023-12-30 05:29:53 +00:00
|
|
|
'releases_actors.release_id',
|
|
|
|
'birth_countries.alpha2 as birth_country_alpha2',
|
|
|
|
'birth_countries.name as birth_country_name',
|
|
|
|
'residence_countries.alpha2 as residence_country_alpha2',
|
|
|
|
'residence_countries.name as residence_country_name',
|
|
|
|
)
|
|
|
|
.whereIn('release_id', sceneIds)
|
2024-01-10 01:00:38 +00:00
|
|
|
.leftJoin('actors_meta', 'actors_meta.id', 'releases_actors.actor_id')
|
|
|
|
.leftJoin('countries as birth_countries', 'birth_countries.alpha2', 'actors_meta.birth_country_alpha2')
|
|
|
|
.leftJoin('countries as residence_countries', 'residence_countries.alpha2', 'actors_meta.residence_country_alpha2'),
|
2023-12-30 05:29:53 +00:00
|
|
|
knex('releases_directors')
|
|
|
|
.whereIn('release_id', sceneIds)
|
|
|
|
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
|
|
|
|
knex('releases_tags')
|
|
|
|
.select('id', 'slug', 'name', 'release_id')
|
|
|
|
.whereNotNull('tags.id')
|
|
|
|
.whereIn('release_id', sceneIds)
|
2024-01-07 05:13:40 +00:00
|
|
|
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
|
|
|
|
.orderBy('priority', 'desc'),
|
2023-12-30 05:29:53 +00:00
|
|
|
knex('releases_posters')
|
|
|
|
.whereIn('release_id', sceneIds)
|
|
|
|
.leftJoin('media', 'media.id', 'releases_posters.media_id'),
|
|
|
|
knex('releases_photos')
|
|
|
|
.whereIn('release_id', sceneIds)
|
|
|
|
.leftJoin('media', 'media.id', 'releases_photos.media_id'),
|
|
|
|
]);
|
|
|
|
|
|
|
|
return sceneIds.map((sceneId) => {
|
|
|
|
const scene = scenes.find((sceneEntry) => sceneEntry.id === sceneId);
|
|
|
|
|
|
|
|
if (!scene) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const sceneChannel = channels.find((entity) => entity.id === scene.entity_id);
|
|
|
|
const sceneActors = actors.filter((actor) => actor.release_id === sceneId);
|
|
|
|
const sceneDirectors = directors.filter((director) => director.release_id === sceneId);
|
|
|
|
const sceneTags = tags.filter((tag) => tag.release_id === sceneId);
|
|
|
|
const scenePoster = posters.find((poster) => poster.release_id === sceneId);
|
|
|
|
const scenePhotos = photos.filter((photo) => photo.release_id === sceneId);
|
|
|
|
|
|
|
|
return curateScene(scene, {
|
|
|
|
channel: sceneChannel,
|
|
|
|
actors: sceneActors,
|
|
|
|
directors: sceneDirectors,
|
|
|
|
tags: sceneTags,
|
|
|
|
poster: scenePoster,
|
|
|
|
photos: scenePhotos,
|
|
|
|
});
|
|
|
|
}).filter(Boolean);
|
|
|
|
}
|
|
|
|
|
2024-01-07 05:13:40 +00:00
|
|
|
function curateOptions(options) {
|
|
|
|
if (options?.limit > 100) {
|
|
|
|
throw new HttpError('Limit must be <= 100', 400);
|
|
|
|
}
|
2023-12-30 05:29:53 +00:00
|
|
|
|
|
|
|
return {
|
2024-01-07 05:13:40 +00:00
|
|
|
limit: options?.limit || 30,
|
|
|
|
page: Number(options?.page) || 1,
|
|
|
|
aggregate: options.aggregate ?? true,
|
2024-01-08 01:21:57 +00:00
|
|
|
aggregateActors: (options.aggregate ?? true) && (options.aggregateActors ?? true),
|
|
|
|
aggregateTags: (options.aggregate ?? true) && (options.aggregateTags ?? true),
|
2024-01-09 01:26:32 +00:00
|
|
|
aggregateChannels: (options.aggregate ?? true) && (options.aggregateChannels ?? true),
|
2023-12-30 05:29:53 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-01-07 05:13:40 +00:00
|
|
|
function buildQuery(filters = {}) {
|
|
|
|
const query = {
|
|
|
|
bool: {
|
|
|
|
must: [],
|
|
|
|
},
|
|
|
|
};
|
2023-12-30 05:29:53 +00:00
|
|
|
|
2024-01-07 05:13:40 +00:00
|
|
|
let sort = [{ effective_date: 'desc' }];
|
|
|
|
|
|
|
|
if (!filters.scope || filters.scope === 'latest') {
|
|
|
|
query.bool.must.push({
|
|
|
|
range: {
|
|
|
|
effective_date: {
|
|
|
|
lte: Math.round(Date.now() / 1000),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (filters.scope === 'upcoming') {
|
|
|
|
query.bool.must.push({
|
|
|
|
range: {
|
|
|
|
effective_date: {
|
|
|
|
gt: Math.round(Date.now() / 1000),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
sort = [{ effective_date: 'asc' }];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (filters.scope === 'new') {
|
|
|
|
sort = [{ created_at: 'desc' }, { effective_date: 'asc' }];
|
|
|
|
}
|
|
|
|
|
2024-01-25 02:07:26 +00:00
|
|
|
if (filters.scope === 'likes') {
|
|
|
|
sort = [{ stashed: 'desc' }, { effective_date: 'desc' }];
|
|
|
|
}
|
|
|
|
|
2024-01-10 01:00:38 +00:00
|
|
|
if (filters.tagIds) {
|
|
|
|
filters.tagIds.forEach((tagId) => {
|
|
|
|
query.bool.must.push({ equals: { 'any(tag_ids)': tagId } });
|
2024-01-07 05:13:40 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-01-10 01:00:38 +00:00
|
|
|
if (filters.entityId) {
|
|
|
|
query.bool.must.push({
|
|
|
|
bool: {
|
|
|
|
should: [
|
|
|
|
{ equals: { channel_id: filters.entityId } },
|
|
|
|
{ equals: { network_id: filters.entityId } },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (filters.actorIds) {
|
|
|
|
filters.actorIds.forEach((actorId) => {
|
|
|
|
query.bool.must.push({ equals: { 'any(actor_ids)': actorId } });
|
2024-01-08 01:21:57 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-01-07 05:13:40 +00:00
|
|
|
/* tag filter
|
|
|
|
must_not: [
|
|
|
|
{
|
|
|
|
in: {
|
|
|
|
'any(tag_ids)': [101, 180, 32],
|
2023-12-30 05:29:53 +00:00
|
|
|
},
|
|
|
|
},
|
2024-01-07 05:13:40 +00:00
|
|
|
],
|
|
|
|
*/
|
2023-12-30 05:29:53 +00:00
|
|
|
|
2024-01-07 05:13:40 +00:00
|
|
|
return { query, sort };
|
2023-12-30 05:29:53 +00:00
|
|
|
}
|
|
|
|
|
2024-01-09 01:26:32 +00:00
|
|
|
function buildAggregates(options) {
|
|
|
|
const aggregates = {};
|
|
|
|
|
|
|
|
if (options.aggregateActors) {
|
|
|
|
aggregates.actorIds = {
|
|
|
|
terms: {
|
|
|
|
field: 'actor_ids',
|
|
|
|
size: 5000,
|
|
|
|
},
|
|
|
|
// sort: [{ doc_count: { order: 'asc' } }],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (options.aggregateTags) {
|
|
|
|
aggregates.tagIds = {
|
|
|
|
terms: {
|
|
|
|
field: 'tag_ids',
|
|
|
|
size: 1000,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (options.aggregateChannels) {
|
|
|
|
aggregates.channelIds = {
|
|
|
|
terms: {
|
|
|
|
field: 'channel_id',
|
|
|
|
size: 1000,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return aggregates;
|
|
|
|
}
|
|
|
|
|
|
|
|
function countAggregations(buckets) {
|
|
|
|
if (!buckets) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
|
|
|
|
}
|
|
|
|
|
2024-01-07 05:13:40 +00:00
|
|
|
export async function fetchScenes(filters, rawOptions) {
|
|
|
|
const options = curateOptions(rawOptions);
|
|
|
|
const { query, sort } = buildQuery(filters);
|
|
|
|
|
2023-12-30 05:29:53 +00:00
|
|
|
const result = await searchApi.search({
|
|
|
|
index: 'scenes',
|
2024-01-07 05:13:40 +00:00
|
|
|
query,
|
|
|
|
limit: options.limit,
|
|
|
|
offset: (options.page - 1) * options.limit,
|
|
|
|
sort,
|
2024-01-09 01:26:32 +00:00
|
|
|
aggs: buildAggregates(options),
|
2024-01-25 02:07:26 +00:00
|
|
|
max_matches: 5000,
|
2023-12-30 05:29:53 +00:00
|
|
|
});
|
|
|
|
|
2024-01-09 01:26:32 +00:00
|
|
|
const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds?.buckets);
|
|
|
|
const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds?.buckets);
|
|
|
|
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds?.buckets);
|
2024-01-07 22:44:33 +00:00
|
|
|
|
2024-01-09 01:26:32 +00:00
|
|
|
const [aggActors, aggTags, aggChannels] = await Promise.all([
|
2024-01-08 01:21:57 +00:00
|
|
|
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: actorCounts }) : [],
|
|
|
|
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: tagCounts }) : [],
|
2024-01-09 01:26:32 +00:00
|
|
|
options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: channelCounts }) : [],
|
2024-01-07 05:13:40 +00:00
|
|
|
]);
|
|
|
|
|
2023-12-30 05:29:53 +00:00
|
|
|
const sceneIds = result.hits.hits.map((hit) => Number(hit._id));
|
|
|
|
const scenes = await fetchScenesById(sceneIds);
|
|
|
|
|
|
|
|
return {
|
|
|
|
scenes,
|
2024-01-08 01:21:57 +00:00
|
|
|
aggActors,
|
|
|
|
aggTags,
|
2024-01-09 01:26:32 +00:00
|
|
|
aggChannels,
|
2023-12-30 05:29:53 +00:00
|
|
|
total: result.hits.total,
|
2024-01-07 05:13:40 +00:00
|
|
|
limit: options.limit,
|
2023-12-30 05:29:53 +00:00
|
|
|
};
|
|
|
|
}
|