Added scene tags filter.

This commit is contained in:
2024-01-08 02:21:57 +01:00
parent 5db4f18123
commit 7d5efd12ef
17 changed files with 1136 additions and 136 deletions

View File

@@ -66,7 +66,7 @@ export async function fetchActorsById(actorIds, options = {}) {
const actor = actors.find((actorEntry) => actorEntry.id === actorId);
if (!actor) {
console.warn(`Can't find ${actorId}`);
console.warn(`Can't match actor ${actorId}`);
return null;
}
@@ -103,6 +103,8 @@ function buildQuery(filters) {
};
if (filters.query) {
console.log(filters.query);
query.bool.must.push({
match: {
name: filters.query,
@@ -198,6 +200,7 @@ function buildQuery(filters) {
const sortMap = {
likes: 'stashed',
scenes: 'scenes',
relevance: '_score',
};
function getSort(order) {
@@ -229,7 +232,7 @@ export async function fetchActors(filters, rawOptions) {
expressions,
limit: options.limit,
offset: (options.page - 1) * options.limit,
sort: getSort(options.order),
sort: getSort(options.order, filters),
aggs: {
countries: {
terms: {

10
src/app.js Normal file
View File

@@ -0,0 +1,10 @@
import initServer from './web/server.js';
import { cacheTagIds } from './tags.js';
async function init() {
await cacheTagIds();
initServer();
}
init();

36
src/logger.js Executable file
View File

@@ -0,0 +1,36 @@
import util from 'util';
import path from 'path';
import * as winston from 'winston';
import 'winston-daily-rotate-file';
import stackParser from 'error-stack-parser';
// import args from './args';
export default function initLogger(customLabel) {
const filepath = stackParser.parse(new Error())[1]?.fileName;
const contextLabel = customLabel || path.basename(filepath, '.js');
return winston.createLogger({
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format((info) => (info instanceof Error
? { ...info, message: info.stack }
: { ...info, message: typeof info.message === 'string' ? info.message : util.inspect(info.message) }))(),
winston.format.colorize(),
winston.format.printf(({
level, timestamp, label, message,
}) => `${timestamp} ${level} [${label || contextLabel}] ${message}`),
),
transports: [
new winston.transports.Console({
level: 'silly',
timestamp: true,
}),
new winston.transports.DailyRotateFile({
datePattern: 'YYYY-MM-DD',
filename: path.join('log', '%DATE%.log'),
level: 'silly',
}),
],
});
}

12
src/redis.js Normal file
View File

@@ -0,0 +1,12 @@
import config from 'config';
import redis from 'redis';
// logger.verbose('Redis module initialized');
const redisClient = redis.createClient({
socket: config.redis,
});
redisClient.connect();
export default redisClient;

View File

@@ -2,6 +2,7 @@ import knex from './knex.js';
import { searchApi } from './manticore.js';
import { HttpError } from './errors.js';
import { fetchActorsById, curateActor, sortActorsByGender } from './actors.js';
import { fetchTagsById } from './tags.js';
function curateMedia(media) {
if (!media) {
@@ -148,6 +149,8 @@ function curateOptions(options) {
limit: options?.limit || 30,
page: Number(options?.page) || 1,
aggregate: options.aggregate ?? true,
aggregateActors: (options.aggregate ?? true) && (options.aggregateActors ?? true),
aggregateTags: (options.aggregate ?? true) && (options.aggregateTags ?? true),
};
}
@@ -187,8 +190,6 @@ function buildQuery(filters = {}) {
}
if (filters.actorIds) {
console.log('actor ids', filters.actorIds);
filters.actorIds.forEach((actorId) => {
query.bool.must.push({
equals: {
@@ -198,6 +199,16 @@ function buildQuery(filters = {}) {
});
}
if (filters.tagIds) {
filters.tagIds.forEach((tagId) => {
query.bool.must.push({
equals: {
'any(tag_ids)': tagId,
},
});
});
}
/* tag filter
must_not: [
{
@@ -225,8 +236,8 @@ export async function fetchScenes(filters, rawOptions) {
limit: options.limit,
offset: (options.page - 1) * options.limit,
sort,
...(options.aggregate && {
aggs: {
aggs: {
...(options.aggregateActors && {
actorIds: {
terms: {
field: 'actor_ids',
@@ -234,14 +245,25 @@ export async function fetchScenes(filters, rawOptions) {
},
// sort: [{ doc_count: { order: 'asc' } }],
},
},
}),
}),
...(options.aggregateTags && {
tagIds: {
terms: {
field: 'tag_ids',
size: 1000,
},
// sort: [{ doc_count: { order: 'asc' } }],
},
}),
},
});
const actorCounts = result.aggregations?.actorIds && Object.fromEntries(result.aggregations?.actorIds?.buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
const actorCounts = options.aggregateActors && Object.fromEntries(result.aggregations?.actorIds?.buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
const tagCounts = options.aggregateTags && Object.fromEntries(result.aggregations?.tagIds?.buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
const [actors] = await Promise.all([
result.aggregations?.actorIds ? fetchActorsById(result.aggregations.actorIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: actorCounts }) : [],
const [aggActors, aggTags] = await Promise.all([
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 }) : [],
]);
const sceneIds = result.hits.hits.map((hit) => Number(hit._id));
@@ -249,7 +271,8 @@ export async function fetchScenes(filters, rawOptions) {
return {
scenes,
actors,
aggActors,
aggTags,
total: result.hits.total,
limit: options.limit,
};

56
src/tags.js Normal file
View File

@@ -0,0 +1,56 @@
import knex from './knex.js';
import redis from './redis.js';
import initLogger from './logger.js';
const logger = initLogger();
function curateTag(tag, context) {
return {
id: tag.id,
name: tag.name,
slug: tag.slug,
priority: tag.priority,
...context.append?.[tag.id],
};
}
export async function fetchTagsById(tagIds, options = {}) {
const [tags] = await Promise.all([
knex('tags')
.whereIn('tags.id', tagIds)
.modify((builder) => {
if (options.order) {
builder.orderBy(...options.order);
}
}),
]);
if (options.order) {
return tags.map((tagEntry) => curateTag(tagEntry, { append: options.append }));
}
const curatedTags = tagIds.map((tagId) => {
const tag = tags.find((tagEntry) => tagEntry.id === tagId);
if (!tag) {
console.warn(`Can't match tag ${tagId}`);
return null;
}
return curateTag(tag, { append: options.append });
}).filter(Boolean);
return curatedTags;
}
export async function cacheTagIds() {
const tags = await knex('tags')
.select('id', 'slug')
.whereNull('alias_for')
.whereNotNull('slug');
await redis.del('traxxx:tags:id_by_slug');
await redis.hSet('traxxx:tags:id_by_slug', tags.map((tag) => [tag.slug, tag.id]));
logger.info('Cached tags IDs by slug');
}

View File

@@ -1,28 +1,50 @@
import { stringify } from '@brillout/json-serializer/stringify';
import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */
import { fetchScenes } from '../scenes.js';
import redis from '../redis.js';
export function curateScenesQuery(query) {
async function getTagIdsBySlug(tagSlugs) {
const tagIds = await Promise.all(tagSlugs.map(async (slug) => {
if (!slug) {
return null;
}
if (Number(slug)) {
return Number(slug); // already an ID or missing
}
const tagId = await redis.hGet('traxxx:tags:id_by_slug', slug);
return Number(tagId);
}));
return tagIds.filter(Boolean);
}
export async function curateScenesQuery(query) {
return {
scope: query.scope || 'latest',
actorIds: [query.actorId, ...(query.actors?.split(',') || [])].filter(Boolean).map((actorId) => Number(actorId)),
tagIds: await getTagIdsBySlug([query.tagId, ...(query.tags?.split(',') || [])]),
};
}
export async function fetchScenesApi(req, res) {
const {
scenes,
actors,
aggActors,
aggTags,
limit,
total,
} = await fetchScenes(curateScenesQuery(req.query), {
} = await fetchScenes(await curateScenesQuery(req.query), {
page: Number(req.query.page) || 1,
limit: Number(req.query.limit) || 30,
});
res.send(stringify({
scenes,
actors,
aggActors,
aggTags,
limit,
total,
}));

View File

@@ -23,9 +23,12 @@ import { renderPage } from 'vike/server'; // eslint-disable-line import/extensio
import { fetchScenesApi } from './scenes.js';
import { fetchActorsApi } from './actors.js';
import initLogger from '../logger.js';
const logger = initLogger();
const isProduction = process.env.NODE_ENV === 'production';
async function startServer() {
export default async function initServer() {
const app = express();
const router = Router();
@@ -106,7 +109,5 @@ async function startServer() {
const port = process.env.PORT || config.web.port || 3000;
app.listen(port);
console.log(`Server running at http://localhost:${port}`);
logger.info(`Server running at http://localhost:${port}`);
}
startServer();