Added scene tags filter.
This commit is contained in:
@@ -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
10
src/app.js
Normal 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
36
src/logger.js
Executable 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
12
src/redis.js
Normal 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;
|
||||
@@ -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
56
src/tags.js
Normal 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');
|
||||
}
|
||||
@@ -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,
|
||||
}));
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user