+
+ updateRange(positions, false)"
+ @change="(positions) => updateRange(positions, true)"
+ >
+ {{ slider.display[1] }}
+ {{ slider.display[0] }}
+
+
diff --git a/components/movies/movies.vue b/components/movies/movies.vue
index e8294d8..3bd6766 100644
--- a/components/movies/movies.vue
+++ b/components/movies/movies.vue
@@ -15,6 +15,7 @@ import { get } from '#/src/api.js';
import events from '#/src/events.js';
import navigate from '#/src/navigate.js';
import { getActorIdentifier, parseActorIdentifier } from '#/src/query.js';
+import { formatYears, parseYears } from '#/utils/parse-range.js';
const pageContext = inject('pageContext');
const { pageProps, routeParams, urlParsed } = pageContext;
@@ -49,7 +50,7 @@ const queryEntity = networks[urlParsed.search.e] || channels[urlParsed.search.e]
const filters = ref({
search: urlParsed.search.q,
- years: urlParsed.search.years?.split(',').map((year) => Number(year)).filter(Boolean) || [],
+ ...parseYears(urlParsed.search.years),
tags: urlParsed.search.tags?.split(',').filter(Boolean) || [],
entity: queryEntity,
actors: queryActors,
@@ -103,7 +104,7 @@ async function search(options = {}) {
navigate(getPath(scope.value, false), {
...query,
- years: filters.value.years.join(',') || undefined,
+ years: formatYears(filters.value),
actors: filters.value.actors.map((filterActor) => getActorIdentifier(filterActor)).join(',') || undefined, // don't include page actor ID in query, already a parameter
tags: filters.value.tags.join(',') || undefined,
e: filters.value.entity?.type === 'network' ? `_${filters.value.entity.slug}` : (filters.value.entity?.slug || undefined),
@@ -111,7 +112,7 @@ async function search(options = {}) {
const res = await get('/movies', {
...query,
- years: filters.value.years.filter(Boolean).join(','), // if we're on an actor page, that actor ID needs to be included
+ years: formatYears(filters.value),
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(','),
e: entitySlug,
diff --git a/components/scenes/scenes.vue b/components/scenes/scenes.vue
index 6194c8f..97d9011 100644
--- a/components/scenes/scenes.vue
+++ b/components/scenes/scenes.vue
@@ -24,7 +24,7 @@ import { get } from '#/src/api.js';
import entityPrefixes from '#/src/entities-prefixes.js';
import navigate from '#/src/navigate.js';
import { getActorIdentifier, parseActorIdentifier } from '#/src/query.js';
-import { formatRange, parseRange } from '#/utils/parse-range.js';
+import { formatRange, formatYears, parseRange, parseYears } from '#/utils/parse-range.js';
const props = defineProps({
showFilters: {
@@ -86,7 +86,7 @@ const queryEntity = networks[urlParsed.search.e] || channels[urlParsed.search.e]
const filters = ref({
search: urlParsed.search.q,
- years: urlParsed.search.years?.split(',').filter(Boolean).map(Number) || [],
+ ...parseYears(urlParsed.search.years),
tags: urlParsed.search.tags?.split(',').filter(Boolean) || [],
onlyActorTags: Object.hasOwn(urlParsed.search, 'at'),
entity: queryEntity,
@@ -153,7 +153,7 @@ async function search(options = {}) {
navigate(getPath(scope.value, false), {
...query,
- years: filters.value.years.join(',') || undefined,
+ years: formatYears(filters.value),
actors: filters.value.actors.map((filterActor) => getActorIdentifier(filterActor)).join(',') || undefined, // don't include page actor ID in query, already a parameter
tags: filters.value.tags.join(',') || undefined,
at: (filters.value.tags.length > 0 && filters.value.onlyActorTags) || undefined,
@@ -163,7 +163,7 @@ async function search(options = {}) {
const res = await get('/scenes', {
...query,
- years: filters.value.years.filter(Boolean).join(','), // if we're on an actor page, that actor ID needs to be included
+ years: formatYears(filters.value),
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(','),
at: !!filters.value.onlyActorTags,
diff --git a/src/movies.js b/src/movies.js
index ee58abb..09f17c7 100644
--- a/src/movies.js
+++ b/src/movies.js
@@ -289,6 +289,14 @@ async function queryManticoreSql(filters, options) {
builder.whereIn('effective_year', filters.years);
}
+ if (typeof filters.yearRange?.min === 'number') {
+ builder.where('effective_year', '>=', filters.yearRange.min);
+ }
+
+ if (typeof filters.yearRange?.max === 'number') {
+ builder.where('effective_year', '<=', filters.yearRange.max);
+ }
+
filters.tagIds?.forEach((tagId) => {
builder.where('any(tag_ids)', tagId);
});
@@ -428,9 +436,24 @@ export async function fetchMovies(filters, rawOptions, reqUser, context) {
console.log(options);
console.log(filters);
- const result = await queryManticoreSql(filters, options);
+ // an active year filter would narrow the year range to the filtered years, get the full range from a lightweight parallel query
+ const hasYearFilter = options.aggregateYears && (filters.years?.length > 0 || !!filters.yearRange);
- const aggYears = options.aggregateYears && result.aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count }));
+ const yearsOptions = {
+ ...options,
+ limit: 1,
+ page: 1,
+ aggregateActors: false,
+ aggregateTags: false,
+ aggregateChannels: false,
+ };
+
+ const [result, yearsResult] = await Promise.all([
+ queryManticoreSql(filters, options),
+ hasYearFilter ? queryManticoreSql({ ...filters, years: [], yearRange: null }, yearsOptions) : null,
+ ]);
+
+ const aggYears = options.aggregateYears && (yearsResult || result).aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count }));
const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds);
const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds);
diff --git a/src/scenes.js b/src/scenes.js
index 1a16dd7..7a8be5e 100644
--- a/src/scenes.js
+++ b/src/scenes.js
@@ -558,6 +558,14 @@ async function queryManticoreSql(filters, options, _reqUser) {
builder.whereIn('effective_year', filters.years);
}
+ if (typeof filters.yearRange?.min === 'number') {
+ builder.where('effective_year', '>=', filters.yearRange.min);
+ }
+
+ if (typeof filters.yearRange?.max === 'number') {
+ builder.where('effective_year', '<=', filters.yearRange.max);
+ }
+
filters.tagIds?.forEach((tagId) => {
if (filters.onlyActorTags) {
builder.where((whereBuilder) => {
@@ -776,18 +784,22 @@ export async function fetchScenes(filters, rawOptions, reqUser, context) {
console.log('options', options);
}
- // an active count filter would narrow its own range to the filtered value, get the unfiltered ranges from a lightweight parallel query (~10ms vs 40-500ms for the main query with its actor and tag facets)
+ // an active count or year filter would narrow its own aggregation to the filtered values, get the unfiltered ranges from a lightweight parallel query (~10ms vs 40-500ms for the main query with its actor and tag facets)
+ // the range filters are stripped together, so with several active, each of their aggregations ignores the others
const activeCountKeys = options.aggregateActorCounts
? Object.keys(actorCountFilters).filter((key) => filters[actorCountFilters[key]])
: [];
- const countsFilters = Object.fromEntries(Object.entries(filters).filter(([key]) => !Object.values(actorCountFilters).includes(key)));
+ const hasYearFilter = options.aggregateYears && (filters.years?.length > 0 || !!filters.yearRange);
+ const rangeFilterKeys = [...Object.values(actorCountFilters), 'years', 'yearRange'];
+
+ const countsFilters = Object.fromEntries(Object.entries(filters).filter(([key]) => !rangeFilterKeys.includes(key)));
const countsOptions = {
...options,
limit: 1,
page: 1,
- aggregateYears: false,
+ aggregateYears: hasYearFilter,
aggregateActors: false,
aggregateTags: false,
aggregateChannels: false,
@@ -797,12 +809,12 @@ export async function fetchScenes(filters, rawOptions, reqUser, context) {
const [result, countsResult] = await Promise.all([
queryManticoreSql(filters, options, reqUser),
- activeCountKeys.length > 0 ? queryManticoreSql(countsFilters, countsOptions, reqUser) : null,
+ activeCountKeys.length > 0 || hasYearFilter ? queryManticoreSql(countsFilters, countsOptions, reqUser) : null,
]);
console.timeEnd('manticore sql');
- const aggYears = options.aggregateYears && result.aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count }));
+ const aggYears = options.aggregateYears && (hasYearFilter ? countsResult : result).aggregations.years.map((bucket) => ({ year: bucket.key, count: bucket.doc_count }));
const entityIds = options.aggregateChannels && [...(result.aggregations.channelIds || []), ...(result.aggregations.studioIds || [])];
// keyed by URL parameter, e.g. { group: [{ value: 2, count: 1234 }, ...], females: [...] }, sorted by value so the first and last bucket give the range
diff --git a/src/web/movies.js b/src/web/movies.js
index c671608..cae57cb 100644
--- a/src/web/movies.js
+++ b/src/web/movies.js
@@ -1,3 +1,4 @@
+import { parseYears } from '../../utils/parse-range.js';
import promiseProps from '../../utils/promise-props.js';
import slugify from '../../utils/slugify.js';
import { getIdsBySlug } from '../cache.js';
@@ -10,7 +11,7 @@ export async function curateMoviesQuery(query) {
return {
scope: query.scope || 'latest',
query: query.q,
- years: query.years?.split(',')?.map((year) => Number(year)).filter(Boolean) || [],
+ ...parseYears(query.years),
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,
diff --git a/src/web/scenes.js b/src/web/scenes.js
index cf627bf..8a5b657 100644
--- a/src/web/scenes.js
+++ b/src/web/scenes.js
@@ -1,6 +1,6 @@
import { Router } from 'express';
-import { parseRange } from '../../utils/parse-range.js';
+import { parseRange, parseYears } from '../../utils/parse-range.js';
import promiseProps from '../../utils/promise-props.js';
import slugify from '../../utils/slugify.js';
@@ -18,7 +18,7 @@ import {
import { fetchStashByUsernameAndSlug } from '../stashes.js';
export async function curateScenesQuery(query) {
- const splitYears = query.years?.split(',') || [];
+ const { years, yearRange } = parseYears(query.years);
const splitTags = query.tags?.split(',') || [];
const splitActors = query.actors?.split(',') || [];
const splitEntities = query.e?.split(',') || [];
@@ -39,7 +39,8 @@ export async function curateScenesQuery(query) {
return {
scope: query.scope || 'latest',
query: query.q,
- years: splitYears.map((year) => Number(year)).filter(Boolean) || [],
+ years,
+ yearRange,
actorIds: [query.actorId, ...splitActors.filter((actor) => actor.charAt(0) !== '!').map((identifier) => parseActorIdentifier(identifier)?.id)].filter(Boolean),
notActorIds: splitActors.filter((actor) => actor.charAt(0) === '!').map((identifier) => parseActorIdentifier(identifier.slice(1))?.id).filter(Boolean),
actorCount: parseRange(query.group),
diff --git a/tools/manticore-actors.js b/tools/manticore-actors.js
index fdec725..92607a5 100644
--- a/tools/manticore-actors.js
+++ b/tools/manticore-actors.js
@@ -8,6 +8,7 @@ import { syncManticoreActors } from '../src/sync.js';
async function init() {
if (argv.update) {
await utilsApi.sql('drop table if exists actors');
+ // bigint rather than timestamp, which is unsigned 32-bit and wraps dates before 1970 into the 2100s
await utilsApi.sql(`create table actors(
id int,
name text,
@@ -16,7 +17,7 @@ async function init() {
entity_id int,
alias_for int,
gender string,
- date_of_birth timestamp,
+ date_of_birth bigint,
country string,
has_avatar bool,
mass int,
diff --git a/tools/manticore-movies.js b/tools/manticore-movies.js
index 5143742..08e78f5 100644
--- a/tools/manticore-movies.js
+++ b/tools/manticore-movies.js
@@ -7,6 +7,7 @@ import { syncManticoreMovies } from '../src/sync.js';
async function init() {
if (argv.update) {
await utilsApi.sql('drop table if exists movies');
+ // bigint rather than timestamp, which is unsigned 32-bit and wraps dates before 1970 into the 2100s
await utilsApi.sql(`create table movies (
id int,
title text,
@@ -23,10 +24,10 @@ async function init() {
tag_ids multi,
tags text,
meta text,
- date timestamp,
+ date bigint,
has_cover bool,
- created_at timestamp,
- effective_date timestamp,
+ created_at bigint,
+ effective_date bigint,
stashed int,
stashed_scenes int,
stashed_total int,
diff --git a/tools/manticore-scenes.js b/tools/manticore-scenes.js
index 03d9854..11f1f4a 100644
--- a/tools/manticore-scenes.js
+++ b/tools/manticore-scenes.js
@@ -7,6 +7,7 @@ import { syncManticoreScenes } from '../src/sync.js';
async function init() {
if (argv.update) {
await utilsApi.sql('drop table if exists scenes');
+ // bigint rather than timestamp, which is unsigned 32-bit and wraps dates before 1970 into the 2100s
await utilsApi.sql(`create table scenes (
id int,
title text,
@@ -38,11 +39,11 @@ async function init() {
serie_ids multi,
series text,
meta text,
- date timestamp,
+ date bigint,
fingerprints text,
is_showcased bool,
- created_at timestamp,
- effective_date timestamp,
+ created_at bigint,
+ effective_date bigint,
stashed int,
dupe_index int
)`);
diff --git a/utils/parse-range.js b/utils/parse-range.js
index 4cd7b83..e929d3d 100644
--- a/utils/parse-range.js
+++ b/utils/parse-range.js
@@ -38,3 +38,19 @@ export function formatRange(range) {
return `${range.min ?? ''}-${range.max ?? ''}`;
}
+
+// years are a range like 2015-2020 or 2020-, or a list like 2015,2019 from older links
+export function parseYears(yearsString) {
+ if (yearsString?.includes('-')) {
+ return { years: [], yearRange: parseRange(yearsString) };
+ }
+
+ return {
+ years: yearsString?.split(',').map((year) => Number(year)).filter(Boolean) || [],
+ yearRange: null,
+ };
+}
+
+export function formatYears({ years, yearRange }) {
+ return formatRange(yearRange) || years?.filter(Boolean).join(',') || undefined;
+}