diff --git a/assets/img/icons/clapboard.svg b/assets/img/icons/clapboard.svg new file mode 100644 index 0000000..3d618fd --- /dev/null +++ b/assets/img/icons/clapboard.svg @@ -0,0 +1,6 @@ + + +clapboard + + + diff --git a/assets/img/icons/movie.svg b/assets/img/icons/movie.svg new file mode 100644 index 0000000..848f19a --- /dev/null +++ b/assets/img/icons/movie.svg @@ -0,0 +1,5 @@ + + +movie + + diff --git a/assets/img/icons/play2.svg b/assets/img/icons/play2.svg new file mode 100644 index 0000000..05282e0 --- /dev/null +++ b/assets/img/icons/play2.svg @@ -0,0 +1,5 @@ + + +play2 + + diff --git a/assets/img/icons/video-camera.svg b/assets/img/icons/video-camera.svg new file mode 100644 index 0000000..b73f2ea --- /dev/null +++ b/assets/img/icons/video-camera.svg @@ -0,0 +1,5 @@ + + +video-camera + + diff --git a/components/filters/channels.vue b/components/filters/channels.vue index e4a368b..b24954b 100644 --- a/components/filters/channels.vue +++ b/components/filters/channels.vue @@ -118,8 +118,8 @@ const entities = computed(() => { || (channel.parent && searchRegexp.value.test(channel.parent.name)) || (channel.parent && searchRegexp.value.test(channel.parent.slug)))); - return Object.values(filteredChannels.reduce((acc, channel) => { - if (!channel.parent || channel.isIndependent) { + const networks = Object.values(filteredChannels.reduce((acc, channel) => { + if (!acc[channel.id] && (channel.type === 'network' || !channel.parent || channel.isIndependent)) { // network may have already been created by a child acc[channel.id] = { ...channel, children: [], @@ -128,14 +128,16 @@ const entities = computed(() => { return acc; } - if (!acc[channel.parent.id]) { + if (!acc[channel.parent.id] && channel.type === 'channel') { acc[channel.parent.id] = { ...channel.parent, children: [], }; } - acc[channel.parent.id].children.push(channel); + if (channel.type === 'channel') { + acc[channel.parent.id].children.push(channel); + } return acc; }, {})) @@ -146,6 +148,8 @@ const entities = computed(() => { })) .sort(sort) .flatMap((network) => [network, ...(network.children || [])]); + + return networks; }); diff --git a/components/scenes/tile.vue b/components/scenes/tile.vue index 67d1743..f161716 100644 --- a/components/scenes/tile.vue +++ b/components/scenes/tile.vue @@ -97,6 +97,10 @@ defineProps({ background: var(--background-base); border-radius: .25rem; box-shadow: 0 0 3px var(--shadow-weak-30); + + &:hover { + box-shadow: 0 0 3px var(--shadow-weak-20); + } } .poster { diff --git a/pages/entities/@entitySlug/+Page.vue b/pages/entities/@entitySlug/+Page.vue index a5406fa..737b5a8 100644 --- a/pages/entities/@entitySlug/+Page.vue +++ b/pages/entities/@entitySlug/+Page.vue @@ -139,7 +139,9 @@ const scrollable = computed(() => children.value?.scrollWidth > children.value?. .logo { height: 2.5rem; + max-width: 15rem; padding: .75rem 1rem; + object-fit: contain; } .link-parent { diff --git a/pages/movies/+Page.vue b/pages/movies/+Page.vue new file mode 100644 index 0000000..63a54c8 --- /dev/null +++ b/pages/movies/+Page.vue @@ -0,0 +1,404 @@ + + + + + diff --git a/pages/movies/+onBeforeRender.js b/pages/movies/+onBeforeRender.js new file mode 100644 index 0000000..ff33ed6 --- /dev/null +++ b/pages/movies/+onBeforeRender.js @@ -0,0 +1,36 @@ +import { fetchMovies } from '#/src/movies.js'; +import { curateMoviesQuery } from '#/src/web/movies.js'; + +export async function onBeforeRender(pageContext) { + const movieResults = await fetchMovies(await curateMoviesQuery({ + ...pageContext.urlQuery, + scope: pageContext.routeParams.scope || 'latest', + }), { + page: Number(pageContext.routeParams.page) || 1, + limit: Number(pageContext.urlParsed.search.limit) || 50, + aggregate: true, + }); + + const { + movies, + aggActors, + aggTags, + aggChannels, + total, + limit, + } = movieResults; + + return { + pageContext: { + title: 'Movies', + pageProps: { + movies, + aggActors, + aggTags, + aggChannels, + limit, + total, + }, + }, + }; +} diff --git a/pages/movies/+route.js b/pages/movies/+route.js new file mode 100644 index 0000000..078228e --- /dev/null +++ b/pages/movies/+route.js @@ -0,0 +1,21 @@ +import { match } from 'path-to-regexp'; +// import { resolveRoute } from 'vike/routing'; // eslint-disable-line import/extensions + +const path = '/movies/:scope?/:page?'; +const urlMatch = match(path, { decode: decodeURIComponent }); + +export default (pageContext) => { + const matched = urlMatch(pageContext.urlPathname); + + if (matched) { + return { + routeParams: { + scope: matched.params.scope || 'latest', + page: matched.params.page || '1', + path, + }, + }; + } + + return false; +}; diff --git a/pages/tags/+onBeforeRender.js b/pages/tags/+onBeforeRender.js index 2bc069b..fabbc9d 100644 --- a/pages/tags/+onBeforeRender.js +++ b/pages/tags/+onBeforeRender.js @@ -4,10 +4,10 @@ const tagSlugs = { popular: [ 'anal', 'lesbian', - 'interracial', + // 'interracial', 'mff', 'mfm', - 'teen', + // 'teen', 'milf', 'blowjob', 'gay', @@ -38,21 +38,13 @@ const tagSlugs = { ], oral: [ 'blowjob', - 'pussy-eating', - 'ass-eating', 'deepthroat', 'facefucking', + 'pussy-eating', + 'ass-eating', '69', 'atm', ], - manual: [ - 'handjob', - 'fingering', - 'anal-fingering', - 'titty-fucking', - 'fisting', - 'anal-fisting', - ], group: [ 'mfm', 'mff', @@ -62,14 +54,14 @@ const tagSlugs = { ], cumshot: [ 'facial', + 'bukkake', 'creampie', + 'anal-creampie', + 'oral-creampie', 'cum-in-mouth', 'cum-on-boobs', 'cum-on-butt', 'cum-on-pussy', - 'anal-creampie', - 'oral-creampie', - 'bukkake', 'fake-cum', ], roleplay: [ @@ -78,7 +70,6 @@ const tagSlugs = { 'schoolgirl', 'nurse', 'maid', - 'nun', ], extreme: [ 'dp', @@ -97,6 +88,14 @@ const tagSlugs = { 'latex', 'blindfold', ], + manual: [ + 'handjob', + 'fingering', + 'anal-fingering', + 'titty-fucking', + 'fisting', + 'anal-fisting', + ], toys: [ 'toys', 'toy-anal', diff --git a/public/img/icons/clapboard-play.svg b/public/img/icons/clapboard-play.svg new file mode 100755 index 0000000..3d3ea49 --- /dev/null +++ b/public/img/icons/clapboard-play.svg @@ -0,0 +1,5 @@ + + +clapboard-play + + diff --git a/public/img/icons/clapboard.svg b/public/img/icons/clapboard.svg new file mode 100644 index 0000000..3d618fd --- /dev/null +++ b/public/img/icons/clapboard.svg @@ -0,0 +1,6 @@ + + +clapboard + + + diff --git a/public/img/icons/film2.svg b/public/img/icons/film2.svg new file mode 100755 index 0000000..6ca1671 --- /dev/null +++ b/public/img/icons/film2.svg @@ -0,0 +1,5 @@ + + +film2 + + diff --git a/public/img/icons/film3.svg b/public/img/icons/film3.svg new file mode 100755 index 0000000..88afaa8 --- /dev/null +++ b/public/img/icons/film3.svg @@ -0,0 +1,5 @@ + + +film3 + + diff --git a/public/img/icons/movie.svg b/public/img/icons/movie.svg new file mode 100644 index 0000000..848f19a --- /dev/null +++ b/public/img/icons/movie.svg @@ -0,0 +1,5 @@ + + +movie + + diff --git a/src/actors.js b/src/actors.js index c9c9bd1..b8ba4df 100644 --- a/src/actors.js +++ b/src/actors.js @@ -74,17 +74,19 @@ export function sortActorsByGender(actors) { export async function fetchActorsById(actorIds, options = {}) { const [actors] = await Promise.all([ - knex('actors_meta') + knex('actors') .select( + 'actors.*', 'actors_meta.*', 'birth_countries.alpha2 as birth_country_alpha2', knex.raw('COALESCE(birth_countries.alias, birth_countries.name) as birth_country_name'), 'residence_countries.alpha2 as residence_country_alpha2', knex.raw('COALESCE(residence_countries.alias, residence_countries.name) as residence_country_name'), ) - .leftJoin('countries as birth_countries', 'birth_countries.alpha2', 'actors_meta.birth_country_alpha2') - .leftJoin('countries as residence_countries', 'residence_countries.alpha2', 'actors_meta.residence_country_alpha2') - .whereIn('actors_meta.id', actorIds) + .leftJoin('actors_meta', 'actors_meta.actor_id', 'actors.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') + .whereIn('actors.id', actorIds) .modify((builder) => { if (options.order) { builder.orderBy(...options.order); diff --git a/src/cache.js b/src/cache.js new file mode 100644 index 0000000..501bd49 --- /dev/null +++ b/src/cache.js @@ -0,0 +1,19 @@ +import redis from './redis.js'; + +export async function getIdsBySlug(slugs, domain) { + const ids = await Promise.all(slugs.map(async (slug) => { + if (!slug) { + return null; + } + + if (Number(slug)) { + return Number(slug); // already an ID or missing + } + + const id = await redis.hGet(`traxxx:${domain}:id_by_slug`, slug); + + return Number(id); + })); + + return ids.filter(Boolean); +} diff --git a/src/entities.js b/src/entities.js index 461de34..70b6d1b 100644 --- a/src/entities.js +++ b/src/entities.js @@ -86,6 +86,24 @@ export async function fetchEntitiesById(entityIds, options = {}) { return curatedEntities; } +export async function getEntityIdsBySlug(slugs, domain) { + const ids = await Promise.all(slugs.map(async (slug) => { + if (!slug) { + return null; + } + + if (Number(slug)) { + return Number(slug); // already an ID or missing + } + + const id = await redis.hGet(`traxxx:${domain}:id_by_slug`, slug); + + return Number(id); + })); + + return ids.filter(Boolean); +} + export async function cacheEntityIds() { const entities = await knex('entities').select('id', 'slug', 'type'); diff --git a/src/movies.js b/src/movies.js new file mode 100644 index 0000000..ce1d209 --- /dev/null +++ b/src/movies.js @@ -0,0 +1,365 @@ +import knex from './knex.js'; +import { searchApi } from './manticore.js'; +import { HttpError } from './errors.js'; +import { fetchActorsById, curateActor, sortActorsByGender } from './actors.js'; +import { fetchTagsById } from './tags.js'; +import { fetchEntitiesById } from './entities.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 curateMovie(rawScene, assets) { + if (!rawScene) { + return null; + } + + 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), + covers: assets.covers.map((cover) => curateMedia(cover)), + photos: assets.photos.map((photo) => curateMedia(photo)), + createdBatchId: rawScene.created_batch_id, + updatedBatchId: rawScene.updated_batch_id, + }; +} + +export async function fetchMoviesById(movieIds) { + const [movies, channels, actors, directors, tags, covers, photos] = await Promise.all([ + knex('movies').whereIn('id', movieIds), + // channels + knex('movies') + .select('channels.*', 'networks.id as network_id', 'networks.slug as network_slug', 'networks.name as network_name', 'networks.type as network_type') + .whereIn('movies.id', movieIds) + .leftJoin('entities as channels', 'channels.id', 'movies.entity_id') + .leftJoin('entities as networks', 'networks.id', 'channels.parent_id') + .groupBy('channels.id', 'networks.id'), + // actors + knex('movies') + .select( + 'actors.*', + 'actors_meta.*', + 'releases_actors.release_id', + 'movies.id as movie_id', + ) + .distinctOn('actors.id', 'movies.id') // cannot distinct on JSON column avatar, must specify + .whereIn('movies.id', movieIds) + .whereNotNull('actors.id') + .leftJoin('movies_scenes', 'movies_scenes.movie_id', 'movies.id') + .leftJoin('releases_actors', 'releases_actors.release_id', 'movies_scenes.scene_id') + .leftJoin('actors', 'actors.id', 'releases_actors.actor_id') + .leftJoin('actors_meta', 'actors_meta.actor_id', 'actors.id'), + // directors + knex('movies') + .whereIn('movies.id', movieIds) + .leftJoin('movies_scenes', 'movies_scenes.movie_id', 'movies.id') + .leftJoin('releases_directors', 'releases_directors.release_id', 'movies_scenes.scene_id') + .leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'), + // tags + knex('movies') + .select('tags.id', 'tags.slug', 'tags.name', 'tags.priority', 'movies.id as movie_id') + .distinct() + .whereIn('movies.id', movieIds) + .whereNotNull('tags.id') + .leftJoin('movies_scenes', 'movies_scenes.movie_id', 'movies.id') + .leftJoin('releases_tags', 'releases_tags.release_id', 'movies_scenes.scene_id') + .leftJoin('tags', 'tags.id', 'releases_tags.tag_id') + .orderBy('priority', 'desc'), + // covers + knex('movies_covers') + .whereIn('movie_id', movieIds) + .leftJoin('media', 'media.id', 'movies_covers.media_id'), + // photos + knex('movies') + .whereIn('movies.id', movieIds) + .leftJoin('movies_scenes', 'movies_scenes.movie_id', 'movies.id') + .leftJoin('releases_photos', 'releases_photos.release_id', 'movies_scenes.scene_id') + .leftJoin('media', 'media.id', 'releases_photos.media_id'), + ]); + + return movieIds.map((movieId) => { + const movie = movies.find((movieEntry) => movieEntry.id === movieId); + + if (!movie) { + console.warn('cannot find movie', movieId); + return null; + } + + const movieChannel = channels.find((entity) => entity.id === movie.entity_id); + const movieActors = actors.filter((actor) => actor.movie_id === movieId); + const movieDirectors = directors.filter((director) => director.release_id === movieId); + const movieTags = tags.filter((tag) => tag.movie_id === movieId); + const movieCovers = covers.filter((cover) => cover.movie_id === movieId); + const moviePhotos = photos.filter((photo) => photo.release_id === movieId); + + return curateMovie(movie, { + channel: movieChannel, + actors: movieActors, + directors: movieDirectors, + tags: movieTags, + covers: movieCovers, + photos: moviePhotos, + }); + }).filter(Boolean); +} + +function curateOptions(options) { + if (options?.limit > 100) { + throw new HttpError('Limit must be <= 100', 400); + } + + return { + limit: options?.limit || 30, + page: Number(options?.page) || 1, + aggregate: options.aggregate ?? true, + aggregateActors: (options.aggregate ?? true) && (options.aggregateActors ?? true), + aggregateTags: (options.aggregate ?? true) && (options.aggregateTags ?? true), + aggregateChannels: (options.aggregate ?? true) && (options.aggregateChannels ?? true), + }; +} + +function buildQuery(filters = {}) { + const query = { + bool: { + must: [], + }, + }; + + let sort = [{ effective_date: 'desc' }]; + + if (!filters.scope || filters.scope === 'latest') { + 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.scope === 'likes') { + sort = [{ stashed: 'desc' }, { effective_date: 'desc' }]; + } + + if (filters.scope === 'results') { + sort = [{ _score: 'desc' }, { effective_date: 'desc' }]; + } + + if (filters.query) { + query.bool.must.push({ + bool: { + should: [ + { match: { title_filtered: filters.query } }, + { match: { actors: filters.query } }, + { match: { tags: filters.query } }, + { match: { channel_name: filters.query } }, + { match: { network_name: filters.query } }, + { match: { channel_slug: filters.query } }, + { match: { network_slug: filters.query } }, + { match: { meta: filters.query } }, // date + ], + }, + }); + } + + if (filters.tagIds) { + filters.tagIds.forEach((tagId) => { + query.bool.must.push({ equals: { 'any(tag_ids)': tagId } }); + }); + } + + if (filters.entityId) { + query.bool.must.push({ + bool: { + should: [ + { equals: { channel_id: filters.entityId } }, + { equals: { network_id: filters.entityId } }, + ], + }, + }); + } + + if (filters.actorIds) { + filters.actorIds.forEach((actorId) => { + query.bool.must.push({ equals: { 'any(actor_ids)': actorId } }); + }); + } + + if (filters.requireCover) { + query.bool.must.push({ + equals: { + has_cover: 1, + }, + }); + } + + /* tag filter + must_not: [ + { + in: { + 'any(tag_ids)': [101, 180, 32], + }, + }, + ], + */ + + return { query, sort }; +} + +function buildAggregates(options) { + const aggregates = {}; + + if (options.aggregateActors) { + aggregates.actorIds = { + terms: { + field: 'actor_ids', + size: 5000, + }, + // sort: [{ doc_count: { order: 'asc' } }], + }; + } + + if (options.aggregateTags) { + aggregates.tagIds = { + terms: { + field: 'tag_ids', + size: 1000, + }, + }; + } + + if (options.aggregateChannels) { + aggregates.channelIds = { + terms: { + field: 'channel_id', + size: 1000, + }, + }; + } + + return aggregates; +} + +function countAggregations(buckets) { + if (!buckets) { + return null; + } + + return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }])); +} + +export async function fetchMovies(filters, rawOptions) { + const options = curateOptions(rawOptions); + const { query, sort } = buildQuery(filters); + + console.log('filters', filters); + console.log('options', options); + console.log('query', query.bool.must); + + const result = await searchApi.search({ + index: 'movies', + query, + limit: options.limit, + offset: (options.page - 1) * options.limit, + sort, + aggs: buildAggregates(options), + options: { + max_matches: 1000, + max_query_time: 10000, + field_weights: { + title_filtered: 7, + actors: 10, + tags: 9, + meta: 6, + channel_name: 2, + channel_slug: 3, + network_name: 1, + network_slug: 1, + }, + }, + }); + + 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); + + 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 }) : [], + ]); + + const movieIds = result.hits.hits.map((hit) => Number(hit._id)); + const movies = await fetchMoviesById(movieIds); + + return { + movies, + aggActors, + aggTags, + aggChannels, + total: result.hits.total, + limit: options.limit, + }; +} diff --git a/src/scenes.js b/src/scenes.js index 5dd080f..12dd726 100644 --- a/src/scenes.js +++ b/src/scenes.js @@ -80,6 +80,7 @@ export async function fetchScenesById(sceneIds) { .groupBy('channels.id', 'networks.id'), knex('releases_actors') .select( + 'actors.*', 'actors_meta.*', 'releases_actors.release_id', /* why would we need this for scenes? @@ -90,7 +91,8 @@ export async function fetchScenesById(sceneIds) { */ ) .whereIn('release_id', sceneIds) - .leftJoin('actors_meta', 'actors_meta.id', 'releases_actors.actor_id'), + .leftJoin('actors', 'actors.id', 'releases_actors.actor_id') + .leftJoin('actors_meta', 'actors_meta.actor_id', 'actors.id'), /* .leftJoin('countries as birth_countries', 'birth_countries.alpha2', 'actors_meta.birth_country_alpha2') .leftJoin('countries as residence_countries', 'residence_countries.alpha2', 'actors_meta.residence_country_alpha2'), @@ -321,8 +323,6 @@ export async function fetchScenes(filters, rawOptions) { }, }); - console.log(result.hits.hits); - 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); diff --git a/src/web/movies.js b/src/web/movies.js new file mode 100644 index 0000000..2bc5c89 --- /dev/null +++ b/src/web/movies.js @@ -0,0 +1,39 @@ +import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */ + +import { fetchMovies } from '../movies.js'; +import { parseActorIdentifier } from '../query.js'; +import { getIdsBySlug } from '../cache.js'; + +export async function curateMoviesQuery(query) { + return { + scope: query.scope || 'latest', + query: query.q, + 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, + requireCover: query.cover, + }; +} + +export async function fetchMoviesApi(req, res) { + const { + movies, + aggActors, + aggTags, + aggChannels, + limit, + total, + } = await fetchMovies(await curateMoviesQuery(req.query), { + page: Number(req.query.page) || 1, + limit: Number(req.query.limit) || 30, + }); + + res.send(stringify({ + movies, + aggActors, + aggTags, + aggChannels, + limit, + total, + })); +} diff --git a/src/web/scenes.js b/src/web/scenes.js index 2a2e211..b25fc6e 100644 --- a/src/web/scenes.js +++ b/src/web/scenes.js @@ -2,25 +2,7 @@ import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disab import { fetchScenes } from '../scenes.js'; import { parseActorIdentifier } from '../query.js'; -import redis from '../redis.js'; - -async function getIdsBySlug(slugs, domain) { - const ids = await Promise.all(slugs.map(async (slug) => { - if (!slug) { - return null; - } - - if (Number(slug)) { - return Number(slug); // already an ID or missing - } - - const id = await redis.hGet(`traxxx:${domain}:id_by_slug`, slug); - - return Number(id); - })); - - return ids.filter(Boolean); -} +import { getIdsBySlug } from '../cache.js'; export async function curateScenesQuery(query) { return { diff --git a/src/web/server.js b/src/web/server.js index 3622c17..66b61fc 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -22,6 +22,7 @@ import { renderPage } from 'vike/server'; // eslint-disable-line import/extensio import { fetchScenesApi } from './scenes.js'; import { fetchActorsApi } from './actors.js'; +import { fetchMoviesApi } from './movies.js'; import initLogger from '../logger.js'; @@ -62,9 +63,11 @@ export default async function initServer() { router.use(viteDevMiddleware); } + router.get('/api/scenes', fetchScenesApi); + router.get('/api/actors', fetchActorsApi); - router.get('/api/scenes', fetchScenesApi); + router.get('/api/movies', fetchMoviesApi); // ... // Other middlewares (e.g. some RPC middleware such as Telefunc) diff --git a/static b/static index 62c8a04..a501e96 160000 --- a/static +++ b/static @@ -1 +1 @@ -Subproject commit 62c8a043692d973e6aadb45f3ca3dfc78466f3b6 +Subproject commit a501e96b89785229e447456bfaf2b17954139121