2024-03-03 01:33:35 +00:00
|
|
|
import config from 'config';
|
2024-03-27 01:28:21 +00:00
|
|
|
import { CronJob } from 'cron';
|
2024-03-03 01:33:35 +00:00
|
|
|
|
|
|
|
import { knexOwner as knex } from './knex.js';
|
2024-03-14 23:08:24 +00:00
|
|
|
import { indexApi } from './manticore.js';
|
2024-03-03 01:33:35 +00:00
|
|
|
import { HttpError } from './errors.js';
|
|
|
|
import slugify from './utils/slugify.js';
|
|
|
|
import initLogger from './logger.js';
|
|
|
|
|
|
|
|
const logger = initLogger();
|
|
|
|
|
2024-03-27 01:28:21 +00:00
|
|
|
const lastViewRefresh = {
|
|
|
|
actors: 0,
|
|
|
|
stashes: 0,
|
|
|
|
};
|
2024-03-03 01:33:35 +00:00
|
|
|
|
2024-03-14 23:08:24 +00:00
|
|
|
export function curateStash(stash, assets = {}) {
|
2024-03-03 01:33:35 +00:00
|
|
|
if (!stash) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const curatedStash = {
|
|
|
|
id: stash.id,
|
|
|
|
name: stash.name,
|
|
|
|
slug: stash.slug,
|
2024-03-26 23:06:03 +00:00
|
|
|
isPrimary: stash.primary,
|
2024-03-03 01:33:35 +00:00
|
|
|
public: stash.public,
|
|
|
|
createdAt: stash.created_at,
|
2024-03-26 03:14:42 +00:00
|
|
|
stashedScenes: stash.stashed_scenes ?? null,
|
|
|
|
stashedMovies: stash.stashed_movies ?? null,
|
|
|
|
stashedActors: stash.stashed_actors ?? null,
|
2024-03-14 23:08:24 +00:00
|
|
|
user: assets.user ? {
|
|
|
|
id: assets.user.id,
|
|
|
|
username: assets.user.username,
|
|
|
|
avatar: `/media/avatars/${assets.user.id}_${assets.user.username}.png`,
|
|
|
|
createdAt: assets.user.created_at,
|
|
|
|
} : null,
|
2024-03-03 01:33:35 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
return curatedStash;
|
|
|
|
}
|
|
|
|
|
|
|
|
function curateStashEntry(stash, user) {
|
|
|
|
const curatedStashEntry = {
|
2024-03-26 23:06:03 +00:00
|
|
|
user_id: user?.id || undefined,
|
|
|
|
name: stash.name || undefined,
|
|
|
|
slug: slugify(stash.name) || undefined,
|
|
|
|
public: stash.public ?? false,
|
2024-03-03 01:33:35 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
return curatedStashEntry;
|
|
|
|
}
|
|
|
|
|
2024-03-14 23:08:24 +00:00
|
|
|
function verifyStashAccess(stash, sessionUser) {
|
|
|
|
if (!stash || (!stash.public && stash.user_id !== sessionUser?.id)) {
|
|
|
|
throw new HttpError('This stash does not exist, or you are not allowed access.', 404);
|
2024-03-03 01:33:35 +00:00
|
|
|
}
|
2024-03-14 23:08:24 +00:00
|
|
|
}
|
2024-03-03 01:33:35 +00:00
|
|
|
|
2024-03-14 23:08:24 +00:00
|
|
|
export async function fetchStashById(stashId, sessionUser) {
|
2024-03-03 01:33:35 +00:00
|
|
|
const stash = await knex('stashes')
|
2024-03-14 23:08:24 +00:00
|
|
|
.where('id', stashId)
|
2024-03-03 01:33:35 +00:00
|
|
|
.first();
|
|
|
|
|
2024-03-14 23:08:24 +00:00
|
|
|
verifyStashAccess(stash, sessionUser);
|
2024-03-03 01:33:35 +00:00
|
|
|
|
|
|
|
return curateStash(stash);
|
|
|
|
}
|
|
|
|
|
2024-03-14 23:08:24 +00:00
|
|
|
export async function fetchStashByUsernameAndSlug(username, stashSlug, sessionUser) {
|
|
|
|
const user = await knex('users').where('username', username).first();
|
|
|
|
|
|
|
|
if (!user) {
|
|
|
|
throw new HttpError('This user does not exist.', 404);
|
|
|
|
}
|
|
|
|
|
|
|
|
const stash = await knex('stashes')
|
|
|
|
.select('stashes.*', 'stashes_meta.*')
|
|
|
|
.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id')
|
|
|
|
.where('slug', stashSlug)
|
|
|
|
.where('user_id', user.id)
|
|
|
|
.first();
|
|
|
|
|
|
|
|
verifyStashAccess(stash, sessionUser);
|
|
|
|
|
|
|
|
return curateStash(stash, { user });
|
|
|
|
}
|
|
|
|
|
2024-03-03 01:33:35 +00:00
|
|
|
export async function fetchStashes(domain, itemId, sessionUser) {
|
|
|
|
const stashes = await knex(`stashes_${domain}s`)
|
|
|
|
.select('stashes.*')
|
2024-03-27 01:28:21 +00:00
|
|
|
.leftJoin('stashes', 'stashes.id', `stashes_${domain}s.stash_id`)
|
2024-03-03 01:33:35 +00:00
|
|
|
.where({
|
|
|
|
[`${domain}_id`]: itemId,
|
|
|
|
user_id: sessionUser.id,
|
2024-03-27 01:28:21 +00:00
|
|
|
});
|
2024-03-03 01:33:35 +00:00
|
|
|
|
|
|
|
return stashes.map((stash) => curateStash(stash));
|
|
|
|
}
|
|
|
|
|
2024-03-26 23:06:03 +00:00
|
|
|
function verifyStashName(stash) {
|
|
|
|
if (!stash.name) {
|
2024-03-26 03:14:42 +00:00
|
|
|
throw new HttpError('Stash name required', 400);
|
|
|
|
}
|
|
|
|
|
2024-03-26 23:06:03 +00:00
|
|
|
if (stash.name.length < config.stashes.nameLength[0]) {
|
2024-03-26 03:14:42 +00:00
|
|
|
throw new HttpError('Stash name is too short', 400);
|
|
|
|
}
|
|
|
|
|
2024-03-26 23:06:03 +00:00
|
|
|
if (stash.name.length > config.stashes.nameLength[1]) {
|
2024-03-26 03:14:42 +00:00
|
|
|
throw new HttpError('Stash name is too long', 400);
|
|
|
|
}
|
|
|
|
|
2024-03-26 23:06:03 +00:00
|
|
|
if (!config.stashes.namePattern.test(stash.name)) {
|
2024-03-26 03:14:42 +00:00
|
|
|
throw new HttpError('Stash name contains invalid characters', 400);
|
|
|
|
}
|
2024-03-26 23:06:03 +00:00
|
|
|
}
|
|
|
|
|
2024-03-27 01:28:21 +00:00
|
|
|
export async function refreshView(domain = 'stashes') {
|
|
|
|
// throttle view refreshes
|
|
|
|
if (new Date() - lastViewRefresh[domain] > config.stashes.viewRefreshCooldowns[domain] * 60000) {
|
|
|
|
lastViewRefresh[domain] = new Date();
|
|
|
|
|
|
|
|
logger.verbose(`Refreshing ${domain} view`);
|
|
|
|
|
|
|
|
await knex.schema.refreshMaterializedView(`${domain}_meta`);
|
|
|
|
await knex.schema.refreshMaterializedView('stashes_meta');
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.debug(`Skipping ${domain} view refresh`);
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-03-26 23:06:03 +00:00
|
|
|
export async function createStash(newStash, sessionUser) {
|
|
|
|
if (!sessionUser) {
|
|
|
|
throw new HttpError('You are not authenthicated', 401);
|
|
|
|
}
|
|
|
|
|
|
|
|
verifyStashName(newStash);
|
2024-03-26 03:14:42 +00:00
|
|
|
|
2024-03-03 01:33:35 +00:00
|
|
|
try {
|
|
|
|
const stash = await knex('stashes')
|
|
|
|
.insert(curateStashEntry(newStash, sessionUser))
|
|
|
|
.returning('*');
|
|
|
|
|
|
|
|
return curateStash(stash);
|
|
|
|
} catch (error) {
|
|
|
|
if (error.routine === '_bt_check_unique') {
|
|
|
|
throw new HttpError('Stash name should be unique', 409);
|
|
|
|
}
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-26 23:06:03 +00:00
|
|
|
export async function updateStash(stashId, updatedStash, sessionUser) {
|
2024-03-03 01:33:35 +00:00
|
|
|
if (!sessionUser) {
|
|
|
|
throw new HttpError('You are not authenthicated', 401);
|
|
|
|
}
|
|
|
|
|
2024-03-26 23:06:03 +00:00
|
|
|
if (updatedStash.name) {
|
|
|
|
verifyStashName(updatedStash);
|
2024-03-03 01:33:35 +00:00
|
|
|
}
|
|
|
|
|
2024-03-26 23:06:03 +00:00
|
|
|
try {
|
|
|
|
const stash = await knex('stashes')
|
|
|
|
.where({
|
|
|
|
id: stashId,
|
|
|
|
user_id: sessionUser.id,
|
|
|
|
})
|
|
|
|
.update(curateStashEntry(updatedStash))
|
|
|
|
.returning('*');
|
|
|
|
|
|
|
|
if (!stash) {
|
|
|
|
throw new HttpError('You are not authorized to modify this stash', 403);
|
|
|
|
}
|
|
|
|
|
|
|
|
return curateStash(stash);
|
|
|
|
} catch (error) {
|
|
|
|
if (error.routine === '_bt_check_unique') {
|
|
|
|
throw new HttpError('Stash name should be unique', 409);
|
|
|
|
}
|
|
|
|
|
|
|
|
throw error;
|
|
|
|
}
|
2024-03-03 01:33:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export async function removeStash(stashId, sessionUser) {
|
|
|
|
if (!sessionUser) {
|
|
|
|
throw new HttpError('You are not authenthicated', 401);
|
|
|
|
}
|
|
|
|
|
|
|
|
const removed = await knex('stashes')
|
|
|
|
.where({
|
|
|
|
id: stashId,
|
|
|
|
user_id: sessionUser.id,
|
|
|
|
primary: false,
|
|
|
|
})
|
|
|
|
.delete();
|
|
|
|
|
|
|
|
if (removed === 0) {
|
|
|
|
throw new HttpError('Unable to remove this stash', 400);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function stashActor(actorId, stashId, sessionUser) {
|
2024-03-14 23:08:24 +00:00
|
|
|
const stash = await fetchStashById(stashId, sessionUser);
|
2024-03-03 01:33:35 +00:00
|
|
|
|
2024-03-21 01:54:05 +00:00
|
|
|
const [stashed] = await knex('stashes_actors')
|
2024-03-03 01:33:35 +00:00
|
|
|
.insert({
|
|
|
|
stash_id: stash.id,
|
|
|
|
actor_id: actorId,
|
2024-03-21 01:54:05 +00:00
|
|
|
})
|
|
|
|
.returning(['id', 'created_at']);
|
|
|
|
|
|
|
|
await indexApi.replace({
|
|
|
|
index: 'actors_stashed',
|
|
|
|
id: stashed.id,
|
|
|
|
doc: {
|
|
|
|
actor_id: actorId,
|
|
|
|
user_id: sessionUser.id,
|
|
|
|
stash_id: stashId,
|
|
|
|
created_at: Math.round(stashed.created_at.getTime() / 1000),
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed actor ${actorId} to stash ${stash.id} (${stash.name})`);
|
2024-03-03 01:33:35 +00:00
|
|
|
|
2024-03-27 01:28:21 +00:00
|
|
|
refreshView('actors');
|
2024-03-03 01:33:35 +00:00
|
|
|
|
|
|
|
return fetchStashes('actor', actorId, sessionUser);
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function unstashActor(actorId, stashId, sessionUser) {
|
|
|
|
await knex
|
|
|
|
.from('stashes_actors AS deletable')
|
|
|
|
.where('deletable.actor_id', actorId)
|
|
|
|
.where('deletable.stash_id', stashId)
|
|
|
|
.whereExists(knex('stashes_actors') // verify user owns this stash, complimentary to row-level security
|
|
|
|
.leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id')
|
|
|
|
.where('stashes_actors.stash_id', knex.raw('deletable.stash_id'))
|
|
|
|
.where('stashes.user_id', sessionUser.id))
|
|
|
|
.delete();
|
|
|
|
|
2024-03-21 01:54:05 +00:00
|
|
|
try {
|
|
|
|
await indexApi.callDelete({
|
|
|
|
index: 'actors_stashed',
|
|
|
|
query: {
|
|
|
|
bool: {
|
|
|
|
must: [
|
|
|
|
{ equals: { actor_id: actorId } },
|
|
|
|
{ equals: { stash_id: stashId } },
|
|
|
|
{ equals: { user_id: sessionUser.id } },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.log(error);
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed actor ${actorId} from stash ${stashId}`);
|
|
|
|
|
2024-03-27 01:28:21 +00:00
|
|
|
refreshView('actors');
|
2024-03-03 01:33:35 +00:00
|
|
|
|
|
|
|
return fetchStashes('actor', actorId, sessionUser);
|
|
|
|
}
|
|
|
|
|
2024-03-14 23:08:24 +00:00
|
|
|
export async function stashScene(sceneId, stashId, sessionUser) {
|
|
|
|
const stash = await fetchStashById(stashId, sessionUser);
|
|
|
|
|
2024-03-21 01:54:05 +00:00
|
|
|
const [stashed] = await knex('stashes_scenes')
|
2024-03-14 23:08:24 +00:00
|
|
|
.insert({
|
|
|
|
stash_id: stash.id,
|
|
|
|
scene_id: sceneId,
|
2024-03-21 01:54:05 +00:00
|
|
|
})
|
|
|
|
.returning(['id', 'created_at']);
|
|
|
|
|
|
|
|
await indexApi.replace({
|
|
|
|
index: 'scenes_stashed',
|
|
|
|
id: stashed.id,
|
|
|
|
doc: {
|
|
|
|
// ...doc.replace.doc,
|
|
|
|
scene_id: sceneId,
|
|
|
|
user_id: sessionUser.id,
|
|
|
|
stash_id: stashId,
|
|
|
|
created_at: Math.round(stashed.created_at.getTime() / 1000),
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed scene ${sceneId} to stash ${stash.id} (${stash.name})`);
|
2024-03-14 23:08:24 +00:00
|
|
|
|
2024-03-27 01:28:21 +00:00
|
|
|
refreshView('scenes');
|
|
|
|
|
2024-03-14 23:08:24 +00:00
|
|
|
return fetchStashes('scene', sceneId, sessionUser);
|
|
|
|
}
|
|
|
|
|
2024-03-03 01:33:35 +00:00
|
|
|
export async function unstashScene(sceneId, stashId, sessionUser) {
|
|
|
|
await knex
|
|
|
|
.from('stashes_scenes AS deletable')
|
|
|
|
.where('deletable.scene_id', sceneId)
|
|
|
|
.where('deletable.stash_id', stashId)
|
|
|
|
.whereExists(knex('stashes_scenes') // verify user owns this stash, complimentary to row-level security
|
|
|
|
.leftJoin('stashes', 'stashes.id', 'stashes_scenes.stash_id')
|
|
|
|
.where('stashes_scenes.stash_id', knex.raw('deletable.stash_id'))
|
|
|
|
.where('stashes.user_id', sessionUser.id))
|
|
|
|
.delete();
|
|
|
|
|
2024-03-14 23:08:24 +00:00
|
|
|
await indexApi.callDelete({
|
|
|
|
index: 'scenes_stashed',
|
|
|
|
query: {
|
|
|
|
bool: {
|
|
|
|
must: [
|
2024-03-21 01:54:05 +00:00
|
|
|
{ equals: { scene_id: sceneId } },
|
2024-03-14 23:08:24 +00:00
|
|
|
{ equals: { stash_id: stashId } },
|
|
|
|
{ equals: { user_id: sessionUser.id } },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2024-03-21 01:54:05 +00:00
|
|
|
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed scene ${sceneId} from stash ${stashId}`);
|
|
|
|
|
2024-03-27 01:28:21 +00:00
|
|
|
refreshView('scenes');
|
|
|
|
|
2024-03-03 01:33:35 +00:00
|
|
|
return fetchStashes('scene', sceneId, sessionUser);
|
|
|
|
}
|
|
|
|
|
2024-03-14 23:08:24 +00:00
|
|
|
export async function stashMovie(movieId, stashId, sessionUser) {
|
|
|
|
const stash = await fetchStashById(stashId, sessionUser);
|
|
|
|
|
2024-03-25 01:08:09 +00:00
|
|
|
const [stashed] = await knex('stashes_movies')
|
2024-03-14 23:08:24 +00:00
|
|
|
.insert({
|
|
|
|
stash_id: stash.id,
|
|
|
|
movie_id: movieId,
|
2024-03-25 01:08:09 +00:00
|
|
|
})
|
|
|
|
.returning(['id', 'created_at']);
|
|
|
|
|
|
|
|
await indexApi.replace({
|
|
|
|
index: 'movies_stashed',
|
|
|
|
id: stashed.id,
|
|
|
|
doc: {
|
|
|
|
movie_id: movieId,
|
|
|
|
user_id: sessionUser.id,
|
|
|
|
stash_id: stashId,
|
|
|
|
created_at: Math.round(stashed.created_at.getTime() / 1000),
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
logger.verbose(`${sessionUser.username} (${sessionUser.id}) stashed movie ${movieId} to stash ${stash.id} (${stash.name})`);
|
2024-03-14 23:08:24 +00:00
|
|
|
|
2024-03-27 01:28:21 +00:00
|
|
|
refreshView('movies');
|
|
|
|
|
2024-03-14 23:08:24 +00:00
|
|
|
return fetchStashes('movie', movieId, sessionUser);
|
|
|
|
}
|
|
|
|
|
2024-03-03 01:33:35 +00:00
|
|
|
export async function unstashMovie(movieId, stashId, sessionUser) {
|
|
|
|
await knex
|
|
|
|
.from('stashes_movies AS deletable')
|
|
|
|
.where('deletable.movie_id', movieId)
|
|
|
|
.where('deletable.stash_id', stashId)
|
|
|
|
.whereExists(knex('stashes_movies') // verify user owns this stash, complimentary to row-level security
|
|
|
|
.leftJoin('stashes', 'stashes.id', 'stashes_movies.stash_id')
|
|
|
|
.where('stashes_movies.stash_id', knex.raw('deletable.stash_id'))
|
|
|
|
.where('stashes.user_id', sessionUser.id))
|
|
|
|
.delete();
|
|
|
|
|
2024-03-25 01:08:09 +00:00
|
|
|
await indexApi.callDelete({
|
|
|
|
index: 'movies_stashed',
|
|
|
|
query: {
|
|
|
|
bool: {
|
|
|
|
must: [
|
|
|
|
{ equals: { movie_id: movieId } },
|
|
|
|
{ equals: { stash_id: stashId } },
|
|
|
|
{ equals: { user_id: sessionUser.id } },
|
|
|
|
],
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed movie ${movieId} from stash ${stashId}`);
|
|
|
|
|
2024-03-27 01:28:21 +00:00
|
|
|
refreshView('movies');
|
|
|
|
|
2024-03-03 01:33:35 +00:00
|
|
|
return fetchStashes('movie', movieId, sessionUser);
|
|
|
|
}
|
2024-03-27 01:28:21 +00:00
|
|
|
|
|
|
|
CronJob.from({
|
|
|
|
cronTime: config.stashes.viewRefreshCron,
|
|
|
|
async onTick() {
|
|
|
|
logger.verbose('Updating stash views');
|
|
|
|
|
|
|
|
await refreshView('scenes');
|
|
|
|
await refreshView('actors');
|
|
|
|
await refreshView('movies');
|
|
|
|
await refreshView('stashes');
|
|
|
|
},
|
|
|
|
start: true,
|
|
|
|
runOnInit: true,
|
|
|
|
});
|