feat(i18n): every server A→Z list in the reader's alphabet (E30 S3)

The Postgres image is Alpine, whose en_US.utf8 is byte order: "Életrajzi" and
"Ötödik" sorted after "Zorró" in the feed, the channel lists, the Plex grid,
its genres and collections. `app/collation.py` names the ICU collation of the
reader's language (hu-x-icu puts "Ö" after "O" and "Cs" after "Cu"), wraps
every text ORDER BY with it - the feed's keyset cursor compares with the same
expression, so paging walks the order it sorted in - and orders the lists built
in Python (genre and studio facets, collection members, the discovery re-sort)
through the same collation. Plex title tiebreaks use the shown (HU) title.
This commit is contained in:
2026-09-29 22:39:57 +02:00
parent 584d082b6b
commit 67c9f10153
15 changed files with 328 additions and 38 deletions
+6 -5
View File
@@ -15,6 +15,7 @@ from datetime import date, datetime, timedelta, timezone
from sqlalchemy import BigInteger, Select, and_, cast, false, func, or_, select
from sqlalchemy.orm import Session, aliased
from app.collation import collated
from app.models import (
LIVE_OR_UPCOMING,
BlockedChannel,
@@ -478,8 +479,10 @@ def feed_years(db: Session, user: User, **params) -> list[dict]:
# replaces OFFSET/LIMIT: deep scrolling no longer scans-and-discards skipped rows,
# and the page boundary is stable even as the scheduler ingests new videos mid-scroll
# (OFFSET would shift and duplicate/skip rows).
def sort_keys(sort: str, seed: int, rank_expr=None) -> list[dict]:
def sort_keys(sort: str, seed: int, rank_expr=None, lang: str | None = None) -> list[dict]:
"""`lang` orders the title sorts in the reader's alphabet (`collation`); the other keys ignore it."""
pub = {"expr": Video.published_at, "asc": False, "nullable": False, "kind": "dt"}
title = collated(Video.title, lang)
table: dict[str, list[dict]] = {
"newest": [pub],
"oldest": [{**pub, "asc": True}],
@@ -491,10 +494,8 @@ def sort_keys(sort: str, seed: int, rank_expr=None) -> list[dict]:
"duration_asc": [
{"expr": Video.duration_seconds, "asc": True, "nullable": True, "kind": "num"}
],
"title": [{"expr": func.lower(Video.title), "asc": True, "nullable": True, "kind": "str"}],
"title_desc": [
{"expr": func.lower(Video.title), "asc": False, "nullable": True, "kind": "str"}
],
"title": [{"expr": title, "asc": True, "nullable": True, "kind": "str"}],
"title_desc": [{"expr": title, "asc": False, "nullable": True, "kind": "str"}],
"subscribers": [
{"expr": Channel.subscriber_count, "asc": False, "nullable": True, "kind": "num"}
],