Reinitialized commit. Update and actors overview with some filters.
This commit is contained in:
151
src/actors.js
Normal file
151
src/actors.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import { differenceInYears } from 'date-fns';
|
||||
|
||||
import knex from './knex.js';
|
||||
import { searchApi } from './manticore.js';
|
||||
import { HttpError } from './errors.js';
|
||||
|
||||
export function curateActor(actor, context = {}) {
|
||||
return {
|
||||
id: actor.id,
|
||||
slug: actor.slug,
|
||||
name: actor.name,
|
||||
gender: actor.gender,
|
||||
age: actor.age,
|
||||
dateOfBirth: actor.date_of_birth,
|
||||
ageFromBirth: actor.date_of_birth && differenceInYears(Date.now(), actor.date_of_birth),
|
||||
ageThen: context.sceneDate && actor.date_of_birth && differenceInYears(context.sceneDate, actor.date_of_birth),
|
||||
birthCountry: actor.birth_country_alpha2 && {
|
||||
alpha2: actor.birth_country_alpha2,
|
||||
name: actor.birth_country_name,
|
||||
},
|
||||
residenceCountry: actor.residence_country_alpha2 && {
|
||||
alpha2: actor.residence_country_alpha2,
|
||||
name: actor.residence_country_name,
|
||||
},
|
||||
avatar: actor.avatar_id ? {
|
||||
id: actor.avatar_id,
|
||||
path: actor.avatar_path,
|
||||
thumbnail: actor.avatar_thumbnail,
|
||||
lazy: actor.avatar_lazy,
|
||||
isS3: actor.avatar_s3,
|
||||
} : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function sortActorsByGender(actors) {
|
||||
if (!actors) {
|
||||
return actors;
|
||||
}
|
||||
|
||||
const alphaActors = actors.sort((actorA, actorB) => actorA.name.localeCompare(actorB.name, 'en'));
|
||||
const genderActors = ['transsexual', 'female', 'male', undefined].flatMap((gender) => alphaActors.filter((actor) => actor.gender === gender));
|
||||
|
||||
return genderActors;
|
||||
}
|
||||
|
||||
export async function fetchActorsById(actorIds) {
|
||||
const [actors] = await Promise.all([
|
||||
knex('actors')
|
||||
.select(
|
||||
'actors.*',
|
||||
'avatars.id as avatar_id',
|
||||
'avatars.path as avatar_path',
|
||||
'avatars.thumbnail as avatar_thumbnail',
|
||||
'avatars.lazy as avatar_lazy',
|
||||
'avatars.width as avatar_width',
|
||||
'avatars.height as avatar_height',
|
||||
'avatars.is_s3 as avatar_s3',
|
||||
)
|
||||
.whereIn('actors.id', actorIds)
|
||||
.leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id')
|
||||
.groupBy('actors.id', 'avatars.id'),
|
||||
]);
|
||||
|
||||
return actorIds.map((actorId) => {
|
||||
const actor = actors.find((actorEntry) => actorEntry.id === actorId);
|
||||
|
||||
if (!actor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return curateActor(actor);
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
function curateOptions(options) {
|
||||
if (options?.limit > 100) {
|
||||
throw new HttpError('Limit must be <= 100', 400);
|
||||
}
|
||||
|
||||
return {
|
||||
page: options?.page || 1,
|
||||
limit: options?.limit || 30,
|
||||
requireAvatar: options?.requireAvatar || false,
|
||||
};
|
||||
}
|
||||
|
||||
function buildQuery(filters) {
|
||||
console.log(filters);
|
||||
|
||||
const query = {
|
||||
bool: {
|
||||
must: [],
|
||||
},
|
||||
};
|
||||
|
||||
if (filters.query) {
|
||||
query.bool.must.push({
|
||||
match: {
|
||||
name: filters.query,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
['age', 'height', 'weight'].forEach((attribute) => {
|
||||
if (filters[attribute]) {
|
||||
query.bool.must.push({
|
||||
range: {
|
||||
[attribute]: {
|
||||
gte: filters[attribute][0],
|
||||
lte: filters[attribute][1],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (filters.requireAvatar) {
|
||||
query.bool.must.push({
|
||||
equals: {
|
||||
has_avatar: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
export async function fetchActors(filters, rawOptions) {
|
||||
const options = curateOptions(rawOptions);
|
||||
const query = buildQuery(filters);
|
||||
|
||||
const result = await searchApi.search({
|
||||
index: 'actors',
|
||||
query,
|
||||
expressions: {
|
||||
age: 'if(date_of_birth, floor((now() - date_of_birth) / 31556952), 0)',
|
||||
},
|
||||
limit: options.limit,
|
||||
offset: (options.page - 1) * options.limit,
|
||||
sort: [{ slug: 'asc' }],
|
||||
});
|
||||
|
||||
const actorIds = result.hits.hits.map((hit) => Number(hit._id));
|
||||
const actors = await fetchActorsById(actorIds);
|
||||
|
||||
return {
|
||||
actors,
|
||||
total: result.hits.total,
|
||||
limit: options.limit,
|
||||
};
|
||||
}
|
||||
88
src/api.js
Normal file
88
src/api.js
Normal file
@@ -0,0 +1,88 @@
|
||||
const postHeaders = {
|
||||
mode: 'cors',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
};
|
||||
|
||||
function getQuery(data) {
|
||||
if (!data) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const curatedQuery = Object.fromEntries(Object.entries(data).map(([key, value]) => (value === undefined ? null : [key, value])).filter(Boolean));
|
||||
|
||||
return `?${encodeURI(decodeURIComponent(new URLSearchParams(curatedQuery).toString()))}`; // recode so commas aren't encoded
|
||||
}
|
||||
|
||||
export async function get(path, query = {}) {
|
||||
const res = await fetch(`/api${path}${getQuery(query)}`);
|
||||
const body = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
return body;
|
||||
}
|
||||
|
||||
throw new Error(body.message);
|
||||
}
|
||||
|
||||
export async function post(path, data, { query } = {}) {
|
||||
const res = await fetch(`/api${path}${getQuery(query)}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
...postHeaders,
|
||||
});
|
||||
|
||||
if (res.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
return body;
|
||||
}
|
||||
|
||||
throw new Error(body.statusMessage);
|
||||
}
|
||||
|
||||
export async function patch(path, data, { query } = {}) {
|
||||
const res = await fetch(`/api${path}${getQuery(query)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
...postHeaders,
|
||||
});
|
||||
|
||||
if (res.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
return body;
|
||||
}
|
||||
|
||||
throw new Error(body.message);
|
||||
}
|
||||
|
||||
export async function del(path, { data, query } = {}) {
|
||||
const res = await fetch(`/api${path}${getQuery(query)}`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(data),
|
||||
...postHeaders,
|
||||
});
|
||||
|
||||
if (res.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
return body;
|
||||
}
|
||||
|
||||
throw new Error(body.message);
|
||||
}
|
||||
16
src/errors.js
Executable file
16
src/errors.js
Executable file
@@ -0,0 +1,16 @@
|
||||
export class HttpError extends Error {
|
||||
constructor(message, httpCode, friendlyMessage, data) {
|
||||
super(message);
|
||||
|
||||
this.name = 'HttpError';
|
||||
this.httpCode = httpCode;
|
||||
|
||||
if (friendlyMessage) {
|
||||
this.friendlyMessage = friendlyMessage;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/format.js
Normal file
19
src/format.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export function formatDuration(duration, forceHours) {
|
||||
const hours = Math.floor(duration / 3600);
|
||||
const minutes = Math.floor((duration % 3600) / 60);
|
||||
const seconds = Math.floor(duration % 60);
|
||||
|
||||
const [formattedHours, formattedMinutes, formattedSeconds] = [hours, minutes, seconds].map((segment) => segment.toString().padStart(2, '0'));
|
||||
|
||||
if (duration >= 3600 || forceHours) {
|
||||
return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
|
||||
}
|
||||
|
||||
return `${formattedMinutes}:${formattedSeconds}`;
|
||||
}
|
||||
|
||||
export function formatDate(date, template) {
|
||||
return format(date, template);
|
||||
}
|
||||
9
src/get-title.js
Normal file
9
src/get-title.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import config from 'config';
|
||||
|
||||
export default function getTitle(location) {
|
||||
if (!location) {
|
||||
return config.title;
|
||||
}
|
||||
|
||||
return `${config.title} - ${location.slice(0, 1).toUpperCase()}${location.slice(1)}`;
|
||||
}
|
||||
11
src/knex.js
Executable file
11
src/knex.js
Executable file
@@ -0,0 +1,11 @@
|
||||
import config from 'config';
|
||||
import knex from 'knex';
|
||||
|
||||
export default knex({
|
||||
client: 'pg',
|
||||
connection: config.database.owner,
|
||||
pool: config.database.pool,
|
||||
// performance overhead, don't use asyncStackTraces in production
|
||||
asyncStackTraces: process.env.NODE_ENV === 'development',
|
||||
// debug: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
8
src/manticore.js
Normal file
8
src/manticore.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import config from 'config';
|
||||
import manticore from 'manticoresearch';
|
||||
|
||||
const mantiClient = new manticore.ApiClient();
|
||||
|
||||
mantiClient.basePath = `http://${config.database.manticore.host}:${config.database.manticore.httpPort}`;
|
||||
|
||||
export const searchApi = new manticore.SearchApi(mantiClient);
|
||||
18
src/navigate.js
Normal file
18
src/navigate.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export default function navigate(path, query, options = {}) {
|
||||
const curatedQuery = Object.fromEntries(Object.entries(query || {}).map(([key, value]) => (value === undefined ? null : [key, value])).filter(Boolean));
|
||||
|
||||
const queryString = new URLSearchParams({
|
||||
...(options.preserveQuery ? Object.fromEntries(new URL(window.location).searchParams.entries()) : {}),
|
||||
...curatedQuery,
|
||||
}).toString();
|
||||
|
||||
const url = queryString
|
||||
? `${path}?${encodeURI(decodeURIComponent(queryString))}` // URLSearchParams encodes commas, we don't want that
|
||||
: path;
|
||||
|
||||
if (options.redirect) {
|
||||
window.location.href = url;
|
||||
} else {
|
||||
history.pushState({}, '', url); // eslint-disable-line no-restricted-globals
|
||||
}
|
||||
}
|
||||
246
src/scenes.js
Normal file
246
src/scenes.js
Normal file
@@ -0,0 +1,246 @@
|
||||
import knex from './knex.js';
|
||||
import { searchApi } from './manticore.js';
|
||||
import { HttpError } from './errors.js';
|
||||
import { curateActor, sortActorsByGender } from './actors.js';
|
||||
|
||||
function curateMedia(media) {
|
||||
if (!media) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: media.id,
|
||||
path: media.path,
|
||||
thumbnail: media.thumbnail,
|
||||
lazy: media.lazy,
|
||||
isS3: media.is_s3,
|
||||
width: media.width,
|
||||
height: media.height,
|
||||
};
|
||||
}
|
||||
|
||||
function curateScene(rawScene, assets) {
|
||||
if (!rawScene) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(assets.channel);
|
||||
|
||||
return {
|
||||
id: rawScene.id,
|
||||
title: rawScene.title,
|
||||
slug: rawScene.slug,
|
||||
url: rawScene.url,
|
||||
date: rawScene.date,
|
||||
createdAt: rawScene.created_at,
|
||||
effectiveDate: rawScene.effective_date,
|
||||
description: rawScene.description,
|
||||
duration: rawScene.duration,
|
||||
channel: {
|
||||
id: assets.channel.id,
|
||||
slug: assets.channel.slug,
|
||||
name: assets.channel.name,
|
||||
type: assets.channel.type,
|
||||
isIndependent: assets.channel.independent,
|
||||
hasLogo: assets.channel.has_logo,
|
||||
},
|
||||
network: assets.channel.network_id ? {
|
||||
id: assets.channel.network_id,
|
||||
slug: assets.channel.network_slug,
|
||||
name: assets.channel.network_name,
|
||||
type: assets.channel.network_type,
|
||||
hasLogo: assets.channel.has_logo,
|
||||
} : null,
|
||||
actors: sortActorsByGender(assets.actors.map((actor) => curateActor(actor, { sceneDate: rawScene.effective_date }))),
|
||||
directors: assets.directors.map((director) => ({
|
||||
id: director.id,
|
||||
slug: director.slug,
|
||||
name: director.name,
|
||||
})),
|
||||
tags: assets.tags.map((tag) => ({
|
||||
id: tag.id,
|
||||
slug: tag.slug,
|
||||
name: tag.name,
|
||||
})),
|
||||
poster: curateMedia(assets.poster),
|
||||
photos: assets.photos.map((photo) => curateMedia(photo)),
|
||||
createdBatchId: rawScene.created_batch_id,
|
||||
updatedBatchId: rawScene.updated_batch_id,
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
const [scenes, channels, actors, directors, tags, posters, photos] = await Promise.all([
|
||||
knex('releases').whereIn('id', sceneIds),
|
||||
knex('releases')
|
||||
.select('channels.*', 'networks.id as network_id', 'networks.slug as network_slug', 'networks.name as network_name', 'networks.type as network_type')
|
||||
.whereIn('releases.id', sceneIds)
|
||||
.leftJoin('entities as channels', 'channels.id', 'releases.entity_id')
|
||||
.leftJoin('entities as networks', 'networks.id', 'channels.parent_id')
|
||||
.groupBy('channels.id', 'networks.id'),
|
||||
knex('releases_actors')
|
||||
.select(
|
||||
'actors.*',
|
||||
'releases_actors.release_id',
|
||||
'avatars.id as avatar_id',
|
||||
'avatars.path as avatar_path',
|
||||
'avatars.thumbnail as avatar_thumbnail',
|
||||
'avatars.lazy as avatar_lazy',
|
||||
'avatars.width as avatar_width',
|
||||
'avatars.height as avatar_height',
|
||||
'avatars.is_s3 as avatar_s3',
|
||||
'birth_countries.alpha2 as birth_country_alpha2',
|
||||
'birth_countries.name as birth_country_name',
|
||||
'residence_countries.alpha2 as residence_country_alpha2',
|
||||
'residence_countries.name as residence_country_name',
|
||||
)
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('actors', 'actors.id', 'releases_actors.actor_id')
|
||||
.leftJoin('media as avatars', 'avatars.id', 'actors.avatar_media_id')
|
||||
.leftJoin('countries as birth_countries', 'birth_countries.alpha2', 'actors.birth_country_alpha2')
|
||||
.leftJoin('countries as residence_countries', 'residence_countries.alpha2', 'actors.residence_country_alpha2'),
|
||||
knex('releases_directors')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
|
||||
knex('releases_tags')
|
||||
.select('id', 'slug', 'name', 'release_id')
|
||||
.whereNotNull('tags.id')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id'),
|
||||
knex('releases_posters')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('media', 'media.id', 'releases_posters.media_id'),
|
||||
knex('releases_photos')
|
||||
.whereIn('release_id', sceneIds)
|
||||
.leftJoin('media', 'media.id', 'releases_photos.media_id'),
|
||||
]);
|
||||
|
||||
return sceneIds.map((sceneId) => {
|
||||
const scene = scenes.find((sceneEntry) => sceneEntry.id === sceneId);
|
||||
|
||||
if (!scene) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sceneChannel = channels.find((entity) => entity.id === scene.entity_id);
|
||||
const sceneActors = actors.filter((actor) => actor.release_id === sceneId);
|
||||
const sceneDirectors = directors.filter((director) => director.release_id === sceneId);
|
||||
const sceneTags = tags.filter((tag) => tag.release_id === sceneId);
|
||||
const scenePoster = posters.find((poster) => poster.release_id === sceneId);
|
||||
const scenePhotos = photos.filter((photo) => photo.release_id === sceneId);
|
||||
|
||||
return curateScene(scene, {
|
||||
channel: sceneChannel,
|
||||
actors: sceneActors,
|
||||
directors: sceneDirectors,
|
||||
tags: sceneTags,
|
||||
poster: scenePoster,
|
||||
photos: scenePhotos,
|
||||
});
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
export async function fetchLatest(page, rawOptions) {
|
||||
const { limit } = curateOptions(rawOptions);
|
||||
|
||||
const result = await searchApi.search({
|
||||
index: 'scenes',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
range: {
|
||||
effective_date: {
|
||||
lte: Math.round(Date.now() / 1000),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
/*
|
||||
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));
|
||||
const scenes = await fetchScenesById(sceneIds);
|
||||
|
||||
return {
|
||||
scenes,
|
||||
total: result.hits.total,
|
||||
limit,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchUpcoming(page, rawOptions) {
|
||||
const { limit } = curateOptions(rawOptions);
|
||||
|
||||
const result = await searchApi.search({
|
||||
index: 'scenes',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
range: {
|
||||
effective_date: {
|
||||
gt: Math.round(Date.now() / 1000),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
limit,
|
||||
offset: (page - 1) * limit,
|
||||
sort: [{ 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,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
20
src/web/actors.js
Normal file
20
src/web/actors.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { fetchActors } from '../actors.js';
|
||||
|
||||
export async function fetchActorsApi(req, res) {
|
||||
const { actors, limit, total } = await fetchActors({
|
||||
query: req.query.q,
|
||||
requireAvatar: Object.hasOwn(req.query, 'avatar'),
|
||||
age: req.query.age?.split(',').map((age) => Number(age)),
|
||||
height: req.query.height?.split(',').map((height) => Number(height)),
|
||||
weight: req.query.weight?.split(',').map((weight) => Number(weight)),
|
||||
}, {
|
||||
page: Number(req.query.page) || 1,
|
||||
limit: Number(req.query.limit) || 50,
|
||||
});
|
||||
|
||||
res.send({
|
||||
actors,
|
||||
limit,
|
||||
total,
|
||||
});
|
||||
}
|
||||
8
src/web/root.js
Normal file
8
src/web/root.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// https://stackoverflow.com/questions/46745014/alternative-for-dirname-in-node-when-using-the-experimental-modules-flag/50052194#50052194
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = `${__dirname}/..`;
|
||||
|
||||
export default root;
|
||||
101
src/web/server.js
Normal file
101
src/web/server.js
Normal file
@@ -0,0 +1,101 @@
|
||||
// This file isn't processed by Vite, see https://github.com/vikejs/vike/issues/562
|
||||
// Consequently:
|
||||
// - When changing this file, you needed to manually restart your server for your changes to take effect.
|
||||
// - To use your environment variables defined in your .env files, you need to install dotenv, see https://vike.dev/env
|
||||
// - To use your path aliases defined in your vite.config.js, you need to tell Node.js about them, see https://vike.dev/path-aliases
|
||||
|
||||
// If you want Vite to process your server code then use one of these:
|
||||
// - vavite (https://github.com/cyco130/vavite)
|
||||
// - See vavite + Vike examples at https://github.com/cyco130/vavite/tree/main/examples
|
||||
// - vite-node (https://github.com/antfu/vite-node)
|
||||
// - HatTip (https://github.com/hattipjs/hattip)
|
||||
// - You can use Bati (https://batijs.github.io/) to scaffold a Vike + HatTip app. Note that Bati generates apps that use the V1 design (https://vike.dev/migration/v1-design) and Vike packages (https://vike.dev/vike-packages)
|
||||
|
||||
import config from 'config';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import compression from 'compression';
|
||||
import { renderPage } from 'vike/server'; // eslint-disable-line import/extensions
|
||||
|
||||
// import root from './root.js';
|
||||
|
||||
import { fetchActorsApi } from './actors.js'; // eslint-disable-line import/extensions
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
const router = Router();
|
||||
|
||||
app.use(compression());
|
||||
app.disable('x-powered-by');
|
||||
|
||||
router.use('/', express.static('public'));
|
||||
router.use('/', express.static('static'));
|
||||
router.use('/media', express.static(config.media.path));
|
||||
|
||||
// Vite integration
|
||||
if (isProduction) {
|
||||
// In production, we need to serve our static assets ourselves.
|
||||
// (In dev, Vite's middleware serves our static assets.)
|
||||
const sirv = (await import('sirv')).default;
|
||||
router.use(sirv('dist/client'));
|
||||
} else {
|
||||
// We instantiate Vite's development server and integrate its middleware to our server.
|
||||
// ⚠️ We instantiate it only in development. (It isn't needed in production and it
|
||||
// would unnecessarily bloat our production server.)
|
||||
const vite = await import('vite');
|
||||
const viteDevMiddleware = (
|
||||
await vite.createServer({
|
||||
// root,
|
||||
server: { middlewareMode: true },
|
||||
})
|
||||
).middlewares;
|
||||
|
||||
router.use(viteDevMiddleware);
|
||||
}
|
||||
|
||||
router.get('/api/actors', fetchActorsApi);
|
||||
|
||||
// ...
|
||||
// Other middlewares (e.g. some RPC middleware such as Telefunc)
|
||||
// ...
|
||||
|
||||
// Vike middleware. It should always be our last middleware (because it's a
|
||||
// catch-all middleware superseding any middleware placed after it).
|
||||
router.get('*', async (req, res, next) => {
|
||||
const pageContextInit = {
|
||||
urlOriginal: req.originalUrl,
|
||||
};
|
||||
|
||||
const pageContext = await renderPage(pageContextInit);
|
||||
const { httpResponse } = pageContext;
|
||||
|
||||
if (!httpResponse) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
body, statusCode, headers, earlyHints,
|
||||
} = httpResponse;
|
||||
|
||||
if (res.writeEarlyHints) {
|
||||
res.writeEarlyHints({ link: earlyHints.map((e) => e.earlyHintLink) });
|
||||
}
|
||||
|
||||
headers.forEach(([name, value]) => res.setHeader(name, value));
|
||||
res.status(statusCode);
|
||||
// For HTTP streams use httpResponse.pipe() instead, see https://vike.dev/stream
|
||||
res.send(body);
|
||||
});
|
||||
|
||||
app.use(router);
|
||||
|
||||
const port = process.env.PORT || config.web.port || 3000;
|
||||
app.listen(port);
|
||||
|
||||
console.log(`Server running at http://localhost:${port}`);
|
||||
}
|
||||
|
||||
startServer();
|
||||
Reference in New Issue
Block a user