feat(filters): pick one year from a decade grid, in the feed and in Plex

A shared YearPicker: the recent years as chips with their counts, and a
More dialog that lays every year out by decade (sortable by count), so a
ninety-year library fits on one screen. The feed's scrolling popover gives
way to it; the Plex rail gets it above its range fields, a pick setting
both ends. The Plex facets now count titles per year, the year filter left
out.
This commit is contained in:
2026-11-14 15:08:50 +01:00
parent 94f9ffdb1c
commit 1dff809268
10 changed files with 338 additions and 161 deletions
+14 -11
View File
@@ -242,6 +242,7 @@ def facets(db: Session, user: User, *, scope: str, show: str, lang: str, f: Libr
"content_ratings": [],
"studios": [],
"playable": [],
"years": [],
"year_min": None,
"year_max": None,
"rating_max": None,
@@ -302,7 +303,7 @@ def facets(db: Session, user: User, *, scope: str, show: str, lang: str, f: Libr
cr_counts: dict[str, int] = {}
studio_counts: dict[str, int] = {}
playable_counts: dict[str, int] = {}
years: list[int] = []
year_counts: dict[int, int] = {}
ratings: list[float] = []
durs: list[int] = []
@@ -383,21 +384,22 @@ def facets(db: Session, user: User, *, scope: str, show: str, lang: str, f: Libr
)
for cls, c in db.execute(pcq).all():
playable_counts[cls] = playable_counts.get(cls, 0) + c
# Year bounds — exclude the year filter.
by = db.execute(
# Titles per year — exclude the year filter, so a picked year never empties the list it was
# picked from. The year bounds are read from the same counts.
yq = (
wsq(
select(func.min(model.year), func.max(model.year)),
select(model.year, func.count()),
model,
ids,
{**p, "year_min": None, "year_max": None},
kind_movie,
True,
)
).one()
if by[0] is not None:
years.append(by[0])
if by[1] is not None:
years.append(by[1])
.where(model.year.isnot(None))
.group_by(model.year)
)
for y, c in db.execute(yq).all():
year_counts[y] = year_counts.get(y, 0) + c
# Rating max — exclude the rating filter. Coalesce(imdb, audience) to match the score the
# rating filter and the UI badge use (IMDb preferred, audience fallback).
br = db.scalar(
@@ -465,8 +467,9 @@ def facets(db: Session, user: User, *, scope: str, show: str, lang: str, f: Libr
for c in PLAYBACK_CLASSES
if c in playable_counts
],
"year_min": min(years) if years else None,
"year_max": max(years) if years else None,
"years": [{"year": y, "count": c} for y, c in sorted(year_counts.items(), reverse=True)],
"year_min": min(year_counts) if year_counts else None,
"year_max": max(year_counts) if year_counts else None,
"rating_max": max(ratings) if ratings else None,
"duration_min": min(durs) if durs else None,
"duration_max": max(durs) if durs else None,