Added georestriction with SFW mode.
This commit is contained in:
@@ -122,7 +122,10 @@ export function curateActor(actor, context = {}) {
|
||||
state: actor.residence_state,
|
||||
},
|
||||
agency: actor.agency,
|
||||
avatar: curateMedia(actor.avatar),
|
||||
avatar: actor.avatar && curateMedia({
|
||||
...actor.avatar,
|
||||
sfw_media: actor.sfw_avatar,
|
||||
}),
|
||||
socials: context.socials?.map((social) => ({
|
||||
id: social.id,
|
||||
url: social.url,
|
||||
@@ -214,18 +217,21 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||
'residence_countries.alpha2 as residence_country_alpha2',
|
||||
knex.raw('COALESCE(residence_countries.alias, residence_countries.name) as residence_country_name'),
|
||||
knex.raw('row_to_json(entities) as entity'),
|
||||
knex.raw('row_to_json(sfw_media) as sfw_avatar'),
|
||||
)
|
||||
.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')
|
||||
.leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'avatars.sfw_media_id')
|
||||
.leftJoin('entities', 'entities.id', 'actors.entity_id')
|
||||
.whereIn('actors.id', actorIds)
|
||||
.modify((builder) => {
|
||||
if (options.order) {
|
||||
builder.orderBy(...options.order);
|
||||
}
|
||||
}),
|
||||
})
|
||||
.groupBy('actors.id', 'avatars.id', 'sfw_media.id', 'entities.id', 'actors_meta.stashed', 'birth_countries.alpha2', 'residence_countries.alpha2'),
|
||||
knex('actors_profiles')
|
||||
.select(
|
||||
'actors_profiles.*',
|
||||
@@ -245,10 +251,12 @@ export async function fetchActorsById(actorIds, options = {}, reqUser) {
|
||||
'media.*',
|
||||
'actors_avatars.actor_id',
|
||||
knex.raw('json_agg(actors_avatars.profile_id) as profile_ids'),
|
||||
knex.raw('row_to_json(sfw_media) as sfw_media'),
|
||||
)
|
||||
.whereIn('actor_id', actorIds)
|
||||
.leftJoin('media', 'media.id', 'actors_avatars.media_id')
|
||||
.groupBy('media.id', 'actors_avatars.actor_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.groupBy('media.id', 'sfw_media.id', 'actors_avatars.actor_id')
|
||||
.orderBy(knex.raw('max(actors_avatars.created_at)'), 'desc'),
|
||||
knex('actors_socials')
|
||||
.whereIn('actor_id', actorIds),
|
||||
|
||||
10
src/argv.js
10
src/argv.js
@@ -1,11 +1,15 @@
|
||||
import yargs from 'yargs';
|
||||
|
||||
const { argv } = yargs()
|
||||
.command('npm start')
|
||||
const { argv } = yargs(process.argv.slice(2))
|
||||
.option('debug', {
|
||||
describe: 'Show error stack traces',
|
||||
describe: 'Show error stack traces and inputs',
|
||||
type: 'boolean',
|
||||
default: process.env.NODE_ENV === 'development',
|
||||
})
|
||||
.option('ip', {
|
||||
describe: 'Mock IP address',
|
||||
type: 'string',
|
||||
default: null,
|
||||
});
|
||||
|
||||
export default argv;
|
||||
|
||||
55
src/censor.js
Normal file
55
src/censor.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import config from 'config';
|
||||
|
||||
import {
|
||||
TextCensor,
|
||||
RegExpMatcher,
|
||||
englishDataset,
|
||||
englishRecommendedTransformers,
|
||||
DataSet,
|
||||
pattern,
|
||||
// asteriskCensorStrategy,
|
||||
} from 'obscenity';
|
||||
|
||||
const textCensor = new TextCensor();
|
||||
|
||||
// built-in asterisk strategy replaces entire word
|
||||
textCensor.setStrategy(({
|
||||
input,
|
||||
startIndex,
|
||||
endIndex,
|
||||
matchLength,
|
||||
}) => {
|
||||
if (matchLength <= 2) {
|
||||
return '*'.repeat(matchLength);
|
||||
}
|
||||
|
||||
return `${input.at(startIndex)}${'*'.repeat(matchLength - 2)}${input.at(endIndex)}`;
|
||||
});
|
||||
|
||||
const dataset = new DataSet().addAll(englishDataset);
|
||||
|
||||
config.restrictions.censors.forEach((word) => {
|
||||
dataset.addPhrase((phrase) => phrase
|
||||
.setMetadata({ originalWord: word })
|
||||
.addPattern(pattern`${word}`));
|
||||
});
|
||||
|
||||
const matcher = new RegExpMatcher({
|
||||
...dataset.build(),
|
||||
...englishRecommendedTransformers,
|
||||
});
|
||||
|
||||
export function censor(text, restriction) {
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!restriction) {
|
||||
return text;
|
||||
}
|
||||
|
||||
const censorMatches = matcher.getAllMatches(text);
|
||||
const censoredText = textCensor.applyTo(text, censorMatches);
|
||||
|
||||
return censoredText;
|
||||
}
|
||||
@@ -3,29 +3,36 @@ import redis from './redis.js';
|
||||
import initLogger from './logger.js';
|
||||
import entityPrefixes from './entities-prefixes.js';
|
||||
import { getAffiliateEntityUrl } from './affiliates.js';
|
||||
import { censor } from './censor.js';
|
||||
|
||||
const logger = initLogger();
|
||||
|
||||
export function curateEntity(entity, context) {
|
||||
export function curateEntity(entity, context = {}) {
|
||||
if (!entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const curatedEntity = {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
name: censor(entity.name, context.restriction),
|
||||
slug: entity.slug,
|
||||
type: entity.type,
|
||||
url: entity.url,
|
||||
isIndependent: entity.independent,
|
||||
hasLogo: entity.has_logo,
|
||||
hasLogo: context.restriction ? false : entity.has_logo,
|
||||
parent: curateEntity(entity.parent, context),
|
||||
tags: context?.tags?.map((tag) => ({
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
slug: tag.slug,
|
||||
})),
|
||||
children: context?.children?.filter((child) => child.parent_id === entity.id).map((child) => curateEntity({ ...child, parent: entity }, { parent: entity })) || [],
|
||||
children: context?.children?.filter((child) => child.parent_id === entity.id).map((child) => curateEntity({
|
||||
...child,
|
||||
parent: entity,
|
||||
}, {
|
||||
parent: entity,
|
||||
restriction: context.restriction,
|
||||
})) || [],
|
||||
affiliate: entity.affiliate ? {
|
||||
id: entity.affiliate.id,
|
||||
entityId: entity.affiliate.entity_id,
|
||||
@@ -44,7 +51,7 @@ export function curateEntity(entity, context) {
|
||||
return curatedEntity;
|
||||
}
|
||||
|
||||
export async function fetchEntities(options = {}) {
|
||||
export async function fetchEntities(options = {}, context) {
|
||||
const entities = await knex('entities')
|
||||
.select('entities.*', knex.raw('row_to_json(parents) as parent'))
|
||||
.modify((builder) => {
|
||||
@@ -93,11 +100,12 @@ export async function fetchEntities(options = {}) {
|
||||
.whereIn('entity_id', entities.map((entity) => entity.id));
|
||||
|
||||
return entities.map((entityEntry) => curateEntity(entityEntry, {
|
||||
...context,
|
||||
tags: entitiesTags.filter((tag) => tag.entity_id === entityEntry.id),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
|
||||
export async function fetchEntitiesById(entityIds, options = {}, reqUser, context) {
|
||||
const [entities, children, tags, alerts] = await Promise.all([
|
||||
knex('entities')
|
||||
.select(
|
||||
@@ -136,6 +144,7 @@ export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
|
||||
|
||||
if (options.order) {
|
||||
return entities.map((entityEntry) => curateEntity(entityEntry, {
|
||||
...context,
|
||||
append: options.append,
|
||||
children: children.filter((channel) => channel.parent_id === entityEntry.id),
|
||||
alerts: alerts.filter((alert) => alert.entity_id === entityEntry.id),
|
||||
@@ -151,6 +160,7 @@ export async function fetchEntitiesById(entityIds, options = {}, reqUser) {
|
||||
}
|
||||
|
||||
return curateEntity(entity, {
|
||||
...context,
|
||||
append: options.append,
|
||||
children: children.filter((channel) => channel.parent_id === entity.id),
|
||||
tags: tags.filter((tag) => tag.entity_id === entity.id),
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
// import config from 'config';
|
||||
import { pageContext } from '../renderer/usePageContext.js';
|
||||
|
||||
function getBasePath(media, type, options) {
|
||||
/*
|
||||
if (store.state.ui.sfw) {
|
||||
return config.media.assetPath;
|
||||
function getBasePath(media, options) {
|
||||
if (pageContext.restriction) {
|
||||
return pageContext.env.media.assetPath;
|
||||
}
|
||||
*/
|
||||
|
||||
if (media.isS3) {
|
||||
return options.s3Path;
|
||||
@@ -20,15 +17,13 @@ function getBasePath(media, type, options) {
|
||||
}
|
||||
|
||||
function getFilename(media, type, options) {
|
||||
/*
|
||||
if (store.state.ui.sfw && type && !options?.original) {
|
||||
return media.sfw[type];
|
||||
if (pageContext.restriction && type && !options?.original) {
|
||||
return media.sfw?.[type];
|
||||
}
|
||||
|
||||
if (store.state.ui.sfw) {
|
||||
return media.sfw.path;
|
||||
if (pageContext.restriction) {
|
||||
return media.sfw?.path;
|
||||
}
|
||||
*/
|
||||
|
||||
if (type && !options?.original) {
|
||||
return media[type];
|
||||
@@ -42,7 +37,7 @@ export default function getPath(media, type, options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const path = getBasePath(media, type, { ...pageContext.env.media, ...options });
|
||||
const path = getBasePath(media, { ...pageContext.env.media, ...options });
|
||||
const filename = getFilename(media, type, { ...pageContext.env.media, ...options });
|
||||
|
||||
return `${path}/${filename}`;
|
||||
|
||||
@@ -30,6 +30,7 @@ export function curateMedia(media, context = {}) {
|
||||
parent: media.entity_parent,
|
||||
}),
|
||||
type: context.type || null,
|
||||
sfw: curateMedia(media.sfw_media),
|
||||
isRestricted: context.isRestricted,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,29 +8,30 @@ import { curateMedia } from './media.js';
|
||||
import { fetchTagsById } from './tags.js';
|
||||
import { fetchEntitiesById } from './entities.js';
|
||||
import { curateStash } from './stashes.js';
|
||||
import { censor } from './censor.js';
|
||||
import escape from '../utils/escape-manticore.js';
|
||||
import promiseProps from '../utils/promise-props.js';
|
||||
|
||||
function curateMovie(rawMovie, assets) {
|
||||
function curateMovie(rawMovie, assets, context = {}) {
|
||||
if (!rawMovie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: rawMovie.id,
|
||||
title: rawMovie.title,
|
||||
title: censor(rawMovie.title, context.restriction),
|
||||
slug: rawMovie.slug,
|
||||
url: rawMovie.url,
|
||||
date: rawMovie.date,
|
||||
datePrecision: rawMovie.date_precision,
|
||||
createdAt: rawMovie.created_at,
|
||||
effectiveDate: rawMovie.effective_date,
|
||||
description: rawMovie.description,
|
||||
description: censor(rawMovie.description, context.restriction),
|
||||
duration: rawMovie.duration,
|
||||
channel: {
|
||||
id: assets.channel.id,
|
||||
slug: assets.channel.slug,
|
||||
name: assets.channel.name,
|
||||
name: censor(assets.channel.name, context.restriction),
|
||||
type: assets.channel.type,
|
||||
isIndependent: assets.channel.independent,
|
||||
hasLogo: assets.channel.has_logo,
|
||||
@@ -38,7 +39,7 @@ function curateMovie(rawMovie, assets) {
|
||||
network: assets.channel.network_id ? {
|
||||
id: assets.channel.network_id,
|
||||
slug: assets.channel.network_slug,
|
||||
name: assets.channel.network_name,
|
||||
name: censor(assets.channel.network_name, context.restriction),
|
||||
type: assets.channel.network_type,
|
||||
hasLogo: assets.channel.has_logo,
|
||||
} : null,
|
||||
@@ -51,7 +52,7 @@ function curateMovie(rawMovie, assets) {
|
||||
tags: assets.tags.map((tag) => ({
|
||||
id: tag.id,
|
||||
slug: tag.slug,
|
||||
name: tag.name,
|
||||
name: censor(tag.name, context.restriction),
|
||||
})),
|
||||
// poster: curateMedia(assets.poster),
|
||||
covers: assets.covers.map((cover) => curateMedia(cover, { type: 'cover' })),
|
||||
@@ -64,7 +65,7 @@ function curateMovie(rawMovie, assets) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchMoviesById(movieIds, reqUser) {
|
||||
export async function fetchMoviesById(movieIds, reqUser, context) {
|
||||
const {
|
||||
movies,
|
||||
channels,
|
||||
@@ -123,20 +124,25 @@ export async function fetchMoviesById(movieIds, reqUser) {
|
||||
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
|
||||
.orderBy('priority', 'desc'),
|
||||
covers: knex('movies_covers')
|
||||
.select('media.*', 'movies_covers.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.whereIn('movie_id', movieIds)
|
||||
.leftJoin('media', 'media.id', 'movies_covers.media_id')
|
||||
.orderBy('media.index'),
|
||||
photos: knex.transaction(async (trx) => {
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.orderBy('media.index')
|
||||
.groupBy('media.id', 'movies_covers.movie_id', 'sfw_media.id'),
|
||||
photos: context.restriction ? [] : knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
|
||||
}
|
||||
|
||||
return trx('movies_scenes')
|
||||
.select('media.*', 'movies_scenes.movie_id')
|
||||
.select('media.*', 'movies_scenes.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.whereIn('movies_scenes.movie_id', movieIds)
|
||||
.whereNotNull('media.id')
|
||||
.leftJoin('releases_photos', 'releases_photos.release_id', 'movies_scenes.scene_id')
|
||||
.leftJoin('media', 'media.id', 'releases_photos.media_id');
|
||||
.leftJoin('media', 'media.id', 'releases_photos.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.groupBy('media.id', 'movies_scenes.movie_id', 'sfw_media.id');
|
||||
}),
|
||||
caps: knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
@@ -144,11 +150,13 @@ export async function fetchMoviesById(movieIds, reqUser) {
|
||||
}
|
||||
|
||||
return trx('movies_scenes')
|
||||
.select('media.*', 'movies_scenes.movie_id')
|
||||
.select('media.*', 'movies_scenes.movie_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.whereIn('movies_scenes.movie_id', movieIds)
|
||||
.whereNotNull('media.id')
|
||||
.leftJoin('releases_caps', 'releases_caps.release_id', 'movies_scenes.scene_id')
|
||||
.leftJoin('media', 'media.id', 'releases_caps.media_id');
|
||||
.leftJoin('media', 'media.id', 'releases_caps.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.groupBy('media.id', 'movies_scenes.movie_id', 'sfw_media.id');
|
||||
}),
|
||||
trailers: knex('movies_trailers')
|
||||
.whereIn('movie_id', movieIds)
|
||||
@@ -192,7 +200,7 @@ export async function fetchMoviesById(movieIds, reqUser) {
|
||||
caps: movieCaps,
|
||||
trailer: movieTrailer,
|
||||
stashes: movieStashes,
|
||||
});
|
||||
}, context);
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -398,7 +406,7 @@ function countAggregations(buckets) {
|
||||
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
|
||||
}
|
||||
|
||||
export async function fetchMovies(filters, rawOptions, reqUser) {
|
||||
export async function fetchMovies(filters, rawOptions, reqUser, context) {
|
||||
const options = curateOptions(rawOptions);
|
||||
|
||||
console.log(options);
|
||||
@@ -413,13 +421,13 @@ export async function fetchMovies(filters, rawOptions, reqUser) {
|
||||
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds);
|
||||
|
||||
const [aggActors, aggTags, aggChannels] = await Promise.all([
|
||||
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }) : [],
|
||||
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }) : [],
|
||||
options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }) : [],
|
||||
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }, reqUser) : [],
|
||||
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }, reqUser, context) : [],
|
||||
options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }, reqUser, context) : [],
|
||||
]);
|
||||
|
||||
const movieIds = result.movies.map((movie) => Number(movie.id));
|
||||
const movies = await fetchMoviesById(movieIds, reqUser);
|
||||
const movies = await fetchMoviesById(movieIds, reqUser, context);
|
||||
|
||||
return {
|
||||
movies,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import config from 'config';
|
||||
import { MerkleJson } from 'merkle-json';
|
||||
|
||||
import argv from './argv.js';
|
||||
import { knexQuery as knex, knexOwner, knexManticore } from './knex.js';
|
||||
import { utilsApi } from './manticore.js';
|
||||
import { HttpError } from './errors.js';
|
||||
@@ -15,18 +16,19 @@ import promiseProps from '../utils/promise-props.js';
|
||||
import initLogger from './logger.js';
|
||||
import { curateRevision } from './revisions.js';
|
||||
import { getAffiliateSceneUrl } from './affiliates.js';
|
||||
import { censor } from './censor.js';
|
||||
|
||||
const logger = initLogger();
|
||||
const mj = new MerkleJson();
|
||||
|
||||
function curateScene(rawScene, assets, reqUser) {
|
||||
function curateScene(rawScene, assets, reqUser, context) {
|
||||
if (!rawScene) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const curatedScene = {
|
||||
id: rawScene.id,
|
||||
title: rawScene.title,
|
||||
title: censor(rawScene.title, context.restriction),
|
||||
slug: rawScene.slug,
|
||||
url: rawScene.url,
|
||||
entryId: rawScene.entry_id,
|
||||
@@ -34,14 +36,14 @@ function curateScene(rawScene, assets, reqUser) {
|
||||
datePrecision: rawScene.date_precision,
|
||||
createdAt: rawScene.created_at,
|
||||
effectiveDate: rawScene.effective_date,
|
||||
description: rawScene.description,
|
||||
description: censor(rawScene.description, context.restriction),
|
||||
duration: rawScene.duration,
|
||||
shootId: rawScene.shoot_id,
|
||||
productionDate: rawScene.production_date,
|
||||
channel: {
|
||||
id: assets.channel.id,
|
||||
slug: assets.channel.slug,
|
||||
name: assets.channel.name,
|
||||
name: censor(assets.channel.name, context.restriction),
|
||||
type: assets.channel.type,
|
||||
isIndependent: assets.channel.independent,
|
||||
hasLogo: assets.channel.has_logo,
|
||||
@@ -49,7 +51,7 @@ function curateScene(rawScene, assets, reqUser) {
|
||||
network: assets.channel.network_id ? {
|
||||
id: assets.channel.network_id,
|
||||
slug: assets.channel.network_slug,
|
||||
name: assets.channel.network_name,
|
||||
name: censor(assets.channel.network_name, context.restriction),
|
||||
type: assets.channel.network_type,
|
||||
hasLogo: assets.channel.network_has_logo,
|
||||
} : null,
|
||||
@@ -78,7 +80,7 @@ function curateScene(rawScene, assets, reqUser) {
|
||||
tags: assets.tags.map((tag) => ({
|
||||
id: tag.id,
|
||||
slug: tag.slug,
|
||||
name: tag.name,
|
||||
name: censor(tag.name, context.restriction),
|
||||
priority: tag.priority,
|
||||
})),
|
||||
chapters: assets.chapters.map((chapter) => ({
|
||||
@@ -86,7 +88,9 @@ function curateScene(rawScene, assets, reqUser) {
|
||||
title: chapter.title,
|
||||
time: chapter.time,
|
||||
duration: chapter.duration,
|
||||
poster: curateMedia(chapter.chapter_poster),
|
||||
poster: context.restriction
|
||||
? null
|
||||
: (chapter.chapter_poster),
|
||||
tags: chapter.chapter_tags.map((tag) => ({
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
@@ -98,14 +102,18 @@ function curateScene(rawScene, assets, reqUser) {
|
||||
movies: assets.movies.map((movie) => ({
|
||||
id: movie.id,
|
||||
slug: movie.slug,
|
||||
title: movie.title,
|
||||
covers: movie.movie_covers?.map((cover) => curateMedia(cover, { type: 'cover' })).toSorted((coverA, coverB) => coverA.index - coverB.index) || [],
|
||||
title: censor(movie.title, context.restriction),
|
||||
covers: movie.movie_covers && !context.restriction
|
||||
? movie.movie_covers.map((cover) => curateMedia(cover, { type: 'cover' })).toSorted((coverA, coverB) => coverA.index - coverB.index)
|
||||
: [],
|
||||
})),
|
||||
series: assets.series.map((serie) => ({
|
||||
id: serie.id,
|
||||
slug: serie.slug,
|
||||
title: serie.title,
|
||||
poster: curateMedia(serie.serie_poster, { type: 'poster' }),
|
||||
poster: context.restriction
|
||||
? null
|
||||
: (serie.serie_poster, { type: 'poster' }),
|
||||
})),
|
||||
poster: curateMedia(assets.poster, { type: 'poster' }),
|
||||
photos: assets.photos?.map((photo) => curateMedia(photo, { type: 'photo' })) || [],
|
||||
@@ -189,14 +197,17 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
.select(
|
||||
'actors.*',
|
||||
knex.raw('row_to_json(avatars) as avatar'),
|
||||
knex.raw('row_to_json(sfw_media) as sfw_avatar'),
|
||||
'countries.name as birth_country_name',
|
||||
'countries.alias as birth_country_alias',
|
||||
'releases_actors.release_id',
|
||||
)
|
||||
.leftJoin('actors', 'actors.id', 'releases_actors.actor_id')
|
||||
.leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'avatars.sfw_media_id')
|
||||
.leftJoin('countries', 'countries.alpha2', 'actors.birth_country_alpha2')
|
||||
.whereIn('release_id', sceneIds),
|
||||
.whereIn('release_id', sceneIds)
|
||||
.groupBy('actors.id', 'releases_actors.release_id', 'avatars.id', 'countries.name', 'countries.alias', 'sfw_media.id'),
|
||||
directors: knex('releases_directors')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
|
||||
@@ -234,17 +245,23 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
.whereIn('scene_id', sceneIds)
|
||||
.groupBy('series.id', 'series_scenes.scene_id', 'media.*') : [],
|
||||
posters: knex('releases_posters')
|
||||
.select('media.*', 'releases_posters.release_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('media', 'media.id', 'releases_posters.media_id'),
|
||||
photos: context.includeAssets ? knex.transaction(async (trx) => {
|
||||
.leftJoin('media', 'media.id', 'releases_posters.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.groupBy('media.id', 'releases_posters.release_id', 'sfw_media.id'),
|
||||
photos: context.includeAssets && !context.restriction ? knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
await trx.select(knex.raw('set_config(\'user.id\', :userId, true)', { userId: reqUser.id }));
|
||||
}
|
||||
|
||||
return trx('releases_photos')
|
||||
.select('media.*', 'releases_photos.release_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.leftJoin('media', 'media.id', 'releases_photos.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.orderBy('index');
|
||||
.orderBy('index')
|
||||
.groupBy('media.id', 'releases_photos.release_id', 'sfw_media.id');
|
||||
}) : [],
|
||||
caps: context.includeAssets ? knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
@@ -252,9 +269,12 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
}
|
||||
|
||||
return trx('releases_caps')
|
||||
.select('media.*', 'releases_caps.release_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.leftJoin('media', 'media.id', 'releases_caps.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.orderBy('index');
|
||||
.orderBy('index')
|
||||
.groupBy('media.id', 'releases_caps.release_id', 'sfw_media.id');
|
||||
}) : [],
|
||||
trailers: context.includeAssets ? knex.transaction(async (trx) => {
|
||||
if (reqUser) {
|
||||
@@ -350,7 +370,7 @@ export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
|
||||
stashes: sceneStashes,
|
||||
actorStashes: sceneActorStashes,
|
||||
lastBatchId: lastBatch?.id,
|
||||
}, reqUser);
|
||||
}, reqUser, context);
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
@@ -545,7 +565,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
|
||||
? sqlQuery
|
||||
: sqlQuery.replace(/scenes\./g, '');
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (process.env.NODE_ENV === 'development' && argv.debug) {
|
||||
console.log(curatedSqlQuery);
|
||||
}
|
||||
|
||||
@@ -603,14 +623,18 @@ function countAggregations(buckets) {
|
||||
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
|
||||
}
|
||||
|
||||
export async function fetchScenes(filters, rawOptions, reqUser) {
|
||||
export async function fetchScenes(filters, rawOptions, reqUser, context) {
|
||||
const options = curateOptions(rawOptions);
|
||||
|
||||
console.log('filters', filters);
|
||||
console.log('options', options);
|
||||
if (argv.debug) {
|
||||
console.log('filters', filters);
|
||||
console.log('options', options);
|
||||
}
|
||||
|
||||
console.time('manticore sql');
|
||||
|
||||
const result = await queryManticoreSql(filters, options, reqUser);
|
||||
|
||||
console.timeEnd('manticore sql');
|
||||
|
||||
const aggYears = options.aggregateYears && result.aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count }));
|
||||
@@ -623,16 +647,16 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
|
||||
console.time('fetch aggregations');
|
||||
|
||||
const [aggActors, aggTags, aggChannels] = await Promise.all([
|
||||
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }) : [],
|
||||
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }) : [],
|
||||
options.aggregateChannels ? fetchEntitiesById(entityIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }) : [],
|
||||
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: actorCounts }, reqUser) : [],
|
||||
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: [knex.raw('lower(name)'), 'asc'], append: tagCounts }, reqUser, context) : [],
|
||||
options.aggregateChannels ? fetchEntitiesById(entityIds.map((bucket) => bucket.key), { order: ['slug', 'asc'], append: channelCounts }, reqUser, context) : [],
|
||||
]);
|
||||
|
||||
console.timeEnd('fetch aggregations');
|
||||
|
||||
console.time('fetch full');
|
||||
const sceneIds = result.scenes.map((scene) => Number(scene.id));
|
||||
const scenes = await fetchScenesById(sceneIds, { reqUser });
|
||||
const scenes = await fetchScenesById(sceneIds, { reqUser, ...context });
|
||||
console.timeEnd('fetch full');
|
||||
|
||||
return {
|
||||
|
||||
21
src/tags.js
21
src/tags.js
@@ -1,6 +1,7 @@
|
||||
import knex from './knex.js';
|
||||
import redis from './redis.js';
|
||||
import initLogger from './logger.js';
|
||||
import { censor } from './censor.js';
|
||||
|
||||
import { curateMedia } from './media.js';
|
||||
|
||||
@@ -9,9 +10,9 @@ const logger = initLogger();
|
||||
function curateTag(tag, context) {
|
||||
return {
|
||||
id: tag.id,
|
||||
name: tag.name,
|
||||
name: censor(tag.name, context.restriction),
|
||||
slug: tag.slug,
|
||||
description: tag.description,
|
||||
description: context.restriction ? null : tag.description, // censor interferes with markdown
|
||||
priority: tag.priority,
|
||||
poster: tag.poster && curateMedia(tag.poster),
|
||||
photos: tag.photos?.map((photo) => curateMedia(photo)) || [],
|
||||
@@ -55,10 +56,13 @@ export async function fetchTags(options = {}) {
|
||||
}
|
||||
}),
|
||||
knex('tags_posters')
|
||||
.select('media.*', 'tags_posters.tag_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.select('tags_posters.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
|
||||
.leftJoin('media', 'media.id', 'tags_posters.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.leftJoin('entities', 'entities.id', 'media.entity_id')
|
||||
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id'),
|
||||
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
|
||||
.groupBy('media.id', 'tags_posters.tag_id', 'sfw_media.id', 'entities.id', 'parents.id'),
|
||||
]);
|
||||
|
||||
const postersByTagId = Object.fromEntries(posters.map((poster) => [poster.tag_id, poster]));
|
||||
@@ -69,7 +73,7 @@ export async function fetchTags(options = {}) {
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
||||
export async function fetchTagsById(tagIds, options = {}, reqUser, context = {}) {
|
||||
const [tags, posters, photos, alerts] = await Promise.all([
|
||||
knex('tags')
|
||||
.whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number'))
|
||||
@@ -80,14 +84,17 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
||||
}
|
||||
}),
|
||||
knex('tags_posters')
|
||||
.select('media.*', 'tags_posters.tag_id', knex.raw('row_to_json(sfw_media) as sfw_media'))
|
||||
.select('tags_posters.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
|
||||
.leftJoin('tags', 'tags.id', 'tags_posters.tag_id')
|
||||
.leftJoin('media', 'media.id', 'tags_posters.media_id')
|
||||
.leftJoin('media as sfw_media', 'sfw_media.id', 'media.sfw_media_id')
|
||||
.leftJoin('entities', 'entities.id', 'media.entity_id')
|
||||
.leftJoin('entities as parents', 'parents.id', 'entities.parent_id')
|
||||
.whereIn('tags.id', tagIds.filter((tagId) => typeof tagId === 'number'))
|
||||
.orWhereIn('tags.slug', tagIds.filter((tagId) => typeof tagId === 'string')),
|
||||
knex('tags_photos')
|
||||
.orWhereIn('tags.slug', tagIds.filter((tagId) => typeof tagId === 'string'))
|
||||
.groupBy('media.id', 'tags_posters.tag_id', 'sfw_media.id', 'entities.id', 'parents.id'),
|
||||
context.restriction ? [] : knex('tags_photos')
|
||||
.select('tags_photos.tag_id', 'media.*', knex.raw('row_to_json(entities) as entity'), knex.raw('row_to_json(parents) as entity_parent'))
|
||||
.leftJoin('tags', 'tags.id', 'tags_photos.tag_id')
|
||||
.leftJoin('media', 'media.id', 'tags_photos.media_id')
|
||||
@@ -118,6 +125,7 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
||||
}, {
|
||||
alerts: alerts.filter((alert) => alert.tag_id === tagEntry.id),
|
||||
append: options.append,
|
||||
...context,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -136,6 +144,7 @@ export async function fetchTagsById(tagIds, options = {}, reqUser) {
|
||||
}, {
|
||||
alerts: alerts.filter((alert) => alert.tag_id === tag.id),
|
||||
append: options.append,
|
||||
...context,
|
||||
});
|
||||
}).filter(Boolean);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */
|
||||
import IPCIDR from 'ip-cidr';
|
||||
import argv from '../argv.js';
|
||||
|
||||
import {
|
||||
login,
|
||||
@@ -14,6 +15,10 @@ import {
|
||||
import { fetchUser } from '../users.js';
|
||||
|
||||
function getIp(req) {
|
||||
if (argv.ip) {
|
||||
return argv.ip;
|
||||
}
|
||||
|
||||
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.connection.remoteAddress;
|
||||
|
||||
const unmappedIp = ip?.includes('.')
|
||||
|
||||
@@ -51,6 +51,7 @@ export default async function mainHandler(req, res, next) {
|
||||
siteKey: config.auth.captcha.siteKey,
|
||||
},
|
||||
},
|
||||
restriction: req.restriction,
|
||||
meta: {
|
||||
now: new Date().toISOString(),
|
||||
},
|
||||
|
||||
80
src/web/restrictions.js
Normal file
80
src/web/restrictions.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import config from 'config';
|
||||
import path from 'path';
|
||||
import { Reader } from '@maxmind/geoip2-node';
|
||||
|
||||
import initLogger from '../logger.js';
|
||||
|
||||
const logger = initLogger();
|
||||
const regions = config.restrictions.regions;
|
||||
|
||||
export default async function initRestrictionHandler() {
|
||||
const reader = await Reader.open('assets/GeoLite2-City.mmdb');
|
||||
|
||||
function getRestriction(req) {
|
||||
if (req.session.restriction && req.session.country && req.session.restrictionIp === req.userIp) {
|
||||
return {
|
||||
restriction: req.session.restriction,
|
||||
country: req.session.country,
|
||||
};
|
||||
}
|
||||
|
||||
const location = reader.city(req.userIp);
|
||||
const country = location.country.isoCode;
|
||||
const subdivision = location.subdivisions?.[0]?.isoCode;
|
||||
|
||||
if (regions[country]?.[subdivision]) {
|
||||
// state or province restriction
|
||||
return {
|
||||
restriction: config.restrictions.modes[regions[country][subdivision]],
|
||||
country,
|
||||
};
|
||||
}
|
||||
|
||||
if (regions[country]) {
|
||||
// country restriction
|
||||
return {
|
||||
restriction: config.restrictions.modes[regions[country]],
|
||||
country,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
restriction: null,
|
||||
country,
|
||||
};
|
||||
}
|
||||
|
||||
function restrictionHandler(req, res, next) {
|
||||
if (!config.restrictions.enabled) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { restriction, country } = getRestriction(req);
|
||||
|
||||
if (restriction === 'block' || req.path === '/sfw/') {
|
||||
res.render(path.join(import.meta.dirname, '../../assets/sfw.ejs'), {
|
||||
noVpn: config.restrictions.noVpn.includes(country),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.session.restriction !== restriction) {
|
||||
req.session.restrictionIp = req.userIp;
|
||||
req.session.restriction = restriction;
|
||||
req.session.country = country;
|
||||
}
|
||||
|
||||
req.restriction = restriction;
|
||||
req.country = country;
|
||||
} catch (error) {
|
||||
logger.error(`Failed Maxmind IP lookup for ${req.ip}: ${error.message}`);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
return restrictionHandler;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import redis from '../redis.js';
|
||||
|
||||
import errorHandler from './error.js';
|
||||
import consentHandler from './consent.js';
|
||||
import initRestrictionHandler from './restrictions.js';
|
||||
|
||||
import { scenesRouter } from './scenes.js';
|
||||
import { actorsRouter } from './actors.js';
|
||||
@@ -48,9 +49,11 @@ const isProduction = process.env.NODE_ENV === 'production';
|
||||
export default async function initServer() {
|
||||
const app = express();
|
||||
const router = Router();
|
||||
const restrictionHandler = await initRestrictionHandler();
|
||||
|
||||
app.use(compression());
|
||||
app.disable('x-powered-by');
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
router.use(boolParser());
|
||||
|
||||
@@ -58,7 +61,7 @@ export default async function initServer() {
|
||||
router.use('/', express.static('static'));
|
||||
router.use('/media', express.static(config.media.path));
|
||||
|
||||
router.use((req, res, next) => {
|
||||
router.use((req, _res, next) => {
|
||||
if (req.headers.cookie) {
|
||||
const cookies = cookie.parse(req.headers.cookie);
|
||||
|
||||
@@ -109,11 +112,13 @@ export default async function initServer() {
|
||||
router.use(viteDevMiddleware);
|
||||
}
|
||||
|
||||
router.get('/consent', (req, res) => {
|
||||
router.use(restrictionHandler);
|
||||
|
||||
router.get('/consent', (_req, res) => {
|
||||
res.sendFile(path.join(import.meta.dirname, '../../assets/consent.html'));
|
||||
});
|
||||
|
||||
router.use('/api/*', async (req, res, next) => {
|
||||
router.use('/api/*', async (req, _res, next) => {
|
||||
if (req.headers['api-user']) {
|
||||
await verifyKey(req.headers['api-user'], req.headers['api-key'], req);
|
||||
|
||||
@@ -159,7 +164,7 @@ export default async function initServer() {
|
||||
|
||||
router.use(consentHandler);
|
||||
|
||||
router.use((req, res, next) => {
|
||||
router.use((_req, res, next) => {
|
||||
/* eslint-disable no-param-reassign */
|
||||
res.set('Accept-CH', 'Sec-CH-Prefers-Color-Scheme');
|
||||
res.set('Vary', 'Sec-CH-Prefers-Color-Scheme');
|
||||
|
||||
Reference in New Issue
Block a user