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.
40 lines
1.8 KiB
Python
40 lines
1.8 KiB
Python
"""Alphabetical order in the reader's language (E30 S3).
|
|
|
|
Why not the database default: the Postgres image is Alpine, and musl's `en_US.utf8` collation is BYTE
|
|
order — every accented initial sorted after "Z" ("Zorró < Életrajzi < Ötödik"), in every A→Z list the
|
|
server orders. Postgres ships the ICU collations on the same image (dev, test, prod alike), so a text
|
|
sort names one: Hungarian puts "Ö" after "O" and "Cs" after "Cu", English folds the accents in.
|
|
|
|
`collated` wraps an ORDER BY expression — and because a keyset cursor compares with the SAME expression
|
|
(`feedquery.sort_keys`), the cursor pages in the same order it sorted in. `sorted_words` orders a Python
|
|
list the same way through the database, so a list built in Python matches one built in SQL exactly."""
|
|
|
|
from typing import Any
|
|
|
|
from sqlalchemy import SQLColumnExpression, column, func, select, values
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy.sql.elements import ColumnElement
|
|
from sqlalchemy.types import String
|
|
|
|
from app.mailtext import normalize
|
|
|
|
ICU: dict[str, str] = {"en": "en-x-icu", "hu": "hu-x-icu"}
|
|
|
|
|
|
def collation(lang: str | None) -> str:
|
|
"""The ICU collation for a UI language; anything unknown sorts as English."""
|
|
return ICU[normalize(lang)]
|
|
|
|
|
|
def collated(expr: SQLColumnExpression[Any], lang: str | None) -> ColumnElement[Any]:
|
|
"""`expr` compared in the reader's alphabet. Case-folded, so "apple" and "Apple" sit together."""
|
|
return func.lower(expr).collate(collation(lang))
|
|
|
|
|
|
def sorted_words(db: Session, words: list[str], lang: str | None) -> list[str]:
|
|
"""`words` in the reader's alphabetical order, by the same ICU collation as `collated`."""
|
|
if not words:
|
|
return []
|
|
w = values(column("w", String), name="words").data([(x,) for x in words])
|
|
return list(db.scalars(select(w.c.w).order_by(collated(w.c.w, lang), w.c.w)))
|