Added filterable stash pages.

This commit is contained in:
DebaucheryLibrarian 2024-03-15 00:08:24 +01:00
parent 7f00e31fc4
commit a1b45cb721
39 changed files with 649218 additions and 80 deletions

View File

@ -4,5 +4,8 @@
"@babel/preset-env"
]
],
"plugins": ["@babel/plugin-transform-optional-chaining"],
"plugins": [
"@babel/plugin-transform-optional-chaining",
"@babel/plugin-syntax-import-attributes"
],
}

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="17" height="16" viewBox="0 0 17 16">
<title>archive</title>
<path d="M4 1h-3c-0.55 0-1 0.45-1 1v12c0 0.55 0.45 1 1 1h3c0.55 0 1-0.45 1-1v-12c0-0.55-0.45-1-1-1zM3 12.625c0 0.206-0.169 0.375-0.375 0.375h-0.25c-0.206 0-0.375-0.169-0.375-0.375v-0.25c0-0.206 0.169-0.375 0.375-0.375h0.25c0.206 0 0.375 0.169 0.375 0.375v0.25zM4 10h-3v-5h3v5zM4 4h-3v-1h3v1zM10 1h-3c-0.55 0-1 0.45-1 1v12c0 0.55 0.45 1 1 1h3c0.55 0 1-0.45 1-1v-12c0-0.55-0.45-1-1-1zM9 12.625c0 0.206-0.169 0.375-0.375 0.375h-0.25c-0.206 0-0.375-0.169-0.375-0.375v-0.25c0-0.206 0.169-0.375 0.375-0.375h0.25c0.206 0 0.375 0.169 0.375 0.375v0.25zM10 10h-3v-5h3v5zM10 4h-3v-1h3v1zM16 1h-3c-0.55 0-1 0.45-1 1v12c0 0.55 0.45 1 1 1h3c0.55 0 1-0.45 1-1v-12c0-0.55-0.45-1-1-1zM15 12.625c0 0.206-0.169 0.375-0.375 0.375h-0.25c-0.206 0-0.375-0.169-0.375-0.375v-0.25c0-0.206 0.169-0.375 0.375-0.375h0.25c0.206 0 0.375 0.169 0.375 0.375v0.25zM16 10h-3v-5h3v5zM16 4h-3v-1h3v1z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

5
assets/img/icons/box.svg Normal file
View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>box</title>
<path d="M1 2h14v2h-14zM2 14h12v-9h-12v9zM6 6h4v1h-4v-1z"></path>
</svg>

After

Width:  |  Height:  |  Size: 223 B

View File

@ -0,0 +1,7 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>cabinet</title>
<path d="M2 16h12v-4h-12v4zM6 13.5c0-0.276 0.224-0.5 0.5-0.5h3c0.276 0 0.5 0.224 0.5 0.5v1c0 0.276-0.224 0.5-0.5 0.5s-0.5-0.224-0.5-0.5v-0.5h-2v0.5c0 0.276-0.224 0.5-0.5 0.5s-0.5-0.224-0.5-0.5v-1z"></path>
<path d="M2 11h12v-4h-12v4zM6 8.5c0-0.276 0.224-0.5 0.5-0.5h3c0.276 0 0.5 0.224 0.5 0.5v1c0 0.276-0.224 0.5-0.5 0.5s-0.5-0.224-0.5-0.5v-0.5h-2v0.5c0 0.276-0.224 0.5-0.5 0.5s-0.5-0.224-0.5-0.5v-1z"></path>
<path d="M11 0h-6l-3 2v4h12v-4l-3-2zM10 4.5c0 0.276-0.224 0.5-0.5 0.5s-0.5-0.224-0.5-0.5v-0.5h-2v0.5c0 0.276-0.224 0.5-0.5 0.5s-0.5-0.224-0.5-0.5v-1c0-0.276 0.224-0.5 0.5-0.5h3c0.276 0 0.5 0.224 0.5 0.5v1z"></path>
</svg>

After

Width:  |  Height:  |  Size: 787 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>drawer3</title>
<path d="M14.039 0l1.051 8.931-1.179 0.139-0.938-7.976h-9.944l-0.938 7.976-1.179-0.139 1.051-8.931zM4 2h8v1h-8zM4 4h8v1h-8zM4 6h8v1h-8zM4 8h8v1h-8zM15.5 10h-15c-0.275 0-0.429 0.213-0.342 0.474l1.684 5.051c0.087 0.261 0.383 0.474 0.658 0.474h11c0.275 0 0.571-0.213 0.658-0.474l1.684-5.051c0.087-0.261-0.067-0.474-0.342-0.474zM10 12h-4v-1h4v1z"></path>
</svg>

After

Width:  |  Height:  |  Size: 512 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>folder-open</title>
<path d="M13 15l3-8h-13l-3 8zM2 6l-2 9v-13h4.5l2 2h6.5v2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 232 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>folder-open2</title>
<path d="M15.383 6c0.412 0 0.684 0.331 0.603 0.735l-1.839 6.529c-0.081 0.405-0.485 0.735-0.897 0.735h-10.5c-0.413 0-0.816-0.331-0.897-0.735l-1.839-6.529c-0.081-0.404 0.19-0.735 0.603-0.735h14.766zM14 2.75v2.25h-12v-3.25c0-0.414 0.336-0.75 0.75-0.75h3.25l0.5 1h6.75c0.414 0 0.75 0.336 0.75 0.75z"></path>
</svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>folder</title>
<path d="M0 5h16l-1 10h-14l-1-10zM14.5 3l0.5 1h-14l1-2h5.5l0.5 1h6.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 239 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>folder2</title>
<path d="M7 2l2 2h7v11h-16v-13z"></path>
</svg>

After

Width:  |  Height:  |  Size: 202 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>folder5</title>
<path d="M15.383 4h-14.766c-0.412 0-0.684 0.331-0.603 0.735l1.839 8.529c0.081 0.405 0.485 0.735 0.897 0.735h10.5c0.412 0 0.816-0.331 0.897-0.735l1.839-8.529c0.081-0.404-0.19-0.735-0.603-0.735zM14 2.75c0-0.414-0.336-0.75-0.75-0.75h-6.75l-0.5-1h-3.25c-0.414 0-0.75 0.336-0.75 0.75v1.25h12v-0.25z"></path>
</svg>

After

Width:  |  Height:  |  Size: 464 B

View File

@ -0,0 +1,7 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>hipster</title>
<path d="M8 16c4.418 0 8-3.582 8-8s-3.582-8-8-8-8 3.582-8 8 3.582 8 8 8zM8 1.5c3.59 0 6.5 2.91 6.5 6.5s-2.91 6.5-6.5 6.5-6.5-2.91-6.5-6.5 2.91-6.5 6.5-6.5zM4 5c0-0.552 0.448-1 1-1s1 0.448 1 1-0.448 1-1 1-1-0.448-1-1zM10 5c0-0.552 0.448-1 1-1s1 0.448 1 1-0.448 1-1 1-1-0.448-1-1z"></path>
<path d="M10.561 8.439c-0.586-0.586-1.536-0.586-2.121 0s-0.586 1.536 0 2.121c0.019 0.019 0.038 0.037 0.058 0.055 1.352 1.227 4.503-0.029 4.503-1.615-0.969 0.625-1.726 0.153-2.439-0.561z"></path>
<path d="M5.439 8.439c0.586-0.586 1.536-0.586 2.121 0s0.586 1.536 0 2.121c-0.019 0.019-0.038 0.037-0.058 0.055-1.352 1.227-4.503-0.029-4.503-1.615 0.969 0.625 1.726 0.153 2.439-0.561z"></path>
</svg>

After

Width:  |  Height:  |  Size: 837 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>hipster2</title>
<path d="M8 0c-4.418 0-8 3.582-8 8s3.582 8 8 8 8-3.582 8-8-3.582-8-8-8zM11 4c0.552 0 1 0.448 1 1s-0.448 1-1 1-1-0.448-1-1 0.448-1 1-1zM5 4c0.552 0 1 0.448 1 1s-0.448 1-1 1-1-0.448-1-1 0.448-1 1-1zM8.497 10.615c-0.020-0.018-0.039-0.036-0.058-0.055-0.293-0.293-0.439-0.677-0.439-1.060-0 0.384-0.146 0.768-0.439 1.060-0.019 0.019-0.038 0.037-0.058 0.055-1.352 1.227-4.503-0.029-4.503-1.615 0.969 0.625 1.726 0.153 2.439-0.561 0.586-0.586 1.536-0.586 2.121 0 0.293 0.293 0.439 0.677 0.439 1.060 0-0.384 0.146-0.768 0.439-1.060 0.586-0.586 1.536-0.586 2.121 0 0.713 0.714 1.471 1.186 2.439 0.561 0 1.586-3.151 2.842-4.503 1.615z"></path>
</svg>

After

Width:  |  Height:  |  Size: 795 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>hour-glass</title>
<path d="M11.39 8c2.152-1.365 3.61-3.988 3.61-7 0-0.339-0.019-0.672-0.054-1h-13.891c-0.036 0.328-0.054 0.661-0.054 1 0 3.012 1.457 5.635 3.609 7-2.152 1.365-3.609 3.988-3.609 7 0 0.339 0.019 0.672 0.054 1h13.891c0.036-0.328 0.054-0.661 0.054-1 0-3.012-1.457-5.635-3.609-7zM2.5 15c0-2.921 1.253-5.397 3.5-6.214v-1.572c-2.247-0.817-3.5-3.294-3.5-6.214v0h11c0 2.921-1.253 5.397-3.5 6.214v1.572c2.247 0.817 3.5 3.294 3.5 6.214h-11zM9.682 10.462c-1.12-0.635-1.181-1.459-1.182-1.959v-1.004c0-0.5 0.059-1.327 1.184-1.963 0.602-0.349 1.122-0.88 1.516-1.537h-6.4c0.395 0.657 0.916 1.188 1.518 1.538 1.12 0.635 1.181 1.459 1.182 1.959v1.004c0 0.5-0.059 1.327-1.184 1.963-1.135 0.659-1.98 1.964-2.236 3.537h7.839c-0.256-1.574-1.102-2.879-2.238-3.538z"></path>
</svg>

After

Width:  |  Height:  |  Size: 913 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>hour-glass2</title>
<path d="M10.838 9.824c-1.169-0.761-1.838-1.188-1.838-1.824s0.67-1.064 1.838-1.824c1.882-1.183 3.162-3.505 3.162-6.176h-12c0 2.671 1.28 4.993 3.162 6.176 1.169 0.761 1.838 1.188 1.838 1.824s-0.67 1.064-1.838 1.824c-1.882 1.183-3.162 3.505-3.162 6.176h12c0-2.671-1.279-4.993-3.162-6.176zM4.057 3.028c-0.274-0.525-0.474-1.097-0.593-1.695h9.072c-0.119 0.598-0.318 1.17-0.593 1.695-0.184 0.352-0.399 0.678-0.64 0.972h-6.607c-0.241-0.294-0.455-0.619-0.64-0.972zM7.75 11.963c0 0.681-2.75 1.704-2.75 2.704h-1.536c0.119-0.598 0.318-1.17 0.593-1.695 0.447-0.855 1.074-1.553 1.814-2.018l0.009-0.006 0.009-0.006 0.048-0.031c0.73-0.475 1.376-0.896 1.813-1.399v2.451zM12.536 14.667h-1.536c0-1-2.75-2.023-2.75-2.704v-2.451c0.437 0.503 1.083 0.924 1.813 1.399l0.057 0.037 0.009 0.006c0.74 0.465 1.367 1.163 1.814 2.018 0.274 0.525 0.474 1.097 0.593 1.695z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1015 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>hour-glass3</title>
<path d="M5.939 9.532c-1.49 0.81-2.591 2.472-2.87 4.468h9.862c-0.279-1.996-1.379-3.658-2.87-4.468-1.061-0.754-1.061-1.116-1.061-1.532s0-0.778 1.061-1.532c1.49-0.81 2.591-2.472 2.87-4.468h-9.862c0.279 1.996 1.379 3.658 2.87 4.468 1.061 0.754 1.061 1.116 1.061 1.532s-0 0.778-1.061 1.532zM2 15h12v1h-12zM2 0h12v1h-12z"></path>
</svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>mustache2</title>
<path d="M14.645 8.021c-0.211-0.064-0.439 0.018-0.561 0.201-0.327 0.491-0.734 0.761-1.146 0.761-0.408 0-0.793-0.271-0.982-0.689l-0.007-0.015c-0.229-0.509-0.466-1.035-0.815-1.462-0.442-0.542-0.992-0.817-1.634-0.817-0.817 0-1.544 0.394-2 1.002-0.456-0.608-1.183-1.002-2-1.002-0.642 0-1.192 0.275-1.634 0.817-0.348 0.427-0.586 0.953-0.815 1.462l-0.007 0.015c-0.189 0.419-0.574 0.689-0.982 0.689-0.412 0-0.819-0.27-1.146-0.761-0.122-0.183-0.35-0.265-0.561-0.201s-0.355 0.258-0.355 0.479c0 1.074 0.419 1.978 1.212 2.615 0.711 0.571 1.701 0.885 2.788 0.885 1.038 0 2.035-0.378 2.807-1.064 0.276-0.245 0.508-0.518 0.693-0.809 0.185 0.291 0.417 0.564 0.693 0.809 0.772 0.686 1.769 1.064 2.807 1.064 1.087 0 2.077-0.314 2.788-0.885 0.793-0.636 1.212-1.541 1.212-2.615 0-0.22-0.144-0.415-0.355-0.479zM6.143 10.189c-0.589 0.523-1.35 0.811-2.143 0.811-1.331 0-2.146-0.486-2.584-1.125 0.254 0.086 0.479 0.109 0.646 0.109 0.8-0 1.543-0.502 1.894-1.278l0.007-0.015c0.474-1.051 0.814-1.69 1.537-1.69 0.827 0 1.5 0.673 1.5 1.5 0 0.589-0.312 1.205-0.857 1.689zM11 11c-0.793 0-1.554-0.288-2.143-0.811-0.545-0.484-0.857-1.099-0.857-1.689 0-0.827 0.673-1.5 1.5-1.5 0.723 0 1.063 0.638 1.537 1.69l0.007 0.015c0.35 0.777 1.093 1.278 1.894 1.278 0.171 0 0.404-0.024 0.666-0.116-0.565 0.815-1.641 1.132-2.604 1.132z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -147,6 +147,7 @@ const {
actor: pageActor,
tag: pageTag,
entity: pageEntity,
stash: pageStash,
} = pageProps;
const scenes = ref(pageProps.scenes);
@ -229,6 +230,7 @@ async function search(options = {}) {
...query,
actors: [pageActor, ...filters.value.actors].filter(Boolean).map((filterActor) => getActorIdentifier(filterActor)).join(','), // if we're on an actor page, that actor ID needs to be included
tags: [pageTag?.slug, ...filters.value.tags].filter(Boolean).join(','),
stashId: pageStash?.id,
e: entitySlug,
scope: scope.value,
page: currentPage.value, // client uses param rather than query pagination

View File

@ -1,5 +1,10 @@
<template>
<div class="tile">
<div
class="tile"
:class="{
unstashed: !favorited && pageStash && user && pageStash.id === user.primaryStash?.id
}"
>
<div class="poster-container">
<Link
:href="`/scene/${scene.id}/${scene.slug}`"
@ -112,6 +117,7 @@ const props = defineProps({
const pageContext = inject('pageContext');
const user = pageContext.user;
const pageStash = pageContext.pageProps.stash;
const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.primary));
@ -125,10 +131,13 @@ async function stash() {
}
async function unstash() {
console.log('unstash!', user);
try {
favorited.value = false;
await del(`/stashes/${user.primaryStash.id}/scenes/${props.scene.id}`);
} catch (error) {
console.error(error);
favorited.value = true;
}
}
@ -149,6 +158,10 @@ async function unstash() {
fill: var(--text-light);
}
}
&.unstashed {
opacity: .5;
}
}
.poster-container {

View File

@ -17,6 +17,7 @@ module.exports = {
host: '127.0.0.1',
sqlPort: 9306,
httpPort: 9308,
forceSql: true,
maxMatches: 2000, // high match count needed primarily for actor aggregations
maxAggregateSize: 2000, // must be lower or equal to maxMatches
maxQueryTime: 10000,

282
package-lock.json generated
View File

@ -10,6 +10,7 @@
"@brillout/json-serializer": "^0.5.8",
"@dicebear/collection": "^7.0.5",
"@dicebear/core": "^7.0.5",
"@faker-js/faker": "^8.4.1",
"@floating-ui/dom": "^1.5.3",
"@floating-ui/vue": "^1.0.2",
"@resvg/resvg-js": "^2.6.0",
@ -31,12 +32,15 @@
"express-query-boolean": "^2.0.0",
"express-session": "^1.18.0",
"floating-vue": "^5.2.2",
"ip-cidr": "^4.0.0",
"knex": "^3.1.0",
"manticoresearch": "^4.0.0",
"markdown-it": "^14.0.0",
"mathjs": "^12.2.1",
"mitt": "^3.0.1",
"mysql": "^2.18.1",
"nanoid": "^5.0.4",
"object.omit": "^3.0.0",
"path-to-regexp": "^6.2.1",
"pg": "^8.11.3",
"redis": "^4.6.12",
@ -54,6 +58,7 @@
"@babel/cli": "^7.23.4",
"@babel/core": "^7.23.6",
"@babel/eslint-parser": "^7.23.3",
"@babel/plugin-syntax-import-attributes": "^7.23.3",
"@babel/plugin-transform-optional-chaining": "^7.23.4",
"@babel/preset-env": "^7.23.6",
"@csstools/postcss-global-data": "^2.1.1",
@ -2867,6 +2872,21 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@faker-js/faker": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz",
"integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/fakerjs"
}
],
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
"npm": ">=6.14.13"
}
},
"node_modules/@floating-ui/core": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz",
@ -4029,6 +4049,14 @@
}
]
},
"node_modules/bignumber.js": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==",
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -4527,6 +4555,11 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
@ -6301,6 +6334,29 @@
"node": ">= 0.10"
}
},
"node_modules/ip-address": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "^1.1.3"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/ip-cidr": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/ip-cidr/-/ip-cidr-4.0.0.tgz",
"integrity": "sha512-i1Jhb9sqm2+PuOHTfya3ekAUi+dadhgcEz+4FKKY1hXemocP4Xf7io8Xflc74/i2ejxe/5fp4z8z3BAsfAZ8sw==",
"dependencies": {
"ip-address": "^9.0.5"
},
"engines": {
"node": ">=16.14.0"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -6407,6 +6463,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extendable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
"integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
"dependencies": {
"is-plain-object": "^2.0.4"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -6478,6 +6545,17 @@
"node": ">=8"
}
},
"node_modules/is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
"integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
"dependencies": {
"isobject": "^3.0.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
@ -6590,6 +6668,14 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/isobject": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
@ -6630,6 +6716,11 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
},
"node_modules/jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@ -7069,6 +7160,47 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/mysql": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
"integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==",
"dependencies": {
"bignumber.js": "9.0.0",
"readable-stream": "2.3.7",
"safe-buffer": "5.1.2",
"sqlstring": "2.3.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mysql/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/mysql/node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/mysql/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/nanoid": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.4.tgz",
@ -7267,6 +7399,17 @@
"get-intrinsic": "^1.2.1"
}
},
"node_modules/object.omit": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object.omit/-/object.omit-3.0.0.tgz",
"integrity": "sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==",
"dependencies": {
"is-extendable": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object.values": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz",
@ -7765,6 +7908,11 @@
"node": ">= 0.8.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -8498,6 +8646,19 @@
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
},
"node_modules/sqlstring": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
"integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
@ -11857,6 +12018,11 @@
"integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
"dev": true
},
"@faker-js/faker": {
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz",
"integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg=="
},
"@floating-ui/core": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.2.tgz",
@ -12657,6 +12823,11 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"bignumber.js": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@ -13006,6 +13177,11 @@
"browserslist": "^4.22.2"
}
},
"core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
@ -14317,6 +14493,23 @@
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
"integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw=="
},
"ip-address": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
"requires": {
"jsbn": "1.1.0",
"sprintf-js": "^1.1.3"
}
},
"ip-cidr": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/ip-cidr/-/ip-cidr-4.0.0.tgz",
"integrity": "sha512-i1Jhb9sqm2+PuOHTfya3ekAUi+dadhgcEz+4FKKY1hXemocP4Xf7io8Xflc74/i2ejxe/5fp4z8z3BAsfAZ8sw==",
"requires": {
"ip-address": "^9.0.5"
}
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -14390,6 +14583,14 @@
"has-tostringtag": "^1.0.0"
}
},
"is-extendable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
"integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
"requires": {
"is-plain-object": "^2.0.4"
}
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -14434,6 +14635,14 @@
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
"dev": true
},
"is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
"integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
"requires": {
"isobject": "^3.0.1"
}
},
"is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
@ -14510,6 +14719,11 @@
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"isobject": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
"integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="
},
"jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
@ -14539,6 +14753,11 @@
"argparse": "^2.0.1"
}
},
"jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
},
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@ -14860,6 +15079,46 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"mysql": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
"integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==",
"requires": {
"bignumber.js": "9.0.0",
"readable-stream": "2.3.7",
"safe-buffer": "5.1.2",
"sqlstring": "2.3.1"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
},
"nanoid": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.4.tgz",
@ -15003,6 +15262,14 @@
"get-intrinsic": "^1.2.1"
}
},
"object.omit": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object.omit/-/object.omit-3.0.0.tgz",
"integrity": "sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==",
"requires": {
"is-extendable": "^1.0.0"
}
},
"object.values": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz",
@ -15338,6 +15605,11 @@
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -15857,6 +16129,16 @@
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="
},
"sprintf-js": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
},
"sqlstring": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
"integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ=="
},
"stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",

View File

@ -10,6 +10,7 @@
"@brillout/json-serializer": "^0.5.8",
"@dicebear/collection": "^7.0.5",
"@dicebear/core": "^7.0.5",
"@faker-js/faker": "^8.4.1",
"@floating-ui/dom": "^1.5.3",
"@floating-ui/vue": "^1.0.2",
"@resvg/resvg-js": "^2.6.0",
@ -31,12 +32,15 @@
"express-query-boolean": "^2.0.0",
"express-session": "^1.18.0",
"floating-vue": "^5.2.2",
"ip-cidr": "^4.0.0",
"knex": "^3.1.0",
"manticoresearch": "^4.0.0",
"markdown-it": "^14.0.0",
"mathjs": "^12.2.1",
"mitt": "^3.0.1",
"mysql": "^2.18.1",
"nanoid": "^5.0.4",
"object.omit": "^3.0.0",
"path-to-regexp": "^6.2.1",
"pg": "^8.11.3",
"redis": "^4.6.12",
@ -55,6 +59,7 @@
"@babel/cli": "^7.23.4",
"@babel/core": "^7.23.6",
"@babel/eslint-parser": "^7.23.3",
"@babel/plugin-syntax-import-attributes": "^7.23.3",
"@babel/plugin-transform-optional-chaining": "^7.23.4",
"@babel/preset-env": "^7.23.6",
"@csstools/postcss-global-data": "^2.1.1",

View File

@ -0,0 +1,91 @@
<template>
<div class="stash">
<div class="header">
<h2 class="title">
<Icon
v-if="stash.primary"
icon="heart7"
/>
<Icon
v-else
icon="box"
/>
{{ stash.name }}
</h2>
<a
:href="`/user/${stash.user.username}`"
class="user nolink"
>
<img
:src="stash.user.avatar"
class="avatar"
>{{ stash.user.username }}
</a>
</div>
<div class="scenes-container">
<Scenes />
</div>
</div>
</template>
<script setup>
import { inject } from 'vue';
import Scenes from '#/components/scenes/scenes.vue';
const pageContext = inject('pageContext');
const stash = pageContext.pageProps.stash;
console.log(stash);
</script>
<style scoped>
.stash {
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
display: flex;
justify-content: space-between;
padding: .5rem 1rem;
color: var(--text-light);
background: var(--grey-dark-40);
}
.title {
margin: 0;
text-transform: capitalize;
display: flex;
align-items: center;
.icon {
width: 1.5rem;
height: 1.5rem;
margin-right: 1rem;
fill: var(--text-light);
}
}
.user {
display: flex;
align-items: center;
font-weight: bold;
}
.avatar {
width: 1.5rem;
height: 1.5rem;
margin-right: 1rem;
border-radius: .25rem;
}
.scenes-container {
overflow-y: auto;
}
</style>

View File

@ -0,0 +1,51 @@
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
import { fetchStashByUsernameAndSlug } from '#/src/stashes.js';
import { fetchScenes } from '#/src/scenes.js';
import { curateScenesQuery } from '#/src/web/scenes.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 stashScenes = await fetchScenes(await curateScenesQuery({
...pageContext.urlQuery,
scope: pageContext.routeParams.scope || 'latest',
stashId: stash.id,
}), {
page: Number(pageContext.routeParams.page) || 1,
limit: Number(pageContext.urlParsed.search.limit) || 30,
}, pageContext.user);
const {
scenes,
aggActors,
aggTags,
aggChannels,
total,
limit,
} = stashScenes;
return {
pageContext: {
title: `${stash.name} by ${stash.user.username}`,
pageProps: {
stash,
scenes,
aggActors,
aggTags,
aggChannels,
total,
limit,
},
},
};
} catch (error) {
if (error instanceof HttpError) {
throw render(error.httpCode, error.message);
}
throw error;
}
}

View File

@ -0,0 +1,23 @@
import { match } from 'path-to-regexp';
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
const path = '/stash/:username/:stashSlug/: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,
scope: matched.params.scope || 'latest',
page: matched.params.page || '1',
path,
},
};
}
return false;
};

View File

@ -8,7 +8,9 @@ import { shapes } from '@dicebear/collection';
import { knexOwner as knex } from './knex.js';
import { curateUser, fetchUser } from './users.js';
import { HttpError } from './errors.js';
import initLogger from './logger.js';
const logger = initLogger();
const scrypt = util.promisify(crypto.scrypt);
async function verifyPassword(password, storedPassword) {
@ -32,22 +34,21 @@ async function generateAvatar(user) {
});
await fs.mkdir('media/avatars', { recursive: true });
await avatar.png().toFile(`media/avatars/${user.id}_${user.username}.png`);
logger.verbose(`Generated avatar for '${user.username}' (${user.id})`);
}
export async function login(credentials) {
export async function login(credentials, userIp) {
if (!config.auth.login) {
throw new HttpError('Logins are currently disabled', 405);
}
const user = await fetchUser(credentials.username.trim(), {
const { user, stashes } = await fetchUser(credentials.username.trim(), {
email: true,
raw: true,
});
console.log('login user', user);
if (!user) {
throw new HttpError('Username or password incorrect', 401);
}
@ -58,15 +59,21 @@ export async function login(credentials) {
.update('last_login', 'NOW()')
.where('id', user.id);
if (!user.avatar) {
console.log('login user', user);
logger.verbose(`Login from '${user.username}' (${user.id}, ${userIp})`);
try {
await fs.access(`media/avatars/${user.id}_${user.username}.png`);
} catch (error) {
await generateAvatar(user);
}
// fetched the raw user for password verification, don't return directly to user
return curateUser(user);
return curateUser(user, { stashes });
}
export async function signup(credentials) {
export async function signup(credentials, userIp) {
if (!config.auth.signup) {
throw new HttpError('Sign-ups are currently disabled', 405);
}
@ -126,6 +133,8 @@ export async function signup(credentials) {
primary: true,
});
logger.verbose(`Signup from '${curatedUsername}' (${userId}, ${credentials.email}, ${userIp})`);
await generateAvatar({
id: userId,
username: curatedUsername,

View File

@ -19,4 +19,17 @@ export const knexOwner = knex({
// debug: process.env.NODE_ENV === 'development',
});
export const knexManticore = knex({
client: 'mysql',
connection: {
host: config.database.manticore.host,
port: config.database.manticore.sqlPort,
database: 'Manticore',
},
asyncStackTraces: process.env.NODE_ENV === 'development',
wrapIdentifier(value, _original, _queryContext) {
return value;
},
});
export default knexQuery;

View File

@ -6,3 +6,5 @@ const mantiClient = new manticore.ApiClient();
mantiClient.basePath = `http://${config.database.manticore.host}:${config.database.manticore.httpPort}`;
export const searchApi = new manticore.SearchApi(mantiClient);
export const indexApi = new manticore.IndexApi(mantiClient);
export const utilsApi = new manticore.UtilsApi();

View File

@ -1,7 +1,8 @@
import config from 'config';
import util from 'util'; /* eslint-disable-line no-unused-vars */
import { knexOwner as knex } from './knex.js';
import { searchApi } from './manticore.js';
import { knexOwner as knex, knexManticore } from './knex.js';
import { searchApi, utilsApi } from './manticore.js';
import { HttpError } from './errors.js';
import { fetchActorsById, curateActor, sortActorsByGender } from './actors.js';
import { fetchTagsById } from './tags.js';
@ -151,6 +152,8 @@ export async function fetchScenesById(sceneIds, reqUser) {
}).filter(Boolean);
}
const sqlImplied = ['scenes_stashed'];
function curateOptions(options) {
if (options?.limit > 100) {
throw new HttpError('Limit must be <= 100', 400);
@ -163,10 +166,12 @@ function curateOptions(options) {
aggregateActors: (options.aggregate ?? true) && (options.aggregateActors ?? true),
aggregateTags: (options.aggregate ?? true) && (options.aggregateTags ?? true),
aggregateChannels: (options.aggregate ?? true) && (options.aggregateChannels ?? true),
index: options.index || 'scenes',
useSql: options.useSql || (typeof options.useSql === 'undefined' && sqlImplied.includes(options.index)) || false,
};
}
function buildQuery(filters = {}) {
function buildQuery(filters = {}, options) {
const query = {
bool: {
must: [],
@ -210,6 +215,7 @@ function buildQuery(filters = {}) {
}
if (filters.query) {
/*
query.bool.must.push({
bool: {
should: [
@ -224,6 +230,9 @@ function buildQuery(filters = {}) {
],
},
});
*/
query.bool.must.push({ match: { '!title': filters.query } }); // title_filtered is matched instead of title
}
if (filters.tagIds) {
@ -249,6 +258,10 @@ function buildQuery(filters = {}) {
});
}
if (filters.stashId && options.index === 'scenes_stashed') {
query.bool.must.push({ equals: { stash_id: filters.stashId } });
}
/* tag filter
must_not: [
{
@ -281,6 +294,7 @@ function buildAggregates(options) {
field: 'tag_ids',
size: config.database.manticore.maxAggregateSize,
},
sort: [{ 'count(*)': { order: 'desc' } }],
};
}
@ -290,6 +304,7 @@ function buildAggregates(options) {
field: 'channel_id',
size: config.database.manticore.maxAggregateSize,
},
sort: [{ 'count(*)': { order: 'desc' } }],
};
}
@ -304,20 +319,11 @@ function countAggregations(buckets) {
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
}
export async function fetchScenes(filters, rawOptions, reqUser) {
const options = curateOptions(rawOptions);
const { query, sort } = buildQuery(filters);
console.log('filters', filters);
console.log('options', options);
console.log('query', query.bool.must);
console.log('request user', reqUser);
console.time('manticore');
async function queryManticoreJson(filters, options, _reqUser) {
const { query, sort } = buildQuery(filters, options);
const result = await searchApi.search({
index: 'scenes',
index: options.index,
query,
limit: options.limit,
offset: (options.page - 1) * options.limit,
@ -339,31 +345,181 @@ export async function fetchScenes(filters, rawOptions, reqUser) {
},
});
console.timeEnd('manticore');
const scenes = result.hits.hits.map((hit) => ({
id: hit._id,
...hit._source,
_score: hit._score,
}));
const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds?.buckets);
const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds?.buckets);
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds?.buckets);
return {
scenes,
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 = 10 || 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:
:actorsFacet:
:tagsFacet:
:channelsFacet:
`, {
query: knexManticore('scenes')
.select(knex.raw('*, weight() as _score'))
.modify((builder) => {
if (filters.stashId) {
builder
.innerJoin('scenes_stashed', 'scenes.id', 'scenes_stashed.scene_id')
.where('scenes_stashed.stash_id', filters.stashId);
}
if (filters.query) {
builder.whereRaw('match(\'@!title :query:\', scenes)', { query: filters.query });
}
if (filters.tagIds?.length > 0) {
builder.whereIn('any(tag_ids)', filters.tagIds);
}
if (filters.entityId) {
builder.where((whereBuilder) => {
whereBuilder
.where('channel_id', filters.entityId)
.orWhere('network_id', filters.entityId);
});
}
if (filters.actorIds?.length > 0) {
builder.whereIn('any(actor_ids)', filters.actorIds);
}
if (!filters.scope || filters.scope === 'latest') {
builder
.where('effective_date', '<=', Math.round(Date.now() / 1000))
.orderBy('effective_date', 'desc');
} else if (filters.scope === 'upcoming') {
builder
.where('effective_date', '>', Math.round(Date.now() / 1000))
.orderBy('effective_date', 'asc');
} else if (filters.scope === 'new') {
builder.orderBy([
{ column: 'created_at', order: 'desc' },
{ column: 'effective_date', order: 'asc' },
]);
} else if (filters.scope === 'likes') {
builder.orderBy([
{ column: 'stashed', order: 'desc' },
{ column: 'effective_date', order: 'desc' },
]);
} else if (filters.scope === 'results') {
builder.orderBy([
{ column: '_score', order: 'desc' },
{ column: 'effective_date', order: 'desc' },
]);
} else {
builder.orderBy('effective_date', 'desc');
}
})
.limit(options.limit)
.toString(),
// option threads=1 fixes actors, but drastically slows down performance, wait for fix
actorsFacet: options.aggregateActors ? knex.raw('facet actor_ids order by count(*) desc limit ?', [aggSize]) : null,
tagsFacet: options.aggregateTags ? knex.raw('facet tag_ids order by count(*) desc limit ?', [aggSize]) : null,
channelsFacet: options.aggregateChannels ? knex.raw('facet channel_id order by count(*) desc limit ?', [aggSize]) : null,
maxMatches: config.database.manticore.maxMatches,
maxQueryTime: config.database.manticore.maxQueryTime,
}).toString();
console.log(sqlQuery);
const results = await utilsApi.sql(sqlQuery);
const actorIds = results
.find((result) => result.columns[0].actor_ids && result.columns[1]['count(*)'])
?.data.map((row) => ({ key: row.actor_ids, doc_count: row['count(*)'] }))
|| [];
const tagIds = results
.find((result) => result.columns[0].tag_ids && result.columns[1]['count(*)'])
?.data.map((row) => ({ key: row.tag_ids, doc_count: row['count(*)'] }))
|| [];
const channelIds = results
.find((result) => result.columns[0].channel_id && result.columns[1]['count(*)'])
?.data.map((row) => ({ key: row.channel_id, doc_count: row['count(*)'] }))
|| [];
return {
scenes: results[0].data,
total: results[0].total,
aggregations: {
actorIds,
tagIds,
channelIds,
},
};
}
export async function fetchScenes(filters, rawOptions, reqUser) {
const options = curateOptions(rawOptions);
console.log('filters', filters);
console.log('options', options);
/*
const result = config.database.manticore.forceSql || filters.stashId
? await queryManticoreSql(filters, options, reqUser)
: await queryManticoreJson(filters, options, reqUser);
*/
console.time('manticore sql');
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);
console.time('fetch aggregations');
const [aggActors, aggTags, aggChannels] = await Promise.all([
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: actorCounts }) : [],
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: tagCounts }) : [],
options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'], append: channelCounts }) : [],
options.aggregateActors ? fetchActorsById(result.aggregations.actorIds.map((bucket) => bucket.key), { order: ['name', 'asc'], append: actorCounts }) : [],
options.aggregateTags ? fetchTagsById(result.aggregations.tagIds.map((bucket) => bucket.key), { order: ['name', 'asc'], append: tagCounts }) : [],
options.aggregateChannels ? fetchEntitiesById(result.aggregations.channelIds.map((bucket) => bucket.key), { order: ['name', 'asc'], append: channelCounts }) : [],
]);
console.timeEnd('fetch aggregations');
const sceneIds = result.hits.hits.map((hit) => Number(hit._id));
console.time('fetch full');
const sceneIds = result.scenes.map((scene) => Number(scene.id));
const scenes = await fetchScenesById(sceneIds, reqUser);
console.timeEnd('fetch full');
return {
scenes,
aggActors,
aggTags,
aggChannels,
total: result.hits.total,
total: result.total,
limit: options.limit,
};
}

View File

@ -1,6 +1,7 @@
import config from 'config';
import { knexOwner as knex } from './knex.js';
import { indexApi } from './manticore.js';
import { HttpError } from './errors.js';
import slugify from './utils/slugify.js';
import initLogger from './logger.js';
@ -9,7 +10,7 @@ const logger = initLogger();
let lastActorsViewRefresh = 0;
export function curateStash(stash) {
export function curateStash(stash, assets = {}) {
if (!stash) {
return null;
}
@ -24,6 +25,12 @@ export function curateStash(stash) {
stashedScenes: stash.stashed_scenes || null,
stashedMovies: stash.stashed_movies || null,
stashedActors: stash.stashed_actors || null,
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,
};
return curatedStash;
@ -40,23 +47,39 @@ function curateStashEntry(stash, user) {
return curatedStashEntry;
}
export async function fetchStash(stashId, sessionUser) {
if (!sessionUser) {
throw new HttpError('You are not authenthicated', 401);
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);
}
}
export async function fetchStashById(stashId, sessionUser) {
const stash = await knex('stashes')
.where('id', stashId)
.first();
verifyStashAccess(stash, sessionUser);
return curateStash(stash);
}
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')
.where({
id: stashId,
user_id: sessionUser.id,
})
.select('stashes.*', 'stashes_meta.*')
.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id')
.where('slug', stashSlug)
.where('user_id', user.id)
.first();
if (!stash) {
throw new HttpError('You are not authorized to access this stash', 403);
}
verifyStashAccess(stash, sessionUser);
return curateStash(stash);
return curateStash(stash, { user });
}
export async function fetchStashes(domain, itemId, sessionUser) {
@ -145,7 +168,7 @@ export async function refreshActorsView() {
}
export async function stashActor(actorId, stashId, sessionUser) {
const stash = await fetchStash(stashId, sessionUser);
const stash = await fetchStashById(stashId, sessionUser);
await knex('stashes_actors')
.insert({
@ -158,30 +181,6 @@ export async function stashActor(actorId, stashId, sessionUser) {
return fetchStashes('actor', actorId, sessionUser);
}
export async function stashScene(sceneId, stashId, sessionUser) {
const stash = await fetchStash(stashId, sessionUser);
await knex('stashes_scenes')
.insert({
stash_id: stash.id,
scene_id: sceneId,
});
return fetchStashes('scene', sceneId, sessionUser);
}
export async function stashMovie(movieId, stashId, sessionUser) {
const stash = await fetchStash(stashId, sessionUser);
await knex('stashes_movies')
.insert({
stash_id: stash.id,
movie_id: movieId,
});
return fetchStashes('movie', movieId, sessionUser);
}
export async function unstashActor(actorId, stashId, sessionUser) {
await knex
.from('stashes_actors AS deletable')
@ -198,6 +197,18 @@ export async function unstashActor(actorId, stashId, sessionUser) {
return fetchStashes('actor', actorId, sessionUser);
}
export async function stashScene(sceneId, stashId, sessionUser) {
const stash = await fetchStashById(stashId, sessionUser);
await knex('stashes_scenes')
.insert({
stash_id: stash.id,
scene_id: sceneId,
});
return fetchStashes('scene', sceneId, sessionUser);
}
export async function unstashScene(sceneId, stashId, sessionUser) {
await knex
.from('stashes_scenes AS deletable')
@ -209,9 +220,34 @@ export async function unstashScene(sceneId, stashId, sessionUser) {
.where('stashes.user_id', sessionUser.id))
.delete();
await indexApi.callDelete({
index: 'scenes_stashed',
query: {
bool: {
must: [
{ equals: { id: sceneId } },
{ equals: { stash_id: stashId } },
{ equals: { user_id: sessionUser.id } },
],
},
},
});
return fetchStashes('scene', sceneId, sessionUser);
}
export async function stashMovie(movieId, stashId, sessionUser) {
const stash = await fetchStashById(stashId, sessionUser);
await knex('stashes_movies')
.insert({
stash_id: stash.id,
movie_id: movieId,
});
return fetchStashes('movie', movieId, sessionUser);
}
export async function unstashMovie(movieId, stashId, sessionUser) {
await knex
.from('stashes_movies AS deletable')

View File

@ -0,0 +1,82 @@
import { indexApi, utilsApi } from '../manticore.js';
import rawMovies from './movies.json' with { type: 'json' };
async function fetchMovies() {
const movies = rawMovies
.filter((movie) => movie.cast.length > 0
&& movie.genres.length > 0
&& movie.cast.every((actor) => actor.charCodeAt(0) >= 65)) // throw out movies with non-alphanumerical actor names
.map((movie, index) => ({ id: index, ...movie }));
const actors = Array.from(new Set(movies.flatMap((movie) => movie.cast))).sort();
const genres = Array.from(new Set(movies.flatMap((movie) => movie.genres)));
return {
movies,
actors,
genres,
};
}
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,
actor_ids multi,
actors text,
genre_ids multi,
genres text
)`);
await utilsApi.sql(`create table movies_liked (
id int,
user_id int,
movie_id int
)`);
const { movies, actors, genres } = await fetchMovies();
const likedMovieIds = Array.from(new Set(Array.from({ length: 10.000 }, () => movies[Math.round(Math.random() * movies.length)].id)));
const docs = movies
.map((movie) => ({
replace: {
index: 'movies',
id: movie.id,
doc: {
title: movie.title,
actor_ids: movie.cast.map((actor) => actors.indexOf(actor)),
actors: movie.cast.join(','),
genre_ids: movie.genres.map((genre) => genres.indexOf(genre)),
genres: movie.genres.join(','),
},
},
}))
.concat(likedMovieIds.map((movieId, index) => ({
replace: {
index: 'movies_liked',
id: index + 1,
doc: {
user_id: Math.floor(Math.random() * 51),
movie_id: movieId,
},
},
})));
const data = await indexApi.bulk(docs.map((doc) => JSON.stringify(doc)).join('\n'));
console.log('data', data);
const result = await utilsApi.sql(`
select * from movies_liked
limit 10
`);
console.log(result[0].data);
console.log(result[1]);
}
init();

View File

@ -0,0 +1,203 @@
// import config from 'config';
import { format } from 'date-fns';
import { faker } from '@faker-js/faker';
import { indexApi, utilsApi } from '../manticore.js';
import { knexOwner as knex } from '../knex.js';
import slugify from '../utils/slugify.js';
import chunk from '../utils/chunk.js';
async function fetchScenes() {
const scenes = await knex.raw(`
SELECT
releases.id AS id,
releases.title,
releases.created_at,
releases.date,
releases.shoot_id,
scenes_meta.stashed,
entities.id as channel_id,
entities.slug as channel_slug,
entities.name as channel_name,
parents.id as network_id,
parents.slug as network_slug,
parents.name as network_name,
COALESCE(JSON_AGG(DISTINCT (actors.id, actors.name)) FILTER (WHERE actors.id IS NOT NULL), '[]') as actors,
COALESCE(JSON_AGG(DISTINCT (tags.id, tags.name, tags.priority, tags_aliases.name)) FILTER (WHERE tags.id IS NOT NULL), '[]') as tags
FROM releases
LEFT JOIN scenes_meta ON scenes_meta.scene_id = releases.id
LEFT JOIN entities ON releases.entity_id = entities.id
LEFT JOIN entities AS parents ON parents.id = entities.parent_id
LEFT JOIN releases_actors AS local_actors ON local_actors.release_id = releases.id
LEFT JOIN releases_directors AS local_directors ON local_directors.release_id = releases.id
LEFT JOIN releases_tags AS local_tags ON local_tags.release_id = releases.id
LEFT JOIN actors ON local_actors.actor_id = actors.id
LEFT JOIN actors AS directors ON local_directors.director_id = directors.id
LEFT JOIN tags ON local_tags.tag_id = tags.id
LEFT JOIN tags as tags_aliases ON local_tags.tag_id = tags_aliases.alias_for AND tags_aliases.secondary = true
GROUP BY
releases.id,
releases.title,
releases.created_at,
releases.date,
releases.shoot_id,
scenes_meta.stashed,
entities.id,
entities.name,
entities.slug,
entities.alias,
parents.id,
parents.name,
parents.slug,
parents.alias;
`);
const actors = Object.fromEntries(scenes.rows.flatMap((row) => row.actors.map((actor) => [actor.f1, faker.person.fullName()])));
const tags = Object.fromEntries(scenes.rows.flatMap((row) => row.tags.map((tag) => [tag.f1, faker.word.adjective()])));
return scenes.rows.map((row) => {
const title = faker.lorem.lines(1);
const channelName = faker.company.name();
const channelSlug = slugify(channelName, '');
const networkName = faker.company.name();
const networkSlug = slugify(networkName, '');
const rowActors = row.actors.map((actor) => ({ f1: actor.f1, f2: actors[actor.f1] }));
const rowTags = row.tags.map((tag) => ({ f1: tag.f1, f2: tags[tag.f1], f3: tag.f3 }));
return {
...row,
title,
actors: rowActors,
tags: rowTags,
channel_name: channelName,
channel_slug: channelSlug,
network_name: networkName,
network_slug: networkSlug,
};
});
}
async function updateStashed(docs) {
await chunk(docs, 1000).reduce(async (chain, docsChunk) => {
await chain;
const sceneIds = docsChunk.map((doc) => doc.replace.id);
const stashes = await knex('stashes_scenes')
.select('stashes_scenes.id as stashed_id', 'stashes_scenes.scene_id', 'stashes.id as stash_id', 'stashes.user_id as user_id')
.leftJoin('stashes', 'stashes.id', 'stashes_scenes.stash_id')
.whereIn('scene_id', sceneIds);
if (stashes.length > 0) {
console.log(stashes);
}
const stashDocs = docsChunk.flatMap((doc) => {
const sceneStashes = stashes.filter((stash) => stash.scene_id === doc.replace.id);
if (sceneStashes.length === 0) {
return [];
}
const stashDoc = sceneStashes.map((stash) => ({
replace: {
index: 'movies_liked',
id: stash.stashed_id,
doc: {
// ...doc.replace.doc,
movie_id: doc.replace.id,
user_id: stash.user_id,
},
},
}));
return stashDoc;
});
console.log(stashDocs);
if (stashDocs.length > 0) {
await indexApi.bulk(stashDocs.map((doc) => JSON.stringify(doc)).join('\n'));
}
}, Promise.resolve());
}
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) => {
const flatActors = scene.actors.flatMap((actor) => actor.f2.match(/[\w']+/g)); // match word characters to filter out brackets etc.
const flatTags = scene.tags.filter((tag) => tag.f3 > 6).flatMap((tag) => (tag.f4 ? `${tag.f2} ${tag.f4}` : tag.f2).match(/[\w']+/g)); // only make top tags searchable to minimize cluttered results
const filteredTitle = scene.title && [...flatActors, ...flatTags].reduce((accTitle, tag) => accTitle.replace(new RegExp(tag.replace(/[^\w\s]+/g, ''), 'i'), ''), scene.title).trim().replace(/\s{2,}/, ' ');
return {
replace: {
index: 'movies',
id: scene.id,
doc: {
title: scene.title || undefined,
title_filtered: filteredTitle || undefined,
date: scene.date ? Math.round(scene.date.getTime() / 1000) : undefined,
created_at: Math.round(scene.created_at.getTime() / 1000),
effective_date: Math.round((scene.date || scene.created_at).getTime() / 1000),
// shoot_id: scene.shoot_id || undefined,
channel_id: scene.channel_id,
channel_slug: scene.channel_slug,
channel_name: scene.channel_name,
network_id: scene.network_id || undefined,
network_slug: scene.network_slug || undefined,
network_name: scene.network_name || undefined,
actor_ids: scene.actors.map((actor) => actor.f1),
actors: scene.actors.map((actor) => actor.f2).join(),
tag_ids: scene.tags.map((tag) => tag.f1),
tags: flatTags.join(' '),
meta: scene.date ? format(scene.date, 'y yy M MMM MMMM d') : undefined,
liked: scene.stashed || 0,
},
},
};
});
const data = await indexApi.bulk(docs.map((doc) => JSON.stringify(doc)).join('\n'));
await updateStashed(docs);
console.log('data', data);
knex.destroy();
}
init();

648028
src/tools/movies.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -46,17 +46,17 @@ export async function fetchUser(userId, options = {}) {
.groupBy('users.id', 'users_roles.role')
.first();
const stashes = await knex('stashes')
.where('user_id', user.id)
.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id');
if (!user) {
throw HttpError(`User '${userId}' not found`, 404);
}
if (options.raw) {
return user;
return { user, stashes };
}
const stashes = await knex('stashes')
.where('user_id', user.id)
.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id');
return curateUser(user, { stashes });
}

21
src/users/curate.js Normal file
View File

@ -0,0 +1,21 @@
export function curateUser(user, assets = {}) {
if (!user) {
return null;
}
const curatedStashes = assets.stashes?.filter(Boolean).map((stash) => curateStash(stash)) || [];
const curatedUser = {
id: user.id,
username: user.username,
email: user.email,
emailVerified: user.email_verified,
identityVerified: user.identity_verified,
avatar: `/media/avatars/${user.id}_${user.username}.png`,
createdAt: user.created_at,
stashes: curatedStashes,
primaryStash: curatedStashes.find((stash) => stash.primary),
};
return curatedUser;
}

4
src/utils/chunk.js Executable file
View File

@ -0,0 +1,4 @@
export default function chunk(array, chunkSize = 1000) {
return Array.from({ length: Math.ceil(array.length / chunkSize) })
.map((value, index) => array.slice(index * chunkSize, (index * chunkSize) + chunkSize));
}

View File

@ -1,16 +1,42 @@
/* eslint-disable no-param-reassign */
import IPCIDR from 'ip-cidr';
import { login, signup } from '../auth.js';
function getIp(req) {
const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.connection.remoteAddress; // See src/ws
const unmappedIp = ip?.includes('.')
? ip.slice(ip.lastIndexOf(':') + 1)
: ip;
// ensure IP is in expanded notation for consistency and matching
const expandedIp = unmappedIp.includes(':')
? new IPCIDR(`${ip}/128`) // IPv6
: new IPCIDR(`${ip}/32`); // IPv4
if (!expandedIp.addressStart?.addressMinusSuffix) {
throw new Error(`Could not determine user IP from ${ip}`);
}
return expandedIp.addressStart?.addressMinusSuffix || null;
}
export async function setUserApi(req, res, next) {
const ip = getIp(req);
req.userIp = ip;
if (req.session.user) {
req.user = req.session.user;
req.user.ip = ip;
}
next();
}
export async function loginApi(req, res) {
const user = await login(req.body);
const user = await login(req.body, req.userIp);
req.session.user = user;
res.send(user);
@ -27,7 +53,7 @@ export async function logoutApi(req, res) {
}
export async function signupApi(req, res) {
const user = await signup(req.body);
const user = await signup(req.body, req.userIp);
req.session.user = user;
res.send(user);

View File

@ -11,6 +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),
};
}

View File

@ -135,7 +135,14 @@ export default async function initServer() {
const pageContextInit = {
urlOriginal: req.originalUrl,
urlQuery: req.query, // vike's own query does not apply boolean parser
user: req.user,
user: req.user && {
id: req.user.id,
username: req.user.username,
email: req.user.email,
avatar: req.user.avatar,
stashes: req.user.stashes,
primaryStash: req.user.primaryStash,
},
env: {
maxAggregateSize: config.database.manticore.maxAggregateSize,
},

View File

@ -53,7 +53,7 @@ export async function unstashActorApi(req, res) {
}
export async function unstashSceneApi(req, res) {
const stashes = await unstashScene(req.params.sceneId, req.params.stashId, req.user);
const stashes = await unstashScene(Number(req.params.sceneId), Number(req.params.stashId), req.user);
res.send(stashes);
}