Merge dev (0.53.1 Plex hotfix) into r5-data-integrity
This commit is contained in:
+11
-8
@@ -1020,16 +1020,19 @@ class PlexShow(Base, TimestampMixin, UpdatedAtMixin):
|
|||||||
thumb_key: Mapped[str | None] = mapped_column(String(512)) # Plex thumb path (proxied, not stored)
|
thumb_key: Mapped[str | None] = mapped_column(String(512)) # Plex thumb path (proxied, not stored)
|
||||||
art_key: Mapped[str | None] = mapped_column(String(512))
|
art_key: Mapped[str | None] = mapped_column(String(512))
|
||||||
child_count: Mapped[int | None] = mapped_column(Integer) # seasons
|
child_count: Mapped[int | None] = mapped_column(Integer) # seasons
|
||||||
collection_keys: Mapped[list | None] = mapped_column(JSONB) # Plex collections this show is in
|
collection_keys: Mapped[list | None] = mapped_column(JSONB(none_as_null=True)) # Plex collections this show is in
|
||||||
added_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
|
added_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
|
||||||
# Filterable / orderable metadata mirrored from the show section listing (parallels PlexItem).
|
# Filterable / orderable metadata mirrored from the show section listing (parallels PlexItem).
|
||||||
rating: Mapped[float | None] = mapped_column(Float, index=True) # Plex audienceRating (~IMDb)
|
rating: Mapped[float | None] = mapped_column(Float, index=True) # Plex audienceRating (~IMDb)
|
||||||
content_rating: Mapped[str | None] = mapped_column(String(16), index=True)
|
content_rating: Mapped[str | None] = mapped_column(String(16), index=True)
|
||||||
studio: Mapped[str | None] = mapped_column(String(255), index=True) # network/studio
|
studio: Mapped[str | None] = mapped_column(String(255), index=True) # network/studio
|
||||||
originally_available_at: Mapped[Date | None] = mapped_column(Date, index=True) # first air date
|
originally_available_at: Mapped[Date | None] = mapped_column(Date, index=True) # first air date
|
||||||
genres: Mapped[list | None] = mapped_column(JSONB)
|
# none_as_null: an empty tag list must persist as SQL NULL, not JSON `null`. Without it a Python
|
||||||
directors: Mapped[list | None] = mapped_column(JSONB)
|
# None round-trips as a JSON scalar that jsonb_array_elements_text() can't unnest ("cannot extract
|
||||||
cast_names: Mapped[list | None] = mapped_column(JSONB)
|
# elements from a scalar") — the trap a metadata-poor library sprang on /facets. Same on plex_items.
|
||||||
|
genres: Mapped[list | None] = mapped_column(JSONB(none_as_null=True))
|
||||||
|
directors: Mapped[list | None] = mapped_column(JSONB(none_as_null=True))
|
||||||
|
cast_names: Mapped[list | None] = mapped_column(JSONB(none_as_null=True))
|
||||||
people_text: Mapped[str | None] = mapped_column(Text) # cast+directors, folded into search_vector
|
people_text: Mapped[str | None] = mapped_column(Text) # cast+directors, folded into search_vector
|
||||||
# Set once the show's FULL cast has been rich-fetched by the backfill; NULL = still the listing top-3.
|
# Set once the show's FULL cast has been rich-fetched by the backfill; NULL = still the listing top-3.
|
||||||
people_enriched_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
people_enriched_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||||
@@ -1116,11 +1119,11 @@ class PlexItem(Base, TimestampMixin, UpdatedAtMixin):
|
|||||||
content_rating: Mapped[str | None] = mapped_column(String(16), index=True)
|
content_rating: Mapped[str | None] = mapped_column(String(16), index=True)
|
||||||
studio: Mapped[str | None] = mapped_column(String(255), index=True)
|
studio: Mapped[str | None] = mapped_column(String(255), index=True)
|
||||||
originally_available_at: Mapped[Date | None] = mapped_column(Date, index=True)
|
originally_available_at: Mapped[Date | None] = mapped_column(Date, index=True)
|
||||||
genres: Mapped[list | None] = mapped_column(JSONB)
|
genres: Mapped[list | None] = mapped_column(JSONB(none_as_null=True))
|
||||||
directors: Mapped[list | None] = mapped_column(JSONB)
|
directors: Mapped[list | None] = mapped_column(JSONB(none_as_null=True))
|
||||||
cast_names: Mapped[list | None] = mapped_column(JSONB)
|
cast_names: Mapped[list | None] = mapped_column(JSONB(none_as_null=True))
|
||||||
# rating_keys of the Plex collections this movie belongs to (GIN — filter + card-strip siblings).
|
# rating_keys of the Plex collections this movie belongs to (GIN — filter + card-strip siblings).
|
||||||
collection_keys: Mapped[list | None] = mapped_column(JSONB)
|
collection_keys: Mapped[list | None] = mapped_column(JSONB(none_as_null=True))
|
||||||
# Cast + director names, space-joined — folded into search_vector (weight B) so the search box
|
# Cast + director names, space-joined — folded into search_vector (weight B) so the search box
|
||||||
# finds titles by a person's name (and ranks them above summary-only mentions).
|
# finds titles by a person's name (and ranks them above summary-only mentions).
|
||||||
people_text: Mapped[str | None] = mapped_column(Text)
|
people_text: Mapped[str | None] = mapped_column(Text)
|
||||||
|
|||||||
@@ -675,10 +675,15 @@ def facets(
|
|||||||
# the result down to zero, i.e. a dead-end chip. (The already-selected genres still appear, since
|
# the result down to zero, i.e. a dead-end chip. (The already-selected genres still appear, since
|
||||||
# every matching title carries them, so they stay togglable.)
|
# every matching title carries them, so they stay togglable.)
|
||||||
genre_pp = p if p.get("genre_mode") == "all" else {**p, "genres": None}
|
genre_pp = p if p.get("genre_mode") == "all" else {**p, "genres": None}
|
||||||
|
# Only rows whose `genres` is a JSON array can be unnested. A genre-less item stores JSON
|
||||||
|
# `null` (SQLAlchemy's JSONB persists a Python None as JSON null, not SQL NULL), and
|
||||||
|
# jsonb_array_elements_text() raises on a scalar ("cannot extract elements from a scalar") —
|
||||||
|
# so a single metadata-poor library (e.g. one bare Shows library with no genres) 500'd the
|
||||||
|
# whole facets endpoint. Mirror the exact guard people.py already applies to the same columns.
|
||||||
expanded = wsq(
|
expanded = wsq(
|
||||||
db.query(func.jsonb_array_elements_text(model.genres).label("g")),
|
db.query(func.jsonb_array_elements_text(model.genres).label("g")),
|
||||||
model, ids, genre_pp, kind_movie, True,
|
model, ids, genre_pp, kind_movie, True,
|
||||||
).subquery()
|
).filter(func.jsonb_typeof(model.genres) == "array").subquery()
|
||||||
for g, c in db.query(expanded.c.g, func.count()).group_by(expanded.c.g).all():
|
for g, c in db.query(expanded.c.g, func.count()).group_by(expanded.c.g).all():
|
||||||
genre_counts[g] = genre_counts.get(g, 0) + c
|
genre_counts[g] = genre_counts.get(g, 0) + c
|
||||||
# Content ratings — exclude its own.
|
# Content ratings — exclude its own.
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ export interface ReleaseEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const RELEASE_NOTES: ReleaseEntry[] = [
|
export const RELEASE_NOTES: ReleaseEntry[] = [
|
||||||
|
{
|
||||||
|
version: "0.53.1",
|
||||||
|
date: "2026-07-25",
|
||||||
|
summary: "The Plex page no longer errors when a mirrored library has items without genres.",
|
||||||
|
fixes: [
|
||||||
|
"Opening the Plex module could fail with a server error when a mirrored library contained items with no genre metadata (for example a freshly added, sparsely-tagged library). The filter sidebar now handles genre-less items correctly, so the page loads normally.",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
version: "0.53.0",
|
version: "0.53.0",
|
||||||
date: "2026-07-23",
|
date: "2026-07-23",
|
||||||
|
|||||||
Reference in New Issue
Block a user