From a8aab600c7d3304904bf4fb10f4d6b978dac5377 Mon Sep 17 00:00:00 2001 From: DebaucheryLibrarian Date: Thu, 21 Mar 2024 02:54:05 +0100 Subject: [PATCH] Added actor stash. --- assets/css/tooltip.css | 6 + components/actors/actors.vue | 307 ++++++++++++++++-- components/actors/tile.vue | 134 +++++++- components/header/header.vue | 14 +- components/scenes/scenes.vue | 2 +- components/scenes/tile.vue | 28 +- .../+Page.vue => components/stashes/stash.vue | 58 +++- config/default.cjs | 3 + pages/+config.js | 10 + pages/_error/+data.js | 5 + pages/actors/+Page.vue | 267 +-------------- pages/actors/+onBeforeRender.js | 2 +- pages/actors/@actorId/+Page.vue | 92 +++++- pages/actors/@actorId/+onBeforeRender.js | 4 +- pages/scene/+Page.vue | 58 +++- pages/scene/+onBeforeRender.js | 5 +- .../@username/@stashSlug/actors/+Page.vue | 10 + .../@stashSlug/actors/+onBeforeRender.js | 49 +++ .../@username/@stashSlug/actors/+route.js | 24 ++ .../@username/@stashSlug/movies/+Page.vue | 10 + .../@stashSlug/movies/+onBeforeRender.js | 25 ++ .../{ => @stashSlug/movies}/+route.js | 3 +- .../@username/@stashSlug/scenes/+Page.vue | 10 + .../scenes}/+onBeforeRender.js | 0 .../@username/@stashSlug/scenes/+route.js | 24 ++ renderer/+config.h.js | 9 +- renderer/container.vue | 87 ++++- renderer/usePageContext.js | 17 +- src/actors.js | 244 ++++++++++++-- src/scenes.js | 97 +++--- src/stashes.js | 60 +++- src/tools/manticore-scenes.js | 37 +-- src/tools/sync-stashes.js | 51 +++ src/web/actors.js | 3 +- src/web/scenes.js | 2 +- src/web/stashes.js | 14 +- utils/ellipsis.js | 11 + 37 files changed, 1292 insertions(+), 490 deletions(-) rename pages/stashes/@username/+Page.vue => components/stashes/stash.vue (60%) create mode 100644 pages/+config.js create mode 100644 pages/_error/+data.js create mode 100644 pages/stashes/@username/@stashSlug/actors/+Page.vue create mode 100644 pages/stashes/@username/@stashSlug/actors/+onBeforeRender.js create mode 100644 pages/stashes/@username/@stashSlug/actors/+route.js create mode 100644 pages/stashes/@username/@stashSlug/movies/+Page.vue create mode 100644 pages/stashes/@username/@stashSlug/movies/+onBeforeRender.js rename pages/stashes/@username/{ => @stashSlug/movies}/+route.js (82%) create mode 100644 pages/stashes/@username/@stashSlug/scenes/+Page.vue rename pages/stashes/@username/{ => @stashSlug/scenes}/+onBeforeRender.js (100%) create mode 100644 pages/stashes/@username/@stashSlug/scenes/+route.js create mode 100644 src/tools/sync-stashes.js create mode 100644 utils/ellipsis.js diff --git a/assets/css/tooltip.css b/assets/css/tooltip.css index d5d95e6..91792eb 100644 --- a/assets/css/tooltip.css +++ b/assets/css/tooltip.css @@ -192,3 +192,9 @@ .v-popper--theme-dropdown .v-popper__arrow-outer { border-color: #ddd; } + +.resize-observer { + width: 0; + height: 0; + overflow: hidden; +} diff --git a/components/actors/actors.vue b/components/actors/actors.vue index 19cd8f8..5a0c9b5 100644 --- a/components/actors/actors.vue +++ b/components/actors/actors.vue @@ -1,30 +1,297 @@ - + + diff --git a/components/actors/tile.vue b/components/actors/tile.vue index faf12ef..de162ca 100644 --- a/components/actors/tile.vue +++ b/components/actors/tile.vue @@ -1,19 +1,40 @@ - - diff --git a/pages/actors/+onBeforeRender.js b/pages/actors/+onBeforeRender.js index 9807c20..80d7be9 100644 --- a/pages/actors/+onBeforeRender.js +++ b/pages/actors/+onBeforeRender.js @@ -12,7 +12,7 @@ export async function onBeforeRender(pageContext) { page: Number(pageContext.routeParams.page) || 1, limit: Number(pageContext.urlParsed.search.limit) || 120, order: pageContext.urlParsed.search.order?.split('.') || ['likes', 'desc'], - }); + }, pageContext.user); return { pageContext: { diff --git a/pages/actors/@actorId/+Page.vue b/pages/actors/@actorId/+Page.vue index 0e1d303..cf435dc 100644 --- a/pages/actors/@actorId/+Page.vue +++ b/pages/actors/@actorId/+Page.vue @@ -25,20 +25,19 @@ -
@@ -50,15 +49,61 @@ diff --git a/pages/actors/@actorId/+onBeforeRender.js b/pages/actors/@actorId/+onBeforeRender.js index 80a7d2a..961a897 100644 --- a/pages/actors/@actorId/+onBeforeRender.js +++ b/pages/actors/@actorId/+onBeforeRender.js @@ -4,7 +4,7 @@ import { curateScenesQuery } from '#/src/web/scenes.js'; export async function onBeforeRender(pageContext) { const [[actor], actorScenes] = await Promise.all([ - fetchActorsById([Number(pageContext.routeParams.actorId)]), + fetchActorsById([Number(pageContext.routeParams.actorId)], {}, pageContext.user), fetchScenes(await curateScenesQuery({ ...pageContext.urlQuery, scope: pageContext.routeParams.scope || 'latest', @@ -13,7 +13,7 @@ export async function onBeforeRender(pageContext) { page: Number(pageContext.routeParams.page) || 1, limit: Number(pageContext.urlParsed.search.limit) || 30, aggregate: true, - }), + }, pageContext.user), ]); const { diff --git a/pages/scene/+Page.vue b/pages/scene/+Page.vue index e9b7cda..a63663a 100644 --- a/pages/scene/+Page.vue +++ b/pages/scene/+Page.vue @@ -26,6 +26,7 @@
{{ scene.title }}
-
+

Added

- {{ formatDate(scene.createdAt, 'yyyy-MM-dd hh:mm') }} batch #{{ scene.createdBatchId }} + {{ formatDate(scene.createdAt, 'yyyy-MM-dd') }} + #{{ scene.createdBatchId }}
+ + + + + + 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 @@ + + + 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" /> + +
@@ -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; +}