Added basic actor page with scenes and co-star filtering.
This commit is contained in:
parent
ffcb77ab45
commit
e32a366fff
|
@ -1,60 +1,213 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<nav class="scopes">
|
<Filters v-if="showFilters">
|
||||||
<Link
|
<ul class="filter-actors selected nolist">
|
||||||
href="/updates/latest"
|
<li
|
||||||
class="scope nolink"
|
v-for="aggActor in selectedActors"
|
||||||
:active="scope === 'latest'"
|
:key="`filter-actor-${aggActor.id}`"
|
||||||
>Latest</Link>
|
class="filter-actor"
|
||||||
|
@click="updateFilter('actors', [...filters.actors, aggActor.id])"
|
||||||
|
>
|
||||||
|
{{ aggActor.name }}
|
||||||
|
|
||||||
<Link
|
<button
|
||||||
href="/updates/upcoming"
|
v-if="filters.actors.includes(aggActor.id)"
|
||||||
class="scope nolink"
|
class="filter-actor-remove"
|
||||||
:active="scope === 'upcoming'"
|
@click.stop="updateFilter('actors', filters.actors.filter((actorId) => actorId !== aggActor.id))"
|
||||||
>Upcoming</Link>
|
><Icon icon="cross2" /></button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<Link
|
<ul class="filter-actors available nolist">
|
||||||
href="/updates/new"
|
<li
|
||||||
class="scope nolink"
|
v-for="aggActor in actors"
|
||||||
:active="scope === 'new'"
|
:key="`filter-actor-${aggActor.id}`"
|
||||||
>New</Link>
|
class="filter-actor"
|
||||||
</nav>
|
@click="updateFilter('actors', [...filters.actors, aggActor.id])"
|
||||||
|
>{{ aggActor.name }}</li>
|
||||||
|
</ul>
|
||||||
|
</Filters>
|
||||||
|
|
||||||
<ul class="scenes nolist">
|
<div class="scenes-container">
|
||||||
<li
|
<div class="scenes-header">
|
||||||
v-for="scene in scenes"
|
<div class="meta">{{ total }} results</div>
|
||||||
:key="scene.id"
|
</div>
|
||||||
>
|
|
||||||
<Scene :scene="scene" />
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<Pagination />
|
<nav class="scopes">
|
||||||
|
<Link
|
||||||
|
:href="getPath('latest')"
|
||||||
|
class="scope nolink"
|
||||||
|
:active="scope === 'latest'"
|
||||||
|
>Latest</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
:href="getPath('upcoming')"
|
||||||
|
class="scope nolink"
|
||||||
|
:active="scope === 'upcoming'"
|
||||||
|
>Upcoming</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
:href="getPath('new')"
|
||||||
|
class="scope nolink"
|
||||||
|
:active="scope === 'new'"
|
||||||
|
>New</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<ul class="scenes nolist">
|
||||||
|
<li
|
||||||
|
v-for="scene in scenes"
|
||||||
|
:key="scene.id"
|
||||||
|
>
|
||||||
|
<Scene :scene="scene" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Pagination />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { inject } from 'vue';
|
import { ref, computed, inject } from 'vue';
|
||||||
|
import { parse } from 'path-to-regexp';
|
||||||
|
|
||||||
|
import navigate from '#/src/navigate.js';
|
||||||
|
import { get } from '#/src/api.js';
|
||||||
|
import events from '#/src/events.js';
|
||||||
|
|
||||||
|
import Filters from '#/components/filters/filters.vue';
|
||||||
import Scene from './tile.vue';
|
import Scene from './tile.vue';
|
||||||
import Pagination from '../pagination/pagination.vue';
|
import Pagination from '../pagination/pagination.vue';
|
||||||
|
|
||||||
const { routeParams } = inject('pageContext');
|
|
||||||
const { scope } = routeParams;
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
scenes: {
|
showFilters: {
|
||||||
type: Array,
|
type: Boolean,
|
||||||
default: () => [],
|
default: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { pageProps, routeParams, urlParsed } = inject('pageContext');
|
||||||
|
const { scope } = routeParams;
|
||||||
|
|
||||||
|
console.log(scope);
|
||||||
|
|
||||||
|
const {
|
||||||
|
actor,
|
||||||
|
} = pageProps;
|
||||||
|
|
||||||
|
const scenes = ref(pageProps.scenes);
|
||||||
|
const actors = ref(pageProps.actors);
|
||||||
|
|
||||||
|
const currentPage = ref(Number(routeParams.page));
|
||||||
|
const total = ref(Number(pageProps.total));
|
||||||
|
|
||||||
|
const filters = ref({
|
||||||
|
actors: urlParsed.search.actors?.split(',').filter(Boolean).map((actorId) => Number(actorId)) || [],
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
const sortedActors = computed(() => Array.from(actors.value).sort((actorA, actorB) => {
|
||||||
|
if (filters.value.actors.includes(actorA.id) && !filters.value.actors.includes(actorB.id)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.actors.includes(actorB.id) && !filters.value.actors.includes(actorA.id)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}));
|
||||||
|
*/
|
||||||
|
|
||||||
|
const selectedActors = computed(() => actors.value.filter((filterActor) => filters.value.actors.includes(filterActor.id)));
|
||||||
|
|
||||||
|
console.log(filters.value.actors);
|
||||||
|
|
||||||
|
function getPath(targetScope, preserveQuery) {
|
||||||
|
const path = parse(routeParams.path)
|
||||||
|
.map((segment) => {
|
||||||
|
if (segment.name === 'scope') {
|
||||||
|
return targetScope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.name === 'page') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return routeParams[segment.name] || segment;
|
||||||
|
})
|
||||||
|
.join('/');
|
||||||
|
|
||||||
|
if (preserveQuery && urlParsed.searchOriginal) {
|
||||||
|
return `${path}${urlParsed.searchOriginal}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function search(resetPage = true) {
|
||||||
|
if (resetPage) {
|
||||||
|
currentPage.value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {};
|
||||||
|
|
||||||
|
console.log(query);
|
||||||
|
|
||||||
|
const res = await get('/scenes', {
|
||||||
|
...query,
|
||||||
|
actors: [actor?.id, filters.value.actors].filter(Boolean).join(','), // if we're on an actor page, actor ID needs to be included
|
||||||
|
scope,
|
||||||
|
page: currentPage.value, // client uses param rather than query pagination
|
||||||
|
});
|
||||||
|
|
||||||
|
scenes.value = res.scenes;
|
||||||
|
actors.value = res.actors;
|
||||||
|
total.value = res.total;
|
||||||
|
|
||||||
|
console.log(scenes.value);
|
||||||
|
|
||||||
|
events.emit('scrollUp');
|
||||||
|
|
||||||
|
navigate(getPath(scope, false), {
|
||||||
|
...query,
|
||||||
|
actors: filters.value.actors.join(',') || undefined, // don't include actor page ID in query, already a parameter
|
||||||
|
}, { redirect: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilter(prop, value, reload = true) {
|
||||||
|
filters.value[prop] = value;
|
||||||
|
|
||||||
|
if (reload) {
|
||||||
|
search();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page {
|
.page {
|
||||||
|
display: flex;
|
||||||
background: var(--background-base-10);
|
background: var(--background-base-10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scenes-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: .5rem 0 .25rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenes-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.scenes {
|
.scenes {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
|
||||||
|
@ -78,4 +231,39 @@ defineProps({
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-actors {
|
||||||
|
max-height: 15rem;
|
||||||
|
|
||||||
|
&.available {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actor {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-actor-remove {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: var(--error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -141,6 +141,7 @@ defineProps({
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
margin-right: .5rem;
|
margin-right: .5rem;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"": {
|
"": {
|
||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@brillout/json-serializer": "^0.5.8",
|
||||||
"@floating-ui/dom": "^1.5.3",
|
"@floating-ui/dom": "^1.5.3",
|
||||||
"@floating-ui/vue": "^1.0.2",
|
"@floating-ui/vue": "^1.0.2",
|
||||||
"@vitejs/plugin-vue": "^4.5.2",
|
"@vitejs/plugin-vue": "^4.5.2",
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"server:prod": "cross-env NODE_ENV=production node ./src/web/server"
|
"server:prod": "cross-env NODE_ENV=production node ./src/web/server"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@brillout/json-serializer": "^0.5.8",
|
||||||
"@floating-ui/dom": "^1.5.3",
|
"@floating-ui/dom": "^1.5.3",
|
||||||
"@floating-ui/vue": "^1.0.2",
|
"@floating-ui/vue": "^1.0.2",
|
||||||
"@vitejs/plugin-vue": "^4.5.2",
|
"@vitejs/plugin-vue": "^4.5.2",
|
||||||
|
|
|
@ -1,11 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<h2>Actor</h2>
|
<div class="bio">
|
||||||
|
<img
|
||||||
|
v-if="actor.avatar"
|
||||||
|
:src="actor.avatar.isS3 ? `https://cdndev.traxxx.me/${actor.avatar.thumbnail}` : `/media/${actor.avatar.thumbnail}`"
|
||||||
|
:style="{ 'background-image': actor.avatar.isS3 ? `url(https://cdndev.traxxx.me/${actor.avatar.lazy})` : `url(/media/${actor.avatar.lazy})` }"
|
||||||
|
class="avatar"
|
||||||
|
>
|
||||||
|
|
||||||
|
<h2 class="name">{{ actor.name }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Scenes />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { inject } from 'vue';
|
import { inject } from 'vue';
|
||||||
|
|
||||||
const { routeParams } = inject('pageContext');
|
import Scenes from '#/components/scenes/scenes.vue';
|
||||||
|
|
||||||
|
const pageContext = inject('pageContext');
|
||||||
|
const { pageProps } = pageContext;
|
||||||
|
|
||||||
|
const { actor } = pageProps;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bio {
|
||||||
|
display: flex;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--grey-dark-40);
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
height: 10rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { fetchActorsById } from '#/src/actors.js';
|
||||||
|
import { fetchScenes } from '#/src/scenes.js';
|
||||||
|
import { curateScenesQuery } from '#/src/web/scenes.js';
|
||||||
|
|
||||||
|
export async function onBeforeRender(pageContext) {
|
||||||
|
const [actor] = await fetchActorsById([Number(pageContext.routeParams.actorId)]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
scenes,
|
||||||
|
actors,
|
||||||
|
total,
|
||||||
|
limit,
|
||||||
|
} = await fetchScenes(curateScenesQuery({
|
||||||
|
...pageContext.urlQuery,
|
||||||
|
scope: pageContext.routeParams.scope || 'latest',
|
||||||
|
actorId: Number(pageContext.routeParams.actorId),
|
||||||
|
}), {
|
||||||
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
|
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||||
|
aggregate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageContext: {
|
||||||
|
title: actor.name,
|
||||||
|
pageProps: {
|
||||||
|
actor,
|
||||||
|
actors,
|
||||||
|
scenes,
|
||||||
|
total,
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -1 +1,23 @@
|
||||||
export default '/actor/@actorId/*';
|
import { match } from 'path-to-regexp';
|
||||||
|
// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions
|
||||||
|
|
||||||
|
const path = '/actor/:actorId/:actorSlug?/:scope?/:page?';
|
||||||
|
const urlMatch = match(path, { decode: decodeURIComponent });
|
||||||
|
|
||||||
|
export default (pageContext) => {
|
||||||
|
const matched = urlMatch(pageContext.urlPathname);
|
||||||
|
|
||||||
|
if (matched) {
|
||||||
|
return {
|
||||||
|
routeParams: {
|
||||||
|
actorId: matched.params.actorId,
|
||||||
|
actorSlug: matched.params.actorSlug,
|
||||||
|
scope: matched.params.scope || 'latest',
|
||||||
|
page: matched.params.page || '1',
|
||||||
|
path,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<div>
|
<div>
|
||||||
<Scenes
|
<Scenes
|
||||||
:scenes="scenes"
|
:scenes="scenes"
|
||||||
|
:show-filters="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -9,7 +10,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { inject } from 'vue';
|
import { inject } from 'vue';
|
||||||
|
|
||||||
import Scenes from '../../components/scenes/scenes.vue';
|
import Scenes from '#/components/scenes/scenes.vue';
|
||||||
|
|
||||||
const { pageProps } = inject('pageContext');
|
const { pageProps } = inject('pageContext');
|
||||||
const { scenes } = pageProps;
|
const { scenes } = pageProps;
|
||||||
|
|
|
@ -1,21 +1,13 @@
|
||||||
import { fetchLatest, fetchUpcoming, fetchNew } from '#/src/scenes.js';
|
import { fetchScenes } from '#/src/scenes.js';
|
||||||
|
|
||||||
async function fetchScenes(scope, page, limit) {
|
|
||||||
if (scope === 'new') {
|
|
||||||
return fetchNew(page, { limit });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scope === 'upcoming') {
|
|
||||||
return fetchUpcoming(page, { limit });
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetchLatest(page, { limit });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function onBeforeRender(pageContext) {
|
export async function onBeforeRender(pageContext) {
|
||||||
const { scenes, limit, total } = await fetchScenes(pageContext.routeParams.scope, Number(pageContext.routeParams.page) || 1, Number(pageContext.urlParsed.search.limit) || 30);
|
const { scenes, limit, total } = await fetchScenes({
|
||||||
|
scope: pageContext.routeParams.scope || 'latest',
|
||||||
// console.log(scenes);
|
}, {
|
||||||
|
page: Number(pageContext.routeParams.page) || 1,
|
||||||
|
limit: Number(pageContext.urlParsed.search.limit) || 30,
|
||||||
|
aggregate: false,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageContext: {
|
pageContext: {
|
||||||
|
|
|
@ -45,13 +45,22 @@ export function sortActorsByGender(actors) {
|
||||||
return genderActors;
|
return genderActors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchActorsById(actorIds) {
|
export async function fetchActorsById(actorIds, options = {}) {
|
||||||
const [actors] = await Promise.all([
|
const [actors] = await Promise.all([
|
||||||
knex('actors_meta')
|
knex('actors_meta')
|
||||||
.select('actors_meta.*')
|
.select('actors_meta.*')
|
||||||
.whereIn('actors_meta.id', actorIds),
|
.whereIn('actors_meta.id', actorIds)
|
||||||
|
.modify((builder) => {
|
||||||
|
if (options.order) {
|
||||||
|
builder.orderBy(...options.order);
|
||||||
|
}
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (options.order) {
|
||||||
|
return actors.map((actorEntry) => curateActor(actorEntry));
|
||||||
|
}
|
||||||
|
|
||||||
const curatedActors = actorIds.map((actorId) => {
|
const curatedActors = actorIds.map((actorId) => {
|
||||||
const actor = actors.find((actorEntry) => actorEntry.id === actorId);
|
const actor = actors.find((actorEntry) => actorEntry.id === actorId);
|
||||||
|
|
||||||
|
|
10
src/api.js
10
src/api.js
|
@ -1,3 +1,5 @@
|
||||||
|
import { parse } from '@brillout/json-serializer/parse';
|
||||||
|
|
||||||
const postHeaders = {
|
const postHeaders = {
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
|
@ -18,7 +20,7 @@ function getQuery(data) {
|
||||||
|
|
||||||
export async function get(path, query = {}) {
|
export async function get(path, query = {}) {
|
||||||
const res = await fetch(`/api${path}${getQuery(query)}`);
|
const res = await fetch(`/api${path}${getQuery(query)}`);
|
||||||
const body = await res.json();
|
const body = parse(await res.text());
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return body;
|
return body;
|
||||||
|
@ -38,7 +40,7 @@ export async function post(path, data, { query } = {}) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await res.json();
|
const body = parse(await res.text());
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return body;
|
return body;
|
||||||
|
@ -58,7 +60,7 @@ export async function patch(path, data, { query } = {}) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await res.json();
|
const body = parse(await res.text());
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return body;
|
return body;
|
||||||
|
@ -78,7 +80,7 @@ export async function del(path, { data, query } = {}) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await res.json();
|
const body = parse(await res.text());
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
return body;
|
return body;
|
||||||
|
|
178
src/scenes.js
178
src/scenes.js
|
@ -1,7 +1,7 @@
|
||||||
import knex from './knex.js';
|
import knex from './knex.js';
|
||||||
import { searchApi } from './manticore.js';
|
import { searchApi } from './manticore.js';
|
||||||
import { HttpError } from './errors.js';
|
import { HttpError } from './errors.js';
|
||||||
import { curateActor, sortActorsByGender } from './actors.js';
|
import { fetchActorsById, curateActor, sortActorsByGender } from './actors.js';
|
||||||
|
|
||||||
function curateMedia(media) {
|
function curateMedia(media) {
|
||||||
if (!media) {
|
if (!media) {
|
||||||
|
@ -67,16 +67,6 @@ function curateScene(rawScene, assets) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function curateOptions(options) {
|
|
||||||
if (options?.limit > 100) {
|
|
||||||
throw new HttpError('Limit must be <= 100', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
limit: options.limit || 30,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchScenesById(sceneIds) {
|
export async function fetchScenesById(sceneIds) {
|
||||||
const [scenes, channels, actors, directors, tags, posters, photos] = await Promise.all([
|
const [scenes, channels, actors, directors, tags, posters, photos] = await Promise.all([
|
||||||
knex('releases').whereIn('id', sceneIds),
|
knex('releases').whereIn('id', sceneIds),
|
||||||
|
@ -114,7 +104,8 @@ export async function fetchScenesById(sceneIds) {
|
||||||
.select('id', 'slug', 'name', 'release_id')
|
.select('id', 'slug', 'name', 'release_id')
|
||||||
.whereNotNull('tags.id')
|
.whereNotNull('tags.id')
|
||||||
.whereIn('release_id', sceneIds)
|
.whereIn('release_id', sceneIds)
|
||||||
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id'),
|
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
|
||||||
|
.orderBy('priority', 'desc'),
|
||||||
knex('releases_posters')
|
knex('releases_posters')
|
||||||
.whereIn('release_id', sceneIds)
|
.whereIn('release_id', sceneIds)
|
||||||
.leftJoin('media', 'media.id', 'releases_posters.media_id'),
|
.leftJoin('media', 'media.id', 'releases_posters.media_id'),
|
||||||
|
@ -148,97 +139,116 @@ export async function fetchScenesById(sceneIds) {
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchLatest(page, rawOptions) {
|
function curateOptions(options) {
|
||||||
const { limit } = curateOptions(rawOptions);
|
if (options?.limit > 100) {
|
||||||
|
throw new HttpError('Limit must be <= 100', 400);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await searchApi.search({
|
return {
|
||||||
index: 'scenes',
|
limit: options?.limit || 30,
|
||||||
query: {
|
page: Number(options?.page) || 1,
|
||||||
bool: {
|
aggregate: options.aggregate ?? true,
|
||||||
must: [
|
};
|
||||||
{
|
}
|
||||||
range: {
|
|
||||||
effective_date: {
|
function buildQuery(filters = {}) {
|
||||||
lte: Math.round(Date.now() / 1000),
|
const query = {
|
||||||
},
|
bool: {
|
||||||
},
|
must: [],
|
||||||
},
|
},
|
||||||
],
|
};
|
||||||
/*
|
|
||||||
must_not: [
|
let sort = [{ effective_date: 'desc' }];
|
||||||
{
|
|
||||||
in: {
|
if (!filters.scope || filters.scope === 'latest') {
|
||||||
'any(tag_ids)': [101, 180, 32],
|
query.bool.must.push({
|
||||||
},
|
range: {
|
||||||
},
|
effective_date: {
|
||||||
],
|
lte: Math.round(Date.now() / 1000),
|
||||||
*/
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.scope === 'upcoming') {
|
||||||
|
query.bool.must.push({
|
||||||
|
range: {
|
||||||
|
effective_date: {
|
||||||
|
gt: Math.round(Date.now() / 1000),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
sort = [{ effective_date: 'asc' }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.scope === 'new') {
|
||||||
|
sort = [{ created_at: 'desc' }, { effective_date: 'asc' }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.actorIds) {
|
||||||
|
console.log('actor ids', filters.actorIds);
|
||||||
|
|
||||||
|
filters.actorIds.forEach((actorId) => {
|
||||||
|
query.bool.must.push({
|
||||||
|
equals: {
|
||||||
|
'any(actor_ids)': actorId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tag filter
|
||||||
|
must_not: [
|
||||||
|
{
|
||||||
|
in: {
|
||||||
|
'any(tag_ids)': [101, 180, 32],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
limit,
|
],
|
||||||
offset: (page - 1) * limit,
|
*/
|
||||||
sort: [{ effective_date: 'desc' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const sceneIds = result.hits.hits.map((hit) => Number(hit._id));
|
console.log(query.bool.must);
|
||||||
const scenes = await fetchScenesById(sceneIds);
|
|
||||||
|
|
||||||
return {
|
return { query, sort };
|
||||||
scenes,
|
|
||||||
total: result.hits.total,
|
|
||||||
limit,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchUpcoming(page, rawOptions) {
|
export async function fetchScenes(filters, rawOptions) {
|
||||||
const { limit } = curateOptions(rawOptions);
|
const options = curateOptions(rawOptions);
|
||||||
|
const { query, sort } = buildQuery(filters);
|
||||||
|
|
||||||
|
console.log(filters);
|
||||||
|
|
||||||
const result = await searchApi.search({
|
const result = await searchApi.search({
|
||||||
index: 'scenes',
|
index: 'scenes',
|
||||||
query: {
|
query,
|
||||||
bool: {
|
limit: options.limit,
|
||||||
must: [
|
offset: (options.page - 1) * options.limit,
|
||||||
{
|
sort,
|
||||||
range: {
|
...(options.aggregate && {
|
||||||
effective_date: {
|
aggs: {
|
||||||
gt: Math.round(Date.now() / 1000),
|
actorIds: {
|
||||||
},
|
terms: {
|
||||||
},
|
field: 'actor_ids',
|
||||||
|
size: 5000,
|
||||||
},
|
},
|
||||||
],
|
// sort: [{ doc_count: { order: 'asc' } }],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
limit,
|
|
||||||
offset: (page - 1) * limit,
|
|
||||||
sort: [{ effective_date: 'asc' }],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [actors] = await Promise.all([
|
||||||
|
options.aggregate ? fetchActorsById(result.aggregations.actorIds.buckets.map((bucket) => bucket.key), { order: ['name', 'asc'] }) : [],
|
||||||
|
]);
|
||||||
|
|
||||||
const sceneIds = result.hits.hits.map((hit) => Number(hit._id));
|
const sceneIds = result.hits.hits.map((hit) => Number(hit._id));
|
||||||
const scenes = await fetchScenesById(sceneIds);
|
const scenes = await fetchScenesById(sceneIds);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scenes,
|
scenes,
|
||||||
|
actors,
|
||||||
total: result.hits.total,
|
total: result.hits.total,
|
||||||
limit,
|
limit: options.limit,
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchNew(page, rawOptions) {
|
|
||||||
const { limit } = curateOptions(rawOptions);
|
|
||||||
|
|
||||||
const result = await searchApi.search({
|
|
||||||
index: 'scenes',
|
|
||||||
limit,
|
|
||||||
offset: (page - 1) * limit,
|
|
||||||
sort: [{ created_at: 'desc' }, { effective_date: 'asc' }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const sceneIds = result.hits.hits.map((hit) => Number(hit._id));
|
|
||||||
const scenes = await fetchScenesById(sceneIds);
|
|
||||||
|
|
||||||
return {
|
|
||||||
scenes,
|
|
||||||
total: result.hits.total,
|
|
||||||
limit,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { stringify } from '@brillout/json-serializer/stringify';
|
||||||
|
|
||||||
|
import { fetchScenes } from '../scenes.js';
|
||||||
|
|
||||||
|
export function curateScenesQuery(query) {
|
||||||
|
return {
|
||||||
|
scope: query.scope || 'latest',
|
||||||
|
actorIds: [query.actorId, ...(query.actors?.split(',') || [])].filter(Boolean).map((actorId) => Number(actorId)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchScenesApi(req, res) {
|
||||||
|
const {
|
||||||
|
scenes,
|
||||||
|
actors,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
} = await fetchScenes(curateScenesQuery(req.query), {
|
||||||
|
page: Number(req.query.page) || 1,
|
||||||
|
limit: Number(req.query.limit) || 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send(stringify({
|
||||||
|
scenes,
|
||||||
|
actors,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
}));
|
||||||
|
}
|
|
@ -20,7 +20,8 @@ import { renderPage } from 'vike/server'; // eslint-disable-line import/extensio
|
||||||
|
|
||||||
// import root from './root.js';
|
// import root from './root.js';
|
||||||
|
|
||||||
import { fetchActorsApi } from './actors.js'; // eslint-disable-line import/extensions
|
import { fetchScenesApi } from './scenes.js';
|
||||||
|
import { fetchActorsApi } from './actors.js';
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
@ -60,6 +61,8 @@ async function startServer() {
|
||||||
|
|
||||||
router.get('/api/actors', fetchActorsApi);
|
router.get('/api/actors', fetchActorsApi);
|
||||||
|
|
||||||
|
router.get('/api/scenes', fetchScenesApi);
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
// Other middlewares (e.g. some RPC middleware such as Telefunc)
|
// Other middlewares (e.g. some RPC middleware such as Telefunc)
|
||||||
// ...
|
// ...
|
||||||
|
@ -75,6 +78,10 @@ async function startServer() {
|
||||||
const pageContext = await renderPage(pageContextInit);
|
const pageContext = await renderPage(pageContextInit);
|
||||||
const { httpResponse } = pageContext;
|
const { httpResponse } = pageContext;
|
||||||
|
|
||||||
|
if (pageContext.errorWhileRendering) {
|
||||||
|
console.error(pageContext.errorWhileRendering);
|
||||||
|
}
|
||||||
|
|
||||||
if (!httpResponse) {
|
if (!httpResponse) {
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
|
|
Loading…
Reference in New Issue