+
+
+
+
+
+
diff --git a/pages/stashes/@username/@stashSlug/actors/+onBeforeRender.js b/pages/stashes/@username/@stashSlug/actors/+onBeforeRender.js
new file mode 100644
index 0000000..f7eb1f1
--- /dev/null
+++ b/pages/stashes/@username/@stashSlug/actors/+onBeforeRender.js
@@ -0,0 +1,49 @@
+import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
+
+import { fetchStashByUsernameAndSlug } from '#/src/stashes.js';
+import { fetchActors } from '#/src/actors.js';
+import { curateActorsQuery } from '#/src/web/actors.js';
+import { HttpError } from '#/src/errors.js';
+
+export async function onBeforeRender(pageContext) {
+ try {
+ const stash = await fetchStashByUsernameAndSlug(pageContext.routeParams.username, pageContext.routeParams.stashSlug, pageContext.user);
+
+ const stashActors = await fetchActors(curateActorsQuery({
+ ...pageContext.urlQuery,
+ stashId: stash.id,
+ }), {
+ page: Number(pageContext.routeParams.page) || 1,
+ limit: Number(pageContext.urlParsed.search.limit) || 120,
+ order: pageContext.urlParsed.search.order?.split('.') || ['stashed', 'desc'],
+ }, pageContext.user);
+
+ const {
+ actors,
+ countries,
+ cupRange,
+ limit,
+ total,
+ } = stashActors;
+
+ return {
+ pageContext: {
+ title: `${stash.name} by ${stash.user.username}`,
+ pageProps: {
+ stash,
+ actors,
+ countries,
+ cupRange,
+ limit,
+ total,
+ },
+ },
+ };
+ } catch (error) {
+ if (error instanceof HttpError) {
+ throw render(error.httpCode, error.message);
+ }
+
+ throw error;
+ }
+}
diff --git a/pages/stashes/@username/@stashSlug/actors/+route.js b/pages/stashes/@username/@stashSlug/actors/+route.js
new file mode 100644
index 0000000..e4edd8b
--- /dev/null
+++ b/pages/stashes/@username/@stashSlug/actors/+route.js
@@ -0,0 +1,24 @@
+import { match } from 'path-to-regexp';
+// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
+
+const path = '/stash/:username/:stashSlug/:domain(actors)/:page?';
+const urlMatch = match(path, { decode: decodeURIComponent });
+
+export default (pageContext) => {
+ const matched = urlMatch(pageContext.urlPathname);
+
+ if (matched) {
+ return {
+ routeParams: {
+ username: matched.params.username,
+ stashSlug: matched.params.stashSlug,
+ domain: matched.params.domain,
+ order: 'stashed.desc',
+ page: matched.params.page || '1',
+ path,
+ },
+ };
+ }
+
+ return false;
+};
diff --git a/pages/stashes/@username/@stashSlug/movies/+Page.vue b/pages/stashes/@username/@stashSlug/movies/+Page.vue
new file mode 100644
index 0000000..f318230
--- /dev/null
+++ b/pages/stashes/@username/@stashSlug/movies/+Page.vue
@@ -0,0 +1,10 @@
+
+
+ Movies
+
+
+
+
diff --git a/pages/stashes/@username/@stashSlug/movies/+onBeforeRender.js b/pages/stashes/@username/@stashSlug/movies/+onBeforeRender.js
new file mode 100644
index 0000000..159a43e
--- /dev/null
+++ b/pages/stashes/@username/@stashSlug/movies/+onBeforeRender.js
@@ -0,0 +1,25 @@
+import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
+
+import { fetchStashByUsernameAndSlug } from '#/src/stashes.js';
+import { HttpError } from '#/src/errors.js';
+
+export async function onBeforeRender(pageContext) {
+ try {
+ const stash = await fetchStashByUsernameAndSlug(pageContext.routeParams.username, pageContext.routeParams.stashSlug, pageContext.user);
+
+ return {
+ pageContext: {
+ title: `${stash.name} by ${stash.user.username}`,
+ pageProps: {
+ stash,
+ },
+ },
+ };
+ } catch (error) {
+ if (error instanceof HttpError) {
+ throw render(error.httpCode, error.message);
+ }
+
+ throw error;
+ }
+}
diff --git a/pages/stashes/@username/+route.js b/pages/stashes/@username/@stashSlug/movies/+route.js
similarity index 82%
rename from pages/stashes/@username/+route.js
rename to pages/stashes/@username/@stashSlug/movies/+route.js
index d8ce6dd..acd64f7 100644
--- a/pages/stashes/@username/+route.js
+++ b/pages/stashes/@username/@stashSlug/movies/+route.js
@@ -1,7 +1,7 @@
import { match } from 'path-to-regexp';
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
-const path = '/stash/:username/:stashSlug/:scope?/:page?';
+const path = '/stash/:username/:stashSlug/:domain(movies)/:scope?/:page?';
const urlMatch = match(path, { decode: decodeURIComponent });
export default (pageContext) => {
@@ -12,6 +12,7 @@ export default (pageContext) => {
routeParams: {
username: matched.params.username,
stashSlug: matched.params.stashSlug,
+ domain: matched.params.domain,
scope: matched.params.scope || 'stashed',
page: matched.params.page || '1',
path,
diff --git a/pages/stashes/@username/@stashSlug/scenes/+Page.vue b/pages/stashes/@username/@stashSlug/scenes/+Page.vue
new file mode 100644
index 0000000..2c71c13
--- /dev/null
+++ b/pages/stashes/@username/@stashSlug/scenes/+Page.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/pages/stashes/@username/+onBeforeRender.js b/pages/stashes/@username/@stashSlug/scenes/+onBeforeRender.js
similarity index 100%
rename from pages/stashes/@username/+onBeforeRender.js
rename to pages/stashes/@username/@stashSlug/scenes/+onBeforeRender.js
diff --git a/pages/stashes/@username/@stashSlug/scenes/+route.js b/pages/stashes/@username/@stashSlug/scenes/+route.js
new file mode 100644
index 0000000..ea661a6
--- /dev/null
+++ b/pages/stashes/@username/@stashSlug/scenes/+route.js
@@ -0,0 +1,24 @@
+import { match } from 'path-to-regexp';
+// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
+
+const path = '/stash/:username/:stashSlug/:domain?/:scope?/:page?';
+const urlMatch = match(path, { decode: decodeURIComponent });
+
+export default (pageContext) => {
+ const matched = urlMatch(pageContext.urlPathname);
+
+ if (matched) {
+ return {
+ routeParams: {
+ username: matched.params.username,
+ stashSlug: matched.params.stashSlug,
+ domain: matched.params.domain || 'scenes',
+ scope: matched.params.scope || 'stashed',
+ page: matched.params.page || '1',
+ path,
+ },
+ };
+ }
+
+ return false;
+};
diff --git a/renderer/+config.h.js b/renderer/+config.h.js
index 700e364..8ac21c6 100644
--- a/renderer/+config.h.js
+++ b/renderer/+config.h.js
@@ -1,3 +1,10 @@
export default {
- passToClient: ['pageProps', 'urlPathname', 'routeParams', 'urlParsed', 'env', 'user'],
+ passToClient: [
+ 'pageProps',
+ 'urlPathname',
+ 'routeParams',
+ 'urlParsed',
+ 'env',
+ 'user',
+ ],
};
diff --git a/renderer/container.vue b/renderer/container.vue
index 51e2b21..6ad1c04 100644
--- a/renderer/container.vue
+++ b/renderer/container.vue
@@ -23,11 +23,23 @@
class="nav"
@sidebar="showSidebar = true"
/>
+
+
+
{{ feedback.message }}
+
@@ -102,9 +149,47 @@ onMounted(() => {
display: none;
}
+.feedback-container {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ position: fixed;
+ bottom: 1rem;;
+ z-index: 1000;
+ pointer-events: none;
+}
+
+.feedback {
+ padding: .5rem 1rem;
+ margin: 0 .5rem;
+ border-radius: 1rem;
+ box-shadow: 0 0 3px var(--shadow-weak-10);
+ background: var(--grey-dark-40);
+ color: var(--text-light);
+ font-size: .9rem;
+ visibility: hidden;
+ line-height: 1.5;
+
+ &.success {
+ background: var(--success);
+ }
+
+ &.error {
+ background: var(--error);
+ }
+
+ &.remove {
+ background: var(--warn);
+ }
+}
+
@media(--small-10) {
.nav {
display: flex;
}
+
+ .feedback-container {
+ bottom: 4rem;
+ }
}
diff --git a/renderer/usePageContext.js b/renderer/usePageContext.js
index 1876dd7..4ce027e 100644
--- a/renderer/usePageContext.js
+++ b/renderer/usePageContext.js
@@ -1,18 +1,15 @@
// `usePageContext` allows us to access `pageContext` in any Vue component.
// See https://vike.dev/pageContext-anywhere
-import { inject } from 'vue'
+import { inject } from 'vue';
-export { usePageContext }
-export { setPageContext }
+const key = Symbol(); // eslint-disable-line symbol-description
-const key = Symbol()
-
-function usePageContext() {
- const pageContext = inject(key)
- return pageContext
+export function usePageContext() {
+ const pageContext = inject(key);
+ return pageContext;
}
-function setPageContext(app, pageContext) {
- app.provide(key, pageContext)
+export function setPageContext(app, pageContext) {
+ app.provide(key, pageContext);
}
diff --git a/src/actors.js b/src/actors.js
index 4e6b856..16826c4 100644
--- a/src/actors.js
+++ b/src/actors.js
@@ -2,10 +2,11 @@ import config from 'config';
import { differenceInYears } from 'date-fns';
import { unit } from 'mathjs';
-import knex from './knex.js';
-import { searchApi } from './manticore.js';
+import { knexOwner as knex, knexManticore } from './knex.js';
+import { utilsApi } from './manticore.js';
import { HttpError } from './errors.js';
import { fetchCountriesByAlpha2 } from './countries.js';
+import { curateStash } from './stashes.js';
export function curateActor(actor, context = {}) {
return {
@@ -58,6 +59,7 @@ export function curateActor(actor, context = {}) {
createdAt: actor.created_at,
updatedAt: actor.updated_at,
likes: actor.stashed,
+ stashes: context.stashes?.map((stash) => curateStash(stash)) || [],
...context.append?.[actor.id],
};
}
@@ -73,8 +75,8 @@ export function sortActorsByGender(actors) {
return genderActors;
}
-export async function fetchActorsById(actorIds, options = {}) {
- const [actors] = await Promise.all([
+export async function fetchActorsById(actorIds, options = {}, reqUser) {
+ const [actors, stashes] = await Promise.all([
knex('actors')
.select(
'actors.*',
@@ -93,10 +95,19 @@ export async function fetchActorsById(actorIds, options = {}) {
builder.orderBy(...options.order);
}
}),
+ reqUser
+ ? knex('stashes_actors')
+ .leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id')
+ .where('stashes.user_id', reqUser.id)
+ .whereIn('stashes_actors.actor_id', actorIds)
+ : [],
]);
if (options.order) {
- return actors.map((actorEntry) => curateActor(actorEntry, { append: options.append }));
+ return actors.map((actorEntry) => curateActor(actorEntry, {
+ stashes: stashes.filter((stash) => stash.actor_id === actorEntry.id),
+ append: options.append,
+ }));
}
const curatedActors = actorIds.map((actorId) => {
@@ -107,7 +118,10 @@ export async function fetchActorsById(actorIds, options = {}) {
return null;
}
- return curateActor(actor, { append: options.append });
+ return curateActor(actor, {
+ stashes: stashes.filter((stash) => stash.actor_id === actor.id),
+ append: options.append,
+ });
}).filter(Boolean);
return curatedActors;
@@ -126,6 +140,30 @@ function curateOptions(options) {
};
}
+/*
+const sortMap = {
+ likes: 'stashed',
+ scenes: 'scenes',
+ relevance: '_score',
+};
+
+function getSort(order) {
+ if (order[0] === 'name') {
+ return [{
+ slug: order[1],
+ }];
+ }
+
+ return [
+ {
+ [sortMap[order[0]]]: order[1],
+ },
+ {
+ slug: 'asc', // sort by name where primary order is equal
+ },
+ ];
+}
+
function buildQuery(filters) {
const query = {
bool: {
@@ -230,31 +268,7 @@ function buildQuery(filters) {
return { query, expressions };
}
-const sortMap = {
- likes: 'stashed',
- scenes: 'scenes',
- relevance: '_score',
-};
-
-function getSort(order) {
- if (order[0] === 'name') {
- return [{
- slug: order[1],
- }];
- }
-
- return [
- {
- [sortMap[order[0]]]: order[1],
- },
- {
- slug: 'asc', // sort by name where primary order is equal
- },
- ];
-}
-
-export async function fetchActors(filters, rawOptions) {
- const options = curateOptions(rawOptions);
+async function queryManticoreJson(filters, options) {
const { query, expressions } = buildQuery(filters);
const result = await searchApi.search({
@@ -279,16 +293,176 @@ export async function fetchActors(filters, rawOptions) {
},
});
- const actorIds = result.hits.hits.map((hit) => Number(hit._id));
+ const actors = result.hits.hits.map((hit) => ({
+ id: hit._id,
+ ...hit._source,
+ _score: hit._score,
+ }));
+
+ return {
+ actors,
+ total: result.hits.total,
+ aggregations: result.aggregations && Object.fromEntries(Object.entries(result.aggregations).map(([key, { buckets }]) => [key, buckets])),
+ };
+}
+*/
+
+async function queryManticoreSql(filters, options, _reqUser) {
+ const aggSize = config.database.manticore.maxAggregateSize;
+
+ const sqlQuery = knexManticore.raw(`
+ :query:
+ OPTION
+ max_matches=:maxMatches:,
+ max_query_time=:maxQueryTime:
+ :countriesFacet:;
+ show meta;
+ `, {
+ query: knexManticore(filters.stashId ? 'actors_stashed' : 'actors')
+ .modify((builder) => {
+ if (filters.stashId) {
+ builder.select(knex.raw(`
+ actors.id as id,
+ actors.country as country,
+ actors.scenes as scenes,
+ actors.stashed as stashed,
+ created_at as stashed_at
+ `));
+ // weight() as _score
+
+ builder
+ .innerJoin('actors', 'actors.id', 'actors_stashed.actor_id')
+ .where('stash_id', filters.stashId);
+ } else {
+ // builder.select(knex.raw('*, weight() as _score'));
+ builder.select(knex.raw('*'));
+ }
+
+ if (filters.query) {
+ builder.whereRaw('match(\'@name :query:\', actors)', { query: filters.query });
+ }
+
+ ['gender', 'country'].forEach((attribute) => {
+ if (filters[attribute]) {
+ builder.where(attribute, filters[attribute]);
+ }
+ });
+
+ ['age', 'height', 'weight'].forEach((attribute) => {
+ if (filters[attribute]) {
+ builder
+ .where(attribute, '>=', filters[attribute][0])
+ .where(attribute, '<=', filters[attribute][1]);
+ }
+ });
+
+ if (filters.dateOfBirth && filters.dobType === 'dateOfBirth') {
+ builder.where('date_of_birth', Math.floor(filters.dateOfBirth.getTime() / 1000));
+ }
+
+ if (filters.dateOfBirth && filters.dobType === 'birthday') {
+ const month = filters.dateOfBirth.getMonth() + 1;
+ const day = filters.dateOfBirth.getDate();
+
+ builder
+ .where('month(date_of_birth)', month)
+ .where('day(date_of_birth)', day);
+ }
+
+ if (filters.cup) {
+ builder.where(`regex(cup, '^[${filters.cup[0]}-${filters.cup[1]}]')`, 1);
+ }
+
+ if (typeof filters.naturalBoobs === 'boolean') {
+ builder.where('natural_boobs', filters.naturalBoobs ? 2 : 1); // manticore boolean does not support null, so 0 = null, 1 = false (enhanced), 2 = true (natural)
+ }
+
+ if (filters.requireAvatar) {
+ builder.where('has_avatar', 1);
+ }
+
+ if (options.order?.[0] === 'name') {
+ builder.orderBy('actors.slug', options.order[1]);
+ } else if (options.order?.[0] === 'likes') {
+ builder.orderBy([
+ { column: 'actors.stashed', order: options.order[1] },
+ { column: 'actors.slug', order: 'asc' },
+ ]);
+ } else if (options.order?.[0] === 'scenes') {
+ builder.orderBy([
+ { column: 'actors.scenes', order: options.order[1] },
+ { column: 'actors.slug', order: 'asc' },
+ ]);
+ } else if (options.order?.[0] === 'stashed' && filters.stashId) {
+ builder.orderBy([
+ { column: 'stashed_at', order: options.order[1] },
+ { column: 'actors.slug', order: 'asc' },
+ ]);
+ } else if (options.order) {
+ builder.orderBy([
+ { column: `actors.${options.order[0]}`, order: options.order[1] },
+ { column: 'actors.slug', order: 'asc' },
+ ]);
+ } else {
+ builder.orderBy('actors.slug', 'asc');
+ }
+ })
+ .limit(options.limit)
+ .offset((options.page - 1) * options.limit)
+ .toString(),
+ // option threads=1 fixes actors, but drastically slows down performance, wait for fix
+ countriesFacet: options.aggregateActors ? knex.raw('facet actors.country order by count(*) desc limit 300', [aggSize]) : null,
+ maxMatches: config.database.manticore.maxMatches,
+ maxQueryTime: config.database.manticore.maxQueryTime,
+ }).toString();
+
+ // manticore does not seem to accept table.column syntax if 'table' is primary (yet?), crude work-around
+ const curatedSqlQuery = filters.stashId
+ ? sqlQuery
+ : sqlQuery.replace(/actors\./g, '');
+
+ if (process.env.NODE_ENV === 'development') {
+ console.log(curatedSqlQuery);
+ }
+
+ const results = await utilsApi.sql(curatedSqlQuery);
+
+ // console.log(results[0]);
+
+ const countries = results
+ .find((result) => (result.columns[0].actor_ids || result.columns[0]['scenes.country']) && result.columns[1]['count(*)'])
+ ?.data.map((row) => ({ key: row.actor_ids || row['scenes.country'], doc_count: row['count(*)'] }))
+ || [];
+
+ const total = Number(results.at(-1).data.find((entry) => entry.Variable_name === 'total_found').Value);
+
+ return {
+ actors: results[0].data,
+ total,
+ aggregations: {
+ countries,
+ },
+ };
+}
+
+export async function fetchActors(filters, rawOptions, reqUser) {
+ const options = curateOptions(rawOptions);
+
+ console.log('filters', filters);
+ console.log('options', options);
+
+ const result = await queryManticoreSql(filters, options, reqUser);
+
+ const actorIds = result.actors.map((actor) => Number(actor.id));
const [actors, countries] = await Promise.all([
- fetchActorsById(actorIds),
- fetchCountriesByAlpha2(result.aggregations.countries.buckets.map((bucket) => bucket.key)),
+ fetchActorsById(actorIds, {}, reqUser),
+ fetchCountriesByAlpha2(result.aggregations.countries.map((bucket) => bucket.key)),
]);
return {
actors,
countries,
- total: result.hits.total,
+ total: result.total,
limit: options.limit,
};
}
diff --git a/src/scenes.js b/src/scenes.js
index c283921..2c4e05e 100644
--- a/src/scenes.js
+++ b/src/scenes.js
@@ -2,7 +2,7 @@ import config from 'config';
import util from 'util'; /* eslint-disable-line no-unused-vars */
import { knexOwner as knex, knexManticore } from './knex.js';
-import { searchApi, utilsApi } from './manticore.js';
+import { utilsApi } from './manticore.js';
import { HttpError } from './errors.js';
import { fetchActorsById, curateActor, sortActorsByGender } from './actors.js';
import { fetchTagsById } from './tags.js';
@@ -55,7 +55,10 @@ function curateScene(rawScene, assets) {
type: assets.channel.network_type,
hasLogo: assets.channel.has_logo,
} : null,
- actors: sortActorsByGender(assets.actors.map((actor) => curateActor(actor, { sceneDate: rawScene.effective_date }))),
+ actors: sortActorsByGender(assets.actors.map((actor) => curateActor(actor, {
+ sceneDate: rawScene.effective_date,
+ stashes: assets.actorStashes,
+ }))),
directors: assets.directors.map((director) => ({
id: director.id,
slug: director.slug,
@@ -74,7 +77,7 @@ function curateScene(rawScene, assets) {
};
}
-export async function fetchScenesById(sceneIds, reqUser) {
+export async function fetchScenesById(sceneIds, { reqUser, ...context } = {}) {
const [scenes, channels, actors, directors, tags, posters, photos, stashes] = await Promise.all([
knex('releases').whereIn('releases.id', sceneIds),
knex('releases')
@@ -125,6 +128,13 @@ export async function fetchScenesById(sceneIds, reqUser) {
: [],
]);
+ const actorStashes = reqUser && context.actorStashes
+ ? await knex('stashes_actors')
+ .leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id')
+ .where('stashes.user_id', reqUser.id)
+ .whereIn('stashes_actors.actor_id', actors.map((actor) => actor.id))
+ : [];
+
return sceneIds.map((sceneId) => {
const scene = scenes.find((sceneEntry) => sceneEntry.id === sceneId);
@@ -139,6 +149,7 @@ export async function fetchScenesById(sceneIds, reqUser) {
const scenePoster = posters.find((poster) => poster.release_id === sceneId);
const scenePhotos = photos.filter((photo) => photo.release_id === sceneId);
const sceneStashes = stashes.filter((stash) => stash.scene_id === sceneId);
+ const sceneActorStashes = sceneActors.map((actor) => actorStashes.find((stash) => stash.actor_id === actor.id)).filter(Boolean);
return curateScene(scene, {
channel: sceneChannel,
@@ -148,6 +159,7 @@ export async function fetchScenesById(sceneIds, reqUser) {
poster: scenePoster,
photos: scenePhotos,
stashes: sceneStashes,
+ actorStashes: sceneActorStashes,
});
}).filter(Boolean);
}
@@ -171,6 +183,7 @@ function curateOptions(options) {
};
}
+/*
function buildQuery(filters = {}, options) {
const query = {
bool: {
@@ -215,23 +228,6 @@ function buildQuery(filters = {}, options) {
}
if (filters.query) {
- /*
- query.bool.must.push({
- bool: {
- should: [
- { match: { title_filtered: filters.query } },
- { match: { actors: filters.query } },
- { match: { tags: filters.query } },
- { match: { channel_name: filters.query } },
- { match: { network_name: filters.query } },
- { match: { channel_slug: filters.query } },
- { match: { network_slug: filters.query } },
- { match: { meta: filters.query } }, // date
- ],
- },
- });
- */
-
query.bool.must.push({ match: { '!title': filters.query } }); // title_filtered is matched instead of title
}
@@ -262,16 +258,6 @@ function buildQuery(filters = {}, options) {
query.bool.must.push({ equals: { stash_id: filters.stashId } });
}
- /* tag filter
- must_not: [
- {
- in: {
- 'any(tag_ids)': [101, 180, 32],
- },
- },
- ],
- */
-
return { query, sort };
}
@@ -311,14 +297,6 @@ function buildAggregates(options) {
return aggregates;
}
-function countAggregations(buckets) {
- if (!buckets) {
- return null;
- }
-
- return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
-}
-
async function queryManticoreJson(filters, options, _reqUser) {
const { query, sort } = buildQuery(filters, options);
@@ -357,24 +335,26 @@ async function queryManticoreJson(filters, options, _reqUser) {
aggregations: result.aggregations && Object.fromEntries(Object.entries(result.aggregations).map(([key, { buckets }]) => [key, buckets])),
};
}
+*/
async function queryManticoreSql(filters, options, _reqUser) {
const aggSize = config.database.manticore.maxAggregateSize;
const sqlQuery = knexManticore.raw(`
:query:
- OPTION field_weights=(
- title_filtered=7,
- actors=10,
- tags=9,
- meta=6,
- channel_name=2,
- channel_slug=3,
- network_name=1,
- network_slug=1
- ),
- max_matches=:maxMatches:,
- max_query_time=:maxQueryTime:
+ OPTION
+ field_weights=(
+ title_filtered=7,
+ actors=10,
+ tags=9,
+ meta=6,
+ channel_name=2,
+ channel_slug=3,
+ network_name=1,
+ network_slug=1
+ ),
+ max_matches=:maxMatches:,
+ max_query_time=:maxQueryTime:
:actorsFacet:
:tagsFacet:
:channelsFacet:;
@@ -391,6 +371,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
scenes.channel_id as channel_id,
scenes.network_id as network_id,
scenes.effective_date as effective_date,
+ scenes.stashed as stashed,
scenes.created_at,
created_at as stashed_at,
weight() as _score
@@ -481,7 +462,7 @@ async function queryManticoreSql(filters, options, _reqUser) {
const results = await utilsApi.sql(curatedSqlQuery);
- console.log(results[0]);
+ // console.log(results[0]);
const actorIds = results
.find((result) => (result.columns[0].actor_ids || result.columns[0]['scenes.actor_ids']) && result.columns[1]['count(*)'])
@@ -511,6 +492,14 @@ async function queryManticoreSql(filters, options, _reqUser) {
};
}
+function countAggregations(buckets) {
+ if (!buckets) {
+ return null;
+ }
+
+ return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
+}
+
export async function fetchScenes(filters, rawOptions, reqUser) {
const options = curateOptions(rawOptions);
@@ -527,10 +516,6 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
const result = await queryManticoreSql(filters, options, reqUser);
console.timeEnd('manticore sql');
- console.time('manticore json');
- await queryManticoreJson(filters, options, reqUser);
- console.timeEnd('manticore json');
-
const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds);
const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds);
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds);
@@ -547,7 +532,7 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
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 });
console.timeEnd('fetch full');
return {
diff --git a/src/stashes.js b/src/stashes.js
index 0a07722..7e27e8d 100755
--- a/src/stashes.js
+++ b/src/stashes.js
@@ -170,11 +170,25 @@ export async function refreshActorsView() {
export async function stashActor(actorId, stashId, sessionUser) {
const stash = await fetchStashById(stashId, sessionUser);
- await knex('stashes_actors')
+ const [stashed] = await knex('stashes_actors')
.insert({
stash_id: stash.id,
actor_id: actorId,
- });
+ })
+ .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})`);
refreshActorsView();
@@ -192,6 +206,25 @@ export async function unstashActor(actorId, stashId, sessionUser) {
.where('stashes.user_id', sessionUser.id))
.delete();
+ 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}`);
+
refreshActorsView();
return fetchStashes('actor', actorId, sessionUser);
@@ -200,11 +233,26 @@ export async function unstashActor(actorId, stashId, sessionUser) {
export async function stashScene(sceneId, stashId, sessionUser) {
const stash = await fetchStashById(stashId, sessionUser);
- await knex('stashes_scenes')
+ const [stashed] = await knex('stashes_scenes')
.insert({
stash_id: stash.id,
scene_id: sceneId,
- });
+ })
+ .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})`);
return fetchStashes('scene', sceneId, sessionUser);
}
@@ -225,7 +273,7 @@ export async function unstashScene(sceneId, stashId, sessionUser) {
query: {
bool: {
must: [
- { equals: { id: sceneId } },
+ { equals: { scene_id: sceneId } },
{ equals: { stash_id: stashId } },
{ equals: { user_id: sessionUser.id } },
],
@@ -233,6 +281,8 @@ export async function unstashScene(sceneId, stashId, sessionUser) {
},
});
+ logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed scene ${sceneId} from stash ${stashId}`);
+
return fetchStashes('scene', sceneId, sessionUser);
}
diff --git a/src/tools/manticore-scenes.js b/src/tools/manticore-scenes.js
index 423ec1f..6f29f0e 100644
--- a/src/tools/manticore-scenes.js
+++ b/src/tools/manticore-scenes.js
@@ -2,7 +2,7 @@
import { format } from 'date-fns';
import { faker } from '@faker-js/faker';
-import { indexApi, utilsApi } from '../manticore.js';
+import { indexApi } from '../manticore.js';
import { knexOwner as knex } from '../knex.js';
import slugify from '../utils/slugify.js';
@@ -105,11 +105,11 @@ async function updateStashed(docs) {
const stashDoc = sceneStashes.map((stash) => ({
replace: {
- index: 'movies_liked',
+ index: 'scenes_stashed',
id: stash.stashed_id,
doc: {
// ...doc.replace.doc,
- movie_id: doc.replace.id,
+ scene_id: doc.replace.id,
user_id: stash.user_id,
},
},
@@ -127,35 +127,6 @@ async function updateStashed(docs) {
}
async function init() {
- await utilsApi.sql('drop table if exists movies');
- await utilsApi.sql('drop table if exists movies_liked');
-
- await utilsApi.sql(`create table movies (
- id int,
- title text,
- title_filtered text,
- channel_id int,
- channel_name text,
- channel_slug text,
- network_id int,
- network_name text,
- network_slug text,
- actor_ids multi,
- actors text,
- tag_ids multi,
- tags text,
- meta text,
- date timestamp,
- created_at timestamp,
- effective_date timestamp,
- liked int
- )`);
-
- await utilsApi.sql(`create table movies_liked (
- movie_id int,
- user_id int
- )`);
-
const scenes = await fetchScenes();
const docs = scenes.map((scene) => {
@@ -165,7 +136,7 @@ async function init() {
return {
replace: {
- index: 'movies',
+ index: 'scenes',
id: scene.id,
doc: {
title: scene.title || undefined,
diff --git a/src/tools/sync-stashes.js b/src/tools/sync-stashes.js
new file mode 100644
index 0000000..ee38dc6
--- /dev/null
+++ b/src/tools/sync-stashes.js
@@ -0,0 +1,51 @@
+import { indexApi } from '../manticore.js';
+import { knexOwner as knex } from '../knex.js';
+import chunk from '../utils/chunk.js';
+
+async function syncActorStashes() {
+ const stashes = await knex('stashes_actors')
+ .select(
+ 'stashes_actors.id as stashed_id',
+ 'stashes_actors.actor_id',
+ 'stashes.id as stash_id',
+ 'stashes.user_id as user_id',
+ 'stashes_actors.created_at as created_at',
+ )
+ .leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id');
+
+ if (stashes.length > 0) {
+ console.log(stashes);
+ }
+
+ await chunk(stashes, 1000).reduce(async (chain, stashChunk, index) => {
+ await chain;
+
+ const stashDocs = stashChunk.map((stash) => ({
+ replace: {
+ index: 'actors_stashed',
+ id: stash.stashed_id,
+ doc: {
+ actor_id: stash.actor_id,
+ stash_id: stash.stash_id,
+ user_id: stash.user_id,
+ created_at: Math.round(stash.created_at.getTime() / 1000),
+ },
+ },
+ }));
+
+ console.log(stashDocs);
+
+ await indexApi.bulk(stashDocs.map((doc) => JSON.stringify(doc)).join('\n'));
+
+ console.log(`Synced ${index * 1000 + stashChunk.length}/${stashes.length} actor stashes`);
+ }, Promise.resolve());
+}
+
+async function init() {
+ await syncActorStashes();
+ console.log('Done!');
+
+ knex.destroy();
+}
+
+init();
diff --git a/src/web/actors.js b/src/web/actors.js
index 44bb981..dcb1dfd 100644
--- a/src/web/actors.js
+++ b/src/web/actors.js
@@ -13,6 +13,7 @@ export function curateActorsQuery(query) {
height: query.height?.split(',').map((height) => Number(height)),
weight: query.weight?.split(',').map((weight) => Number(weight)),
requireAvatar: query.avatar,
+ stashId: Number(query.stashId) || null,
};
}
@@ -26,7 +27,7 @@ export async function fetchActorsApi(req, res) {
page: Number(req.query.page) || 1,
limit: Number(req.query.limit) || 120,
order: req.query.order?.split('.') || ['likes', 'desc'],
- });
+ }, req.user);
res.send({
actors,
diff --git a/src/web/scenes.js b/src/web/scenes.js
index 0c5e6e7..5d7b22f 100644
--- a/src/web/scenes.js
+++ b/src/web/scenes.js
@@ -11,7 +11,7 @@ export async function curateScenesQuery(query) {
actorIds: [query.actorId, ...(query.actors?.split(',') || []).map((identifier) => parseActorIdentifier(identifier)?.id)].filter(Boolean),
tagIds: await getIdsBySlug([query.tagSlug, ...(query.tags?.split(',') || [])], 'tags'),
entityId: query.e ? await getIdsBySlug([query.e], 'entities').then(([id]) => id) : query.entityId,
- stashId: Number(query.stashId),
+ stashId: Number(query.stashId) || null,
};
}
diff --git a/src/web/stashes.js b/src/web/stashes.js
index 2635624..a747614 100755
--- a/src/web/stashes.js
+++ b/src/web/stashes.js
@@ -17,37 +17,37 @@ export async function createStashApi(req, res) {
}
export async function updateStashApi(req, res) {
- const stash = await updateStash(req.params.stashId, req.body, req.session.user);
+ const stash = await updateStash(Number(req.params.stashId), req.body, req.session.user);
res.send(stash);
}
export async function removeStashApi(req, res) {
- await removeStash(req.params.stashId, req.session.user);
+ await removeStash(Number(req.params.stashId), req.session.user);
res.status(204).send();
}
export async function stashActorApi(req, res) {
- const stashes = await stashActor(req.body.actorId, req.params.stashId, req.user);
+ const stashes = await stashActor(req.body.actorId, Number(req.params.stashId), req.user);
res.send(stashes);
}
export async function stashSceneApi(req, res) {
- const stashes = await stashScene(req.body.sceneId, req.params.stashId, req.user);
+ const stashes = await stashScene(req.body.sceneId, Number(req.params.stashId), req.user);
res.send(stashes);
}
export async function stashMovieApi(req, res) {
- const stashes = await stashMovie(req.body.movieId, req.params.stashId, req.user);
+ const stashes = await stashMovie(req.body.movieId, Number(req.params.stashId), req.user);
res.send(stashes);
}
export async function unstashActorApi(req, res) {
- const stashes = await unstashActor(req.params.actorId, req.params.stashId, req.user);
+ const stashes = await unstashActor(Number(req.params.actorId), Number(req.params.stashId), req.user);
res.send(stashes);
}
@@ -59,7 +59,7 @@ export async function unstashSceneApi(req, res) {
}
export async function unstashMovieApi(req, res) {
- const stashes = await unstashMovie(req.params.movieId, req.params.stashId, req.user);
+ const stashes = await unstashMovie(Number(req.params.movieId), Number(req.params.stashId), req.user);
res.send(stashes);
}
diff --git a/utils/ellipsis.js b/utils/ellipsis.js
new file mode 100644
index 0000000..423e6f4
--- /dev/null
+++ b/utils/ellipsis.js
@@ -0,0 +1,11 @@
+export default function ellipsis(text, limit = 50, ellipse = '...') {
+ if (!text) {
+ return '';
+ }
+
+ if (text.length > limit) {
+ return `${text.slice(0, limit - ellipse.length)}${ellipse}`;
+ }
+
+ return text;
+}